Compare commits

...
2533 Commits
Author SHA1 Message Date
Tim Baek 9c9a18d6d4 Merge pull request #21971 from open-webui/dev
0.8.6
2026-03-02 01:03:55 +04:00
Shirasawa 67893b9a57 fix: fix memory leaking in CodeEditor (#22110) 2026-03-01 15:52:20 -05:00
Timothy Jaeryang Baek 2e8c4da17b refac 2026-03-01 14:45:35 -06:00
Timothy Jaeryang Baek ff9f761d65 refac 2026-03-01 14:44:12 -06:00
Timothy Jaeryang Baek 5645d5bccc refac 2026-03-01 14:38:10 -06:00
Timothy Jaeryang Baek 201b93bfcc refac 2026-03-01 14:18:57 -06:00
Timothy Jaeryang Baek 0c2e4270bc chore: format 2026-03-01 14:10:45 -06:00
Timothy Jaeryang Baek 80ad5fd2d0 refac 2026-03-01 14:06:26 -06:00
Shirasawa 9904566513 fix: fix memory leaking in Chat.svelte (#21962)
* fix: fix memory leaking in Chat.svelte

* chore: remove useless chatIdUnsubscriber var

* fix: fix async tick
2026-03-01 15:04:47 -05:00
Classic298 2054ee0b73 fix: enforce ownership check on user-memory collection queries (#22109)
* fix: enforce ownership check on user-memory collection queries

fix: enforce ownership check on user-memory collection queries

Prevent authenticated users from querying other users' memory
collections via the /query/doc and /query/collection endpoints.
A new _validate_collection_access helper rejects requests for
user-memory-{UUID} collections where the UUID does not match
the requesting user. Admins bypass the check.

* Update retrieval.py

* Update retrieval.py
2026-03-01 15:03:37 -05:00
Timothy Jaeryang Baek 93bab8d822 refac 2026-03-01 13:54:44 -06:00
Timothy Jaeryang Baek 259d5ca596 refac 2026-03-01 13:49:36 -06:00
Classic298 597883a179 perf: use structuredClone and fast-path comparison in UserMessage (#22098)
Same optimization as the merged ResponseMessage PR: replace JSON.parse(JSON.stringify()) with structuredClone and add an O(1) fast-path check on content before falling back to full JSON.stringify comparison.
2026-03-01 14:46:05 -05:00
Classic298 387225eb8b fix: suppress internal path leakage in audio transcription errors (GHSA-vvxm-vxmr-624h) (#22108)
- Use os.path.basename() for filename sanitization instead of fragile blocklist

- Replace ERROR_MESSAGES.DEFAULT(e) with generic error message in both except blocks to prevent CWE-209 information disclosure

- Server-side logging via log.exception(e) is preserved for debugging
2026-03-01 14:44:49 -05:00
Timothy Jaeryang Baek c83a42198d refac 2026-03-01 13:37:31 -06:00
Timothy Jaeryang Baek 2cacc2e649 chore: format 2026-03-01 13:34:09 -06:00
Timothy Jaeryang Baek c9a78e5476 refac 2026-03-01 13:30:36 -06:00
Timothy Jaeryang Baek 2cbba2a28a chore: format 2026-03-01 13:29:06 -06:00
Timothy Jaeryang Baek 62ab30f593 refac 2026-03-01 13:28:32 -06:00
Timothy Jaeryang Baek 0fff2fbcab refac 2026-03-01 13:23:39 -06:00
Timothy Jaeryang Baek fcff9c3afd refac 2026-03-01 13:20:55 -06:00
Timothy Jaeryang Baek d415edcfcd chore: bump 2026-03-01 13:14:20 -06:00
Classic298 5f304e57d2 chore: changelog (#22080)
* changelog: MentionList memory leak fix

* changelog: multi-model responses horizontal scroll fix

* changelog: tool, json, error-handling

* changelog: add notification HTML escaping fix

* changelog: fix chat timestamp i18n

* changelog: terminal, file creation, SBOM

* changelog: terminal file editing

* changelog: terminal, toolbar, file-preview

* changelog: terminal, file refresh, automation

* changelog: model toast notification fix

* changelog: sidebar memory leak fix

* changelog: streaming performance optimizations

* changelog: message building, streaming, performance

* changelog: socket, status, event type optimizations

* changelog: offline mode, embedding model fix

* changelog: performance entries reworded for clarity
2026-03-01 14:12:21 -05:00
Classic298andahxxm 0b851cf55a fix: offline model retrieval, re-raise to disable instead of returning useless fallback (#22106)
Co-authored-by: ahxxm <1286225+ahxxm@users.noreply.github.com>
2026-03-01 13:52:31 -05:00
Timothy Jaeryang BaekandAlgorithm5838 ff86283be0 refac
Co-Authored-By: Algorithm5838 <108630393+Algorithm5838@users.noreply.github.com>
2026-03-01 12:50:24 -06:00
Algorithm5838 e9011113b4 perf: skip redundant object spread in buildMessages (#22086) 2026-03-01 13:46:11 -05:00
Classic298 1b89bee098 perf: add fast-path comparison in MultiResponseMessages (#22100)
Same optimization as ResponseMessage: add O(1) fast-path check on content and done fields before falling back to full JSON.stringify comparison. Avoids expensive serialization when only content changes during streaming.
2026-03-01 13:44:59 -05:00
Classic298 c436e0366c perf: async DB calls, skip intermediate status writes, elif chain in event emitter (#22107)
Three improvements to the socket event emitter hot path (when realtime chat save is enabled):

1. Wrap all synchronous Chats.* DB calls in asyncio.to_thread() to avoid blocking the event loop during streaming. With N concurrent users, sync DB calls serialize all writes and block socket event delivery.

2. Only persist final (done=True) status events to DB. Intermediate statuses (tool calling progress, web search progress, etc.) are ephemeral UI-only data already delivered via socket — writing every one to DB is unnecessary I/O.

3. Convert if/if/if chain to if/elif since event types are mutually exclusive, avoiding unnecessary string comparisons after a match.
2026-03-01 13:43:03 -05:00
Timothy Jaeryang BaekandShirasawa 1db36b5eda refac
Co-Authored-By: Shirasawa <kaguyashirasawa@gmail.com>
2026-03-01 12:38:59 -06:00
Classic298 3569280c0b perf: replace JSON.parse(JSON.stringify()) with structuredClone in Chat.svelte (#22102)
Replace 7 instances of JSON.parse(JSON.stringify()) deep cloning with the native structuredClone API. All are on cold paths (model selection, file preparation, history saving) but structuredClone is ~2x faster and more readable.
2026-03-01 13:37:20 -05:00
Classic298 a0d6c209c3 perf: fast-path token comparison in CodeBlock (#22101)
During streaming, every token change triggers a full JSON.stringify comparison on the code block token object. Add an O(1) fast-path check on token.text and token.raw — the fields that actually change during streaming — before falling back to the expensive JSON.stringify comparison for infrequent structural changes.
2026-03-01 13:37:10 -05:00
Classic298 73617ec7fa perf: fast-path length check in StatusHistory comparison (#22103)
Add O(1) array length check before expensive JSON.stringify comparison. During streaming, status history typically only grows via appends, so a length mismatch catches most updates without serialization.
2026-03-01 13:36:42 -05:00
Classic298 391a4878e6 perf: replace JSON.parse(JSON.stringify()) with structuredClone in layout (#22104)
Replace JSON roundtrip with native structuredClone for tool execution result cloning. Also remove unnecessary JSON roundtrip on a static error object literal that is already a fresh value.
2026-03-01 13:36:16 -05:00
Shirasawa 6d7f21b57b fix: fix memory leaking of SIdebar (#22082) 2026-03-01 13:35:09 -05:00
Peter L Jones fe604a8a9b bugfix: Prevent double toast on single hide/show toggle (#22079) 2026-03-01 13:34:45 -05:00
joaoback a9d8348cf9 i18n(pt-BR): add translations for newly added UI items + consistency pass (#22095)
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-03-01 13:33:15 -05:00
Classic298 c37c0e3490 Update translation.json (#22096) 2026-03-01 13:33:01 -05:00
Timothy Jaeryang Baek ddedceb7ad refac 2026-03-01 12:32:44 -06:00
Timothy Jaeryang Baek 18865a9fef refac 2026-03-01 12:30:03 -06:00
Timothy Jaeryang Baek 769ef856bc chore: format 2026-03-01 03:05:47 -06:00
Timothy Jaeryang Baek ed1b959bc6 refac 2026-03-01 02:38:45 -06:00
Timothy Jaeryang Baek d2b38127d0 refac 2026-03-01 02:37:21 -06:00
Timothy Jaeryang Baek 3d535db304 refac 2026-03-01 02:29:37 -06:00
Timothy Jaeryang Baek 234306ff57 refac 2026-03-01 02:08:41 -06:00
Timothy Jaeryang Baek ae28e7d245 refac 2026-03-01 00:17:34 -06:00
Shirasawa 39b87d9683 fix: Fix memory leaking in MentionList.svelte (#21965) 2026-02-28 21:48:56 -06:00
Timothy Jaeryang Baek e83f668107 refac 2026-02-28 21:40:13 -06:00
Timothy Jaeryang Baek 7dda8025fc refac 2026-02-28 21:35:32 -06:00
Timothy Jaeryang Baek 1357dc6737 chore: format 2026-02-28 21:28:59 -06:00
Timothy Jaeryang Baek 43c30428a6 refac 2026-02-28 21:16:53 -06:00
Timothy Jaeryang Baek 668bd44485 refac 2026-02-28 20:22:24 -06:00
Timothy Jaeryang Baek a3de0bcc58 refac 2026-02-28 19:22:35 -06:00
Classic298 aed2f69efe chore: Changelog updates (#21791)
* changelog: add 0.8.6 version with general improvements and translations

* changelog: fix version structure - proper 0.8.6 with today's date

* changelog: add Docker SBOM attestation entry

* changelog: RAG template duplication fix

* changelog: add action button priority sorting feature

* changelog: add public/private model filtering entry

* changelog: fix duplicate model execution, RAG template

* changelog: add USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS env var for user sharing control

* changelog: add reporting-endpoints security header entry

* changelog: add default group share permission env var

* changelog: function valve priority fix

* changelog: german, i18n, translations

* changelog: oauth, session, database-fix

* changelog: models, oauth, cache

* changelog: fix web content knowledge base append

* changelog: password manager autofill fix

* changelog: sidebar menu positioning fix

* changelog: tool query optimization, sidebar menu

* changelog: add 0.8.6 entries for security, models, OAuth, RAG, translations

* changelog: user sharing permission enforcement fix

* changelog: user sharing permission links

* changelog: streaming, performance, rendering

* changelog: database migration execution fix

* changelog: open terminal, tool server

* changelog: terminal, tool-server, optimization

* changelog: add Catalan to translation updates

* changelog: streaming, message comparison, optimization

* changelog: math rendering, performance

* changelog: add Tools to Integrations rename entry

* changelog: add Spanish to translation updates

* changelog: tooltip, performance fix

* changelog: messageinput memory leak fix

* changelog: web search domain filter config fix

* changelog: message cloning performance optimization

* changelog: notes, memory leak, stability

* changelog: streaming scroll optimization performance

* changelog: code block UI fix

* changelog: add model create memory leak fix entry

* changelog: add toast notification to bulk model actions

* changelog: add TailwindCSS gray color theme fix

* changelog: streaming, memory leaks, UI fixes, translations, tools to integrations
2026-02-28 18:10:19 -05:00
Classic298 30ae519226 perf: throttle message list rebuild to once per animation frame during streaming (#21885)
Messages.svelte rebuilds the message list by walking the parent chain and creating spread copies on every history.messages change. During streaming, this runs on every token — hundreds of times per second — even though each ResponseMessage already has its own reactive binding for content updates. Throttle the rebuild to once per animation frame (~60Hz) during content-only updates, while keeping immediate rebuilds for structural changes (currentId changes like chat switches, navigation, or new messages). Adds onDestroy cleanup for the pending rAF.
2026-02-28 18:09:43 -05:00
Timothy Jaeryang BaekandNil Puig 499ca282e5 refac
Co-Authored-By: Nil Puig <244631886+npuigm@users.noreply.github.com>
2026-02-28 17:08:41 -06:00
Shirasawa 40d90286b6 I18n: improve Chinese translation (#21980)
* i18n: improve zh-CN translation

* i18n: improve zh-TW translation
2026-02-28 16:19:59 -05:00
Timothy Jaeryang BaekandIngmar van Hulzen 2d27ef4ece refac
Co-Authored-By: Ingmar van Hulzen <13165062+ingmarvanhulzen@users.noreply.github.com>
2026-02-28 13:46:30 -06:00
Shirasawa e9b5eb6ed3 fix: Fix memory leaking in create model page (#21966) 2026-02-28 14:41:00 -05:00
Timothy Jaeryang Baek 6b462ff121 refac 2026-02-28 13:40:06 -06:00
Timothy Jaeryang Baek c3bac9aa62 refac 2026-02-28 13:30:28 -06:00
Shirasawa f7226333c3 i18n: improve Chinese translation (#21934)
* i18n: improve zh-CN translation

* i18n: improve zh-TW translation
2026-02-28 14:14:13 -05:00
Algorithm5838 fc5f399573 perf: batch scrollToBottom during streaming via rAF (#21946) 2026-02-28 14:13:48 -05:00
Shirasawa ff8cf80fb5 fix: fix memory leaking of Notes.svelte (#21963) 2026-02-28 14:09:43 -05:00
Algorithm5838 54cefedf53 perf: use structuredClone for message deep copies (#21948) 2026-02-28 14:09:29 -05:00
Timothy Jaeryang Baek 9440d09114 refac 2026-02-28 13:07:10 -06:00
Shirasawa 5bb1c42fa8 fix: Fix memory leaking of MessageInput (#21968) 2026-02-28 14:03:08 -05:00
Shirasawa 242b3f0c01 fix: Fix Tooltip memory leaking and type define (#21969) 2026-02-28 14:01:28 -05:00
Shirasawa 144c0f3d76 fix: fix missing i18n keys (#21932) 2026-02-28 13:56:12 -05:00
_00_ 18401de254 upd:i18n es-ES language update v0.8.5 (#21956)
### upd:i18n  es-ES language update v0.8.5

Added new strings and a couple of corrections
2026-02-28 13:54:16 -05:00
Timothy Jaeryang Baek c71beb0a7d refac 2026-02-28 02:05:22 -06:00
Timothy Jaeryang Baek f5bf2a2ed7 refac 2026-02-28 00:41:10 -06:00
Timothy Jaeryang Baek ab3f03bbd5 refac 2026-02-28 00:40:20 -06:00
Timothy Jaeryang Baek 5ac502e93f refac 2026-02-27 17:24:34 -06:00
Timothy Jaeryang Baek c60b0fa0e3 refac 2026-02-27 17:20:49 -06:00
Timothy Jaeryang Baek 9544a80aa0 refac 2026-02-27 17:14:54 -06:00
Timothy Jaeryang Baek 83b17e2ac8 refac 2026-02-27 17:04:09 -06:00
Timothy Jaeryang Baek 3a6c88ade9 refac 2026-02-27 16:47:36 -06:00
Timothy Jaeryang Baek 3be06132db refac 2026-02-27 16:41:52 -06:00
Timothy Jaeryang Baek bbbcf27dd5 refac 2026-02-27 16:37:53 -06:00
Timothy Jaeryang Baek cfa16e1a37 refac 2026-02-27 16:37:33 -06:00
Timothy Jaeryang Baek f60d386b74 refac 2026-02-27 16:21:27 -06:00
Timothy Jaeryang Baek 0324a1bbdd refac 2026-02-27 16:03:43 -06:00
Timothy Jaeryang Baek a677b212d9 refac 2026-02-27 16:03:12 -06:00
Timothy Jaeryang Baek 179a4ad9ea refac 2026-02-27 16:01:57 -06:00
Timothy Jaeryang Baek 2d82d260cc refac 2026-02-27 16:01:33 -06:00
Timothy Jaeryang Baek e7a9988893 chore: format 2026-02-27 15:59:52 -06:00
Timothy Jaeryang Baek 6b01f96eac refac 2026-02-27 15:56:25 -06:00
Timothy Jaeryang Baek 965f242d16 refac 2026-02-27 15:53:03 -06:00
Timothy Jaeryang Baek 758d8fcf31 refac 2026-02-27 15:51:15 -06:00
Timothy Jaeryang Baek 0f8b339f6d refac 2026-02-27 15:48:55 -06:00
Timothy Jaeryang Baek 5d821d21f3 refac 2026-02-27 14:36:22 -06:00
Timothy Jaeryang Baek d6d9d1c535 refac 2026-02-27 14:36:13 -06:00
Timothy Jaeryang Baek 44ab77b4f5 refac 2026-02-27 14:12:59 -06:00
Timothy Jaeryang Baek 646b64a318 refac 2026-02-27 13:37:03 -06:00
Timothy Jaeryang Baek bbab64b53e refac 2026-02-27 13:36:55 -06:00
Timothy Jaeryang Baek 4731ccb73c refac 2026-02-27 13:30:36 -06:00
Timothy Jaeryang Baek 4737e1f118 feat: open terminal integration 2026-02-27 13:08:59 -06:00
Classic298 7ea6afdf95 perf: cache KaTeX module import as singleton across all renderer instances (#21880)
* perf: cache KaTeX module import as singleton across all renderer instances

KatexRenderer.svelte dynamically imports katex, mhchem, and the CSS on every component mount. When a message contains multiple math expressions, this triggers redundant module resolution for each one. Move the import promise to a module-level singleton using Svelte's context='module' script block so it loads once and is shared across all KatexRenderer instances.

* Update KatexRenderer.svelte
2026-02-26 15:34:42 -06:00
Classic298 4654ecbf1b perf: fast-path comparison in ResponseMessage to skip JSON.stringify during streaming (#21884)
ResponseMessage compared the entire message object via JSON.stringify on every reactive tick to detect changes. During streaming, content changes on every token, making the two O(content_length) JSON.stringify calls always return different results — pure wasted work. Add a fast O(1) comparison on content and done fields first. When either differs (the common streaming case), skip straight to cloning. Only fall through to the expensive JSON.stringify comparison for infrequent changes like sources, annotations, or status updates.
2026-02-26 14:47:32 -06:00
Aleix Dorca 527d36e13a Update catalan translation.json (#21895) 2026-02-26 14:28:17 -06:00
Stefan Weil d7d05a4717 fix(ui): fix some broken links (#21904)
The referenced information was moved to a new location.

Signed-off-by: Stefan Weil <sw@weilnetz.de>
2026-02-26 14:27:57 -06:00
Timothy Jaeryang Baek 419ea1c346 refac 2026-02-26 00:00:01 -06:00
Timothy Jaeryang Baek 59214538bb refac 2026-02-25 20:17:39 -06:00
Timothy Jaeryang Baek eca9b405eb refac 2026-02-25 19:58:50 -06:00
Timothy Jaeryang Baek 58d685eea4 refac 2026-02-25 19:39:24 -06:00
Timothy Jaeryang Baek 44ed941a5d refac 2026-02-25 19:38:00 -06:00
Timothy Jaeryang Baek 46229a93ce refac 2026-02-25 19:32:01 -06:00
Timothy Jaeryang Baek 50eff6a672 refac 2026-02-25 19:14:02 -06:00
Timothy Jaeryang Baek 1cb74b0bf7 refac 2026-02-25 19:06:46 -06:00
Timothy Jaeryang Baek c303388296 refac 2026-02-25 19:02:52 -06:00
Timothy Jaeryang Baek 5a08084899 refac 2026-02-25 19:00:56 -06:00
Timothy Jaeryang Baek b1f292965c refac 2026-02-25 19:00:40 -06:00
Timothy Jaeryang Baek 819ea0d9be refac 2026-02-25 18:30:53 -06:00
Timothy Jaeryang Baek 1f77691b01 refac 2026-02-25 18:16:20 -06:00
Timothy Jaeryang Baek 50e6a19957 refac 2026-02-25 18:06:09 -06:00
Timothy Jaeryang Baek cb0165827f refac 2026-02-25 17:30:28 -06:00
Timothy Jaeryang Baek c5225039ab refac 2026-02-25 17:23:22 -06:00
Timothy Jaeryang Baek f2c3fff278 refac 2026-02-25 17:07:24 -06:00
Timothy Jaeryang Baek 3271a5277c refac 2026-02-25 16:56:32 -06:00
Timothy Jaeryang Baek 8b2160f2f7 refac 2026-02-25 16:13:18 -06:00
Timothy Jaeryang Baek bee13f72ad refac 2026-02-25 15:59:23 -06:00
Timothy Jaeryang Baek 64ff15a536 refac 2026-02-25 15:52:12 -06:00
Timothy Jaeryang Baek 345f3e3559 refac 2026-02-25 15:15:59 -06:00
Timothy Jaeryang Baek 636ab99ad8 feat: experimental open terminal integration 2026-02-25 15:15:53 -06:00
Timothy Jaeryang Baek f0c71e5a6d refac 2026-02-25 15:15:00 -06:00
Timothy Jaeryang Baek 87d33f6e18 refac 2026-02-25 14:52:41 -06:00
Timothy Jaeryang Baek fd91fa433a refac 2026-02-25 14:06:06 -06:00
Timothy Jaeryang BaekandAlgorithm5838 484ba91b07 refac
Co-Authored-By: Algorithm5838 <108630393+Algorithm5838@users.noreply.github.com>
2026-02-25 13:56:28 -06:00
Timothy Jaeryang Baek acb2147024 refac 2026-02-25 13:53:08 -06:00
Timothy Jaeryang Baek ace69bba75 refac 2026-02-25 13:45:50 -06:00
joaobackandTim Baek 5beb37c57c i18n(pt-BR): add translations for newly added UI items + consistency pass (#21776)
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.

Co-authored-by: Tim Baek <tim@openwebui.com>
2026-02-25 13:34:24 -06:00
Timothy Jaeryang Baek 50f95a4f1a refac 2026-02-25 13:17:29 -06:00
G30 39e3f8fb81 fix(sidebar): lock user menu position when sidebar is resized (#21853)
Use align="start" (left-anchor) instead of align="end" (right-anchor) on
the user menu DropdownMenu.Content, combined with avoidCollisions={false}
to prevent Floating UI from auto-flipping back to end-alignment when the
menu's left edge is near the viewport boundary.

Previously, the right edge of the full-width trigger row tracked the
right edge of the sidebar, so resizing the sidebar wider caused the menu
to drift rightward. With start alignment the menu is anchored to the
left edge of the trigger, which is stable regardless of sidebar width.
2026-02-25 13:13:52 -06:00
Algorithm5838 b2413f914a perf: early-return in get_tools() for empty tool_ids (#21873)
Avoids a needless Groups.get_groups_by_member_id() query when
no tools are attached to the request.
2026-02-25 13:13:18 -06:00
Timothy Jaeryang Baek 9dff497abf refac 2026-02-25 13:12:34 -06:00
Classic298 e3f21d6c3b Update SECURITY.md (#21859) 2026-02-25 12:55:20 -06:00
Timothy Jaeryang Baek 184e921930 refac 2026-02-25 03:09:23 -06:00
Timothy Jaeryang BaekandJohannes Fahrenkrug 5ee5093259 refac
Co-Authored-By: Johannes Fahrenkrug <16358+jfahrenkrug@users.noreply.github.com>
2026-02-24 17:23:36 -06:00
Timothy Jaeryang Baek 81781e6495 refac 2026-02-24 17:14:07 -06:00
Classic298 82959cec88 Update oauth_sessions.py (#21794) 2026-02-24 17:05:47 -06:00
Timothy Jaeryang Baek 9478c5e7ac refac 2026-02-24 17:04:07 -06:00
Timothy Jaeryang BaekandPeter L Jones 62e7e0bc09 refac
Co-Authored-By: Peter L Jones <1549463+pljones@users.noreply.github.com>
2026-02-24 16:51:28 -06:00
Classic298 7a16e495dd Update print statement from 'Hello' to 'Goodbye' (#21842) 2026-02-24 16:42:45 -06:00
Timothy Jaeryang Baek 958fbdd5c0 refac 2026-02-24 16:12:02 -06:00
Classic298 5c403fb829 fix: resolve valve priority for actions and filters via class instantiation (#21841)
fix: resolve valve priority for actions and filters via class instantiation

The priority sorting for action buttons and filter execution order
read valve data directly from the database JSON column using
Functions.get_function_valves_by_id(). This returns only explicitly
saved values — when a developer defines priority as a class default
in their Valves definition (e.g. priority: int = 5) without ever
opening the Valves UI to persist it, the database column remains
empty. Every function then resolves to priority 0, and the preceding
set() deduplication produces non-deterministic iteration order that
the stable sort preserves — resulting in random button placement on
every page load.

The fix instantiates the Valves class with database values as keyword
overrides: Valves(**(db_valves or {})). This merges any persisted
overrides onto the code-defined defaults, matching the pattern already
established in the action execution handler, filter processing
pipeline, and tool module initialization. A secondary sort key (the
function ID) ensures fully deterministic ordering even when multiple
functions share the same priority value.

Affected locations:
- get_action_priority in utils/models.py (action button ordering)
- get_priority in utils/filter.py (filter execution ordering)
2026-02-24 15:58:23 -06:00
Timothy Jaeryang Baek 538501c88d refac 2026-02-24 15:19:49 -06:00
Timothy Jaeryang Baek 0b6c92baa7 refac 2026-02-24 14:57:59 -06:00
Timothy Jaeryang Baek 64ec73635b refac 2026-02-24 14:47:28 -06:00
Timothy Jaeryang Baek b36e55cf1f refac 2026-02-24 13:27:48 -06:00
Timothy Jaeryang Baek 2461121637 refac 2026-02-23 18:31:26 -06:00
Timothy Jaeryang Baek e6fe3ba8ef refac 2026-02-23 18:23:47 -06:00
Timothy Jaeryang Baek 0b867590a8 refac 2026-02-23 18:23:34 -06:00
Timothy Jaeryang Baek 3c8d658160 fix: tools_dict issue 2026-02-23 16:25:38 -06:00
Timothy Jaeryang Baek 176f9a7816 refac 2026-02-23 16:01:03 -06:00
Timothy Jaeryang Baek 3d99de6771 enh: access grant level perms 2026-02-23 15:49:05 -06:00
Peter L Jones a52e6c2d57 Filter by public/private (#21797) 2026-02-23 14:09:13 -06:00
Classic298 1808d7fd2f feat: sort action buttons by valve priority (#21790)
feat: sort action buttons by valve priority

Action buttons under assistant messages were rendered in
non-deterministic order due to set() deduplication. They now
respect the priority field from function Valves, sorted ascending
(lower value = appears first, default 0), matching the existing
filter priority mechanism.
2026-02-23 13:52:12 -06:00
Timothy Jaeryang Baek f4a1d99f00 refac 2026-02-23 12:52:46 -06:00
Timothy Jaeryang Baek 8f49725aa5 refac 2026-02-23 12:17:36 -06:00
Timothy Jaeryang Baek febc66ef2b enh: sbom docker gh action 2026-02-23 12:03:56 -06:00
Timothy Jaeryang Baek 3761b3ac28 refac 2026-02-23 11:52:35 -06:00
Jannik S. 140ab270af fix: correct ENABLE_AUDIT_STDOUT stdout filter (#21777) 2026-02-23 11:52:29 -06:00
Tim Baek 6ab452a452 Merge pull request #21785 from EventHorizon-AI/fix/shortcuts-i18n
fix: dictation toggle shortcuts i18n
2026-02-23 21:50:12 +04:00
Tim Baek 8962afd586 Merge pull request #21784 from ShirasawaSama/i18n/improve-chinese-translation
I18n: improve Chinese translation
2026-02-23 21:49:58 +04:00
EntropyYue 22f074cf59 fix: dictation toggle shortcuts i18n 2026-02-23 22:18:34 +08:00
Shirasawa ec4fe4f390 i18n: improve zh-TW translation 2026-02-23 21:55:14 +08:00
Shirasawa 32c68e000b i18n: improve zh-CN translation 2026-02-23 21:48:10 +08:00
Tim Baek 1ac3dd4a89 Merge pull request #21773 from open-webui/dev
0.8.5
2026-02-23 13:26:21 +04:00
Timothy Jaeryang Baek 55c489146c doc: changelog 2026-02-23 03:25:17 -06:00
Timothy Jaeryang Baek ffcf97e3e1 chore: bump 2026-02-23 03:24:33 -06:00
Timothy Jaeryang Baek 95bde946ba refac 2026-02-23 03:22:19 -06:00
Timothy Jaeryang Baek 895c805e62 feat: dictation toggle 2026-02-23 02:54:53 -06:00
Tim Baek 2ed3055c42 Merge pull request #21618 from open-webui/dev
0.8.4
2026-02-23 11:58:08 +04:00
Timothy Jaeryang Baek 1792f668f2 refac 2026-02-23 01:53:58 -06:00
Timothy Jaeryang Baek 1d3d3b2d94 refac 2026-02-23 01:46:08 -06:00
Timothy Jaeryang Baek 9044abf3bb chore: format 2026-02-23 01:40:53 -06:00
Timothy Jaeryang Baek 424dba443c refac 2026-02-23 01:37:06 -06:00
Classic298 aa649bec6b Chore: Changelog updates (#21754)
* changelog: add prompt enable/disable toggle entry

* changelog: fix PostgreSQL workspace cloning

* changelog: MCP SSL verification fix

* changelog: mcp ssl, general improvements, french translations

* changelog: add memory deletion and listing tools for agents

* changelog: add embeddings and proxy timeout fix for PR #21558

* changelog: pip requirements toggle

* changelog: fix commit references for memory and MCP entries

* changelog: scim, parameter handling, rfc7644

* changelog: update iframe sandbox entry for clarity

* changelog: shared chat optimization, translation updates

* changelog: file access control respect fix

* changelog: chat title query optimization, shared chat loading

* changelog: hybrid search fix, Finnish translations

* changelog: message list performance optimization

* changelog: archived chats, pinned chats, loading optimization

* changelog: knowledge-base-import, overwrite-flag, API-enhancement

* changelog: message upsert and tag filtering optimizations

* changelog: batch access grants, notes payload optimization

* changelog: skill import, json support

* changelog: add fix for imported items display issue

* changelog: add Anthropic Messages API proxy support

* changelog: WebSocket race condition fix for collaborative editing

* 📝

* changelog: drag-drop, firefox, overlay fix

* changelog: add multi-device OAuth sessions feature

* changelog: cyclic chat history deadlock fix

* changelog: group search visibility fix

* changelog: model default feature permissions

* changelog: admin groups sorting, notes optimization

* changelog: model selector, virtual scroll, UI fix

* changelog: user menu drag and click fixes

* changelog: rich-ui, auto-scroll, ux

* changelog: enhance Anthropic Messages API proxy with tool call support

* changelog: embedding concurrency, knowledge import

* changelog: add You.com web search provider (#21599)

* changelog: admin analytics toggle

* changelog: console log spam fix

* changelog: fetch URL citation sources

* changelog: message send optimization

* changelog: oauth, group sharing, settings

* changelog: admin nav drag fix (PR #21701)

* changelog: signup race condition, security fix

* changelog: playground, nav, drag

* changelog: group description, sort dropdown

* changelog: add model selector accessibility improvements

* changelog: consolidate accessibility entries for PRs #21705 and #21706

* changelog: tools list performance optimization

* changelog: accessibility, components, wcag

* changelog: button accessibility labels, wcag compliance

* changelog: Firefox avatar overflow fix

* changelog: disabled model cloning prevention fix

* changelog: dark mode select background fix

* changelog: update date to 2026-02-22, consolidate accessibility entries

* changelog: new chat message handling fix

* changelog: accessibility, aria-labels, settings components

* changelog: admin settings, tab navigation

* changelog: scroll, messages, deletion

* changelog: scroll, chat, message fixes

* changelog: model fallback routing and default model selection fixes

* changelog: remove duplicate scroll jumping fix from 0.8.3

* changelog: model visibility badges

* changelog: prompt import fix

* changelog: dropdown menu drag fix

* changelog: add workspace accessibility improvements to UI accessibility entry

* changelog: docker hub integration

* changelog: global model defaults, admin settings

* changelog: text file type detection fix

* changelog: update date to 2026-02-23

* changelog: ollama reasoning effort fix

* changelog: emoji deduplication in Fixed section

* changelog: sql, warning-fix

* changelog: add plaintext tool output display entry

* changelog: json, logging, format

* changelog: RAG template mutation fix for sequential tool calls

* changelog: analytics sorting, ldap authentication

* changelog: API tools, LDAP fields, SQLAlchemy fixes

* changelog: add folder menu fix, event call input masking, analytics sorting, LDAP fix, SQL warning fix

* changelog: add prompt suggestions and banners moved entries

* changelog: add prompt suggestions and banners moved to current version

* changelog: improve prompt suggestions and banners moved entries

* changelog: add hybrid search deduplication fix
2026-02-23 01:29:14 -06:00
G30 a8a3098782 fix(ui): apply select-none to remaining dropdown menus globally to prevent text highlighting (#21763) 2026-02-23 01:22:02 -06:00
G30 238e9da209 fix(ui): prevent highlighting and dragging of text in admin settings menu link (#21761) 2026-02-23 01:21:55 -06:00
G30 49a1b37e5d fix(ui): prevent highlighting of text in chat integrations menu (#21758) 2026-02-23 01:21:46 -06:00
Timothy Jaeryang Baek c035ff7d14 refac 2026-02-22 19:18:25 -06:00
Timothy Jaeryang Baek 2558fe1a3b refac 2026-02-22 19:16:35 -06:00
G30 e7848ec712 feat(ui): update admin users and evaluations sidebars to leverage native anchor tags for robust new-tab link capabilities (#21723) 2026-02-22 19:08:18 -06:00
Timothy Jaeryang Baek 39e5422d93 refac 2026-02-22 18:58:47 -06:00
Timothy Jaeryang Baek f6bd54fb1f refac 2026-02-22 18:55:24 -06:00
Timothy Jaeryang Baek d9fd2a3f30 refac 2026-02-22 18:42:25 -06:00
Timothy Jaeryang Baek 824eeba56c refac 2026-02-22 18:23:53 -06:00
Timothy Jaeryang Baek e61406c825 refac 2026-02-22 18:19:49 -06:00
Timothy Jaeryang Baek 4853ededca refac 2026-02-22 18:05:25 -06:00
G30 8c127a4814 fix(ui): make folder menu text non-highlightable (#21753) 2026-02-22 18:04:22 -06:00
Timothy Jaeryang Baek 6eba27ee9c refac 2026-02-22 18:00:16 -06:00
Timothy Jaeryang Baek 8f0658e64f fix: payload tools handling 2026-02-22 17:58:59 -06:00
Johann Frei 4b3543d3c0 fix(ui): allow empty LDAP Application DN value and password in General setting… (#21742)
* Allow empty LDAP Application DN value and password in General settings form

* fix(ui): use LDAP app_dn, app_dn_password with empty string instead of enforcing non-empty values
2026-02-22 17:58:12 -06:00
Classic298 d1b39da911 changelog: yeah (#21575)
* changelog: add prompt enable/disable toggle entry

* changelog: fix PostgreSQL workspace cloning

* changelog: MCP SSL verification fix

* changelog: mcp ssl, general improvements, french translations

* changelog: add memory deletion and listing tools for agents

* changelog: add embeddings and proxy timeout fix for PR #21558

* changelog: pip requirements toggle

* changelog: fix commit references for memory and MCP entries

* changelog: scim, parameter handling, rfc7644

* changelog: update iframe sandbox entry for clarity

* changelog: shared chat optimization, translation updates

* changelog: file access control respect fix

* changelog: chat title query optimization, shared chat loading

* changelog: hybrid search fix, Finnish translations

* changelog: message list performance optimization

* changelog: archived chats, pinned chats, loading optimization

* changelog: knowledge-base-import, overwrite-flag, API-enhancement

* changelog: message upsert and tag filtering optimizations

* changelog: batch access grants, notes payload optimization

* changelog: skill import, json support

* changelog: add fix for imported items display issue

* changelog: add Anthropic Messages API proxy support

* changelog: WebSocket race condition fix for collaborative editing

* 📝

* changelog: drag-drop, firefox, overlay fix

* changelog: add multi-device OAuth sessions feature

* changelog: cyclic chat history deadlock fix

* changelog: group search visibility fix

* changelog: model default feature permissions

* changelog: admin groups sorting, notes optimization

* changelog: model selector, virtual scroll, UI fix

* changelog: user menu drag and click fixes

* changelog: rich-ui, auto-scroll, ux

* changelog: enhance Anthropic Messages API proxy with tool call support

* changelog: embedding concurrency, knowledge import

* changelog: add You.com web search provider (#21599)

* changelog: admin analytics toggle

* changelog: console log spam fix

* changelog: fetch URL citation sources

* changelog: message send optimization

* changelog: oauth, group sharing, settings

* changelog: admin nav drag fix (PR #21701)

* changelog: signup race condition, security fix

* changelog: playground, nav, drag

* changelog: group description, sort dropdown

* changelog: add model selector accessibility improvements

* changelog: consolidate accessibility entries for PRs #21705 and #21706

* changelog: tools list performance optimization

* changelog: accessibility, components, wcag

* changelog: button accessibility labels, wcag compliance

* changelog: Firefox avatar overflow fix

* changelog: disabled model cloning prevention fix

* changelog: dark mode select background fix

* changelog: update date to 2026-02-22, consolidate accessibility entries

* changelog: new chat message handling fix

* changelog: accessibility, aria-labels, settings components

* changelog: admin settings, tab navigation

* changelog: scroll, messages, deletion

* changelog: scroll, chat, message fixes

* changelog: model fallback routing and default model selection fixes

* changelog: remove duplicate scroll jumping fix from 0.8.3

* changelog: model visibility badges

* changelog: prompt import fix

* changelog: dropdown menu drag fix

* changelog: add workspace accessibility improvements to UI accessibility entry

* changelog: docker hub integration

* changelog: global model defaults, admin settings

* changelog: text file type detection fix

* changelog: update date to 2026-02-23

* changelog: ollama reasoning effort fix

* changelog: emoji deduplication in Fixed section

* changelog: sql, warning-fix

* changelog: add plaintext tool output display entry
2026-02-22 17:56:25 -06:00
Timothy Jaeryang Baek 053a33631f refac 2026-02-22 17:55:08 -06:00
Timothy Jaeryang Baek becac2b2b7 refac 2026-02-22 17:51:08 -06:00
Timothy Jaeryang Baek 342aa84bbe refac 2026-02-22 17:51:03 -06:00
Andrei Efanov 9e81e1dda1 feat: add LOG_FORMAT=json for structured JSON logging (#21747)
* feat: add LOG_FORMAT env var with JSON formatter for early logging

Introduce LOG_FORMAT environment variable (set to "json" to enable).
When active, logging.basicConfig() uses a JSONFormatter that outputs
single-line JSON objects with fields: ts, level, msg, caller, error,
stacktrace. This covers all log messages emitted during module imports
before Loguru's start_logger() takes over.

* feat: add JSON sink for Loguru when LOG_FORMAT=json

Add _json_sink() as a Loguru sink function that writes single-line JSON
to stdout. In start_logger(), conditionally use the JSON sink instead of
the plain-text stdout_format when LOG_FORMAT is set to "json".

* feat: suppress ASCII banner and fix alembic logging in JSON mode

- Wrap the ASCII art banner print in main.py with a LOG_FORMAT != "json"
  guard so JSON output stays machine-parseable.
- Skip alembic's fileConfig() call in migrations/env.py when
  LOG_FORMAT=json to prevent it from replacing the JSON log handlers
  installed during early startup.
2026-02-22 17:40:17 -06:00
Timothy Jaeryang Baek 3ad2ea6f28 refac 2026-02-22 17:38:22 -06:00
Timothy Jaeryang Baek bab64c9d52 refac 2026-02-22 17:38:14 -06:00
Timothy Jaeryang Baek 0185f3340d refac 2026-02-22 17:28:01 -06:00
Timothy Jaeryang Baek 1cd26372fb refac 2026-02-22 17:26:59 -06:00
Timothy Jaeryang Baek 0ca2e46ade refac 2026-02-22 17:17:44 -06:00
Timothy Jaeryang Baek 30a13b9b2f refac: ollama str think support 2026-02-22 17:11:50 -06:00
Timothy Jaeryang Baek f651809001 refac 2026-02-22 17:05:39 -06:00
Timothy Jaeryang Baek c341f97cfe feat: default model metadata & params 2026-02-22 16:54:34 -06:00
Timothy Jaeryang Baek 32aabe6bae refac 2026-02-22 16:18:32 -06:00
Timothy Jaeryang Baek 3c54863414 refac 2026-02-22 15:41:10 -06:00
Timothy Jaeryang Baek ad9fbfc1af refac 2026-02-22 15:35:16 -06:00
Timothy Jaeryang Baek 29217cb430 refac 2026-02-22 15:34:28 -06:00
G30 c0096b2a53 fix: explicitly disable dragging and text selection inside dropdown menus (#21713)
* fix(ui): remove select-none from move and pdf menu items to allow highlighting

* fix(ui): explicitly disable dragging and text selection inside dropdown menus globally
2026-02-22 15:30:43 -06:00
Classic298 5b9efeef4d fix(a11y): add aria-labels and structural elements to workspace components (#21719) 2026-02-22 15:24:33 -06:00
Classic298 e0087acfb4 fix: model fallback routing for all model types and default model selection (#21736)
fix: model fallback routing for all model types and default model selection

Backend: When ENABLE_CUSTOM_MODEL_FALLBACK is active and a custom model's
base model is unavailable, the fallback now swaps the model and form data
to the configured default model directly. This ensures routing uses the
fallback model's type (pipe, Ollama, or OpenAI) instead of the original
model's type, which previously caused "Model not found" errors when the
fallback was a different backend type.

Frontend: Fixed default model selection in new chat initialization where
the admin-configured default models were always overwritten by the first
available model. The first-available fallback now only triggers when the
configured defaults don't resolve to valid available models.
2026-02-22 15:24:14 -06:00
Timothy Jaeryang BaekandClassic298 1f474187a7 refac
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2026-02-22 15:22:53 -06:00
Classic298 2beeeb90c2 fix(a11y): add aria-labels to chat message components (#21708)
Add aria-labels, aria-expanded, and semantic improvements to:
- RateComment: close button, rating scale, feedback textarea
- Citations: toggle button with count, source item buttons
- Source/SourceToken: contextual aria-labels for citation buttons
- StatusHistory: toggle button with expanded state
- WebSearchResults: descriptive favicon alt text
- FollowUps: convert div to button element
- RegenerateMenu: submit suggestion button
- FloatingButtons: action buttons, input field, submit button
- CitationModal: close button

WCAG: 4.1.2 (Name, Role, Value), 2.1.1 (Keyboard), 1.1.1 (Non-text Content)
2026-02-22 14:36:42 -06:00
G30 d016cc5771 feat: convert admin settings menu tabs to native anchor tags for new-tab support (#21721)
* docs: generate PR template for navigation drag glitch fixes

* feat(ui): convert admin settings menu tabs to native anchor tags for new-tab support
2026-02-22 14:34:06 -06:00
Classic298 16e567df57 fix(a11y): enhance accessibility for chat settings components (#21715)
This commit adds aria-labels to the text inputs and textareas that previously lacked them, applies role=switch to inputs, and adds accessible titles to floating quick actions.
2026-02-22 14:33:07 -06:00
Classic298 1542dad51a fix(a11y): enhance accessibility for admin user components (#21717)
This commit adds aria-labels to the search inputs, select fields, action buttons, and close buttons on modals across the admin users layout and the site changelog modal for improved screen reader support.
2026-02-22 14:32:49 -06:00
Classic298 2ef55972ff fix: reset taskIds and messageQueue on new chat (#21731)
fix: reset taskIds and messageQueue on new chat

Fixes a bug where clicking "New Chat" after sending a message would
silently drop subsequent messages. The initNewChat function reset most
chat state but did not clear taskIds or messageQueue, causing
submitPrompt to queue messages indefinitely instead of sending them.
2026-02-22 14:30:44 -06:00
G30 75c5d9b179 fix(ui): hide clone button in model menu for models disabled by admins (#21724) 2026-02-22 14:26:33 -06:00
G30 713fe1afa7 fix(ui): prevent avatar alt-text overlap on failed image loads (#21730) 2026-02-22 14:24:16 -06:00
G30 f95cff0895 fix(ui): replace static dropdown backgrounds with transparent mapping (#21728) 2026-02-22 14:23:34 -06:00
Classic298 a0dbd41551 fix(a11y): improve accessibility of top-level auth and onboarding components (#21710)
Adds critical accessibility fixes across various app components:
- auth/+page: provide alt text for logo, turn on screenReader support for password input, add aria-required, hide decorative SVGs from AT
- AppSidebar: wrap navigation icons in a <nav> structure, provide ARIA labels for Home and Chat icons
- s/[id]/+page: convert structural divs into semantically accurate h1 heading and time element, wrap message display in main region
- OnBoarding: replace flawed aria-labelledby with direct aria-label on start button
- NotificationToast: provide role='status' and aria-live='polite' for proper screen reader broadcasting
- ChangelogModal: add required heading semantics for structure
- AddFilesPlaceholder: provide heading element role for standalone text content
- ImportModal: provide aria-label for close button

Addresses WCAG 4.1.3, 1.1.1, 3.3.2, and 1.3.1.
2026-02-22 14:18:53 -06:00
Classic298 bf0fb1c449 fix(a11y): add aria-labels to chat core components (#21709)
Add aria-labels to close, back, and action buttons across:
- Controls/Controls.svelte: close chat controls button
- ChatControls/Embeds.svelte: close embed button
- Overview/Node.svelte: favorite toggle button
- Overview/View.svelte: back and close overview buttons
- ShortcutsModal.svelte: close button
- ShareChatModal.svelte: close button
- ToolServersModal.svelte: close button
- Placeholder/FolderTitle.svelte: folder icon picker, folder options menu

WCAG: 4.1.2 (Name, Role, Value)
2026-02-22 14:18:19 -06:00
Shirasawa 7043751ca4 I18n: improve Chinese translation (#21741)
* i18n: improve zh-CN translation

* i18n: improve zh-TW translation
2026-02-22 14:15:08 -06:00
Classic298 2d5ebf962a Update README.md (#21735) 2026-02-22 14:14:56 -06:00
Timothy Jaeryang Baek b48594a166 refac 2026-02-21 16:27:25 -06:00
Classic298 74e771fec6 fix(a11y): add aria-hidden to all decorative SVG icon components (#21705)
Add aria-hidden='true' to 112 SVG icon components in src/lib/components/icons/ that were missing this attribute. Decorative icons that convey no semantic meaning should be hidden from the accessibility tree to prevent screen readers from attempting to read meaningless SVG markup (WCAG 1.1.1 Non-text Content, WCAG 4.1.2 Name, Role, Value).

The remaining 60 icon files already had aria-hidden='true' set. All 172 icon components now consistently declare aria-hidden='true' on their root svg element.
2026-02-21 16:14:27 -06:00
Classic298 08f1c823ad fix(a11y): improve model selector accessibility with proper listbox/option pattern (#21706)
- Replace incorrect aria-roledescription='model-item' with role='option' and aria-selected on ModelItem.svelte. The previous attribute was not a valid ARIA role description and provided no useful information to screen readers.

- Add contextual aria-label to each model item button (e.g. 'Select GPT-4 model') instead of just the raw model name, making the action clear to screen reader users.

- Add role='listbox' and aria-label='Available models' to the scrollable model list container in Selector.svelte so screen readers announce the container's purpose and navigate items correctly.

- Make the model selector trigger button's aria-label dynamic: it now announces 'Selected model: GPT-4' when a model is selected, falling back to 'Select a model' when nothing is selected.

- Add aria-label to the eject (unload) button in ModelItem.svelte so screen readers announce its purpose.

- Add aria-label to the cancel download button in Selector.svelte with the specific model name being canceled.

- Improve model profile image alt text from generic 'Model' to contextual '{{modelName}} profile image'.
2026-02-21 16:14:09 -06:00
Timothy Jaeryang Baek b559606387 refac 2026-02-21 16:02:45 -06:00
Timothy Jaeryang Baek 914c7ba876 refac: groups ui 2026-02-21 16:01:48 -06:00
G30 96ca47ac9f fix(ui): prevent text-selection ghost dragging on playground navigation tabs (#21704) 2026-02-21 15:45:58 -06:00
theeggorchicken a0c82c8e4c fix: race condition in signup allows multiple admin accounts (#21631)
The signup_handler function checks has_users() before inserting a new user
and assigns the admin role based on that check. With multiple uvicorn workers,
concurrent signup requests during first-user registration can all observe an
empty user table before any insert completes, causing multiple accounts to
receive the admin role.

Fix: insert with the default role first, then check user count after the
insert. Only promote to admin if this is the only user in the database.
This eliminates the TOCTOU window between the check and the insert.
2026-02-21 15:37:08 -06:00
Timothy Jaeryang Baek 631e30e22d refac 2026-02-21 15:35:34 -06:00
Timothy Jaeryang Baek c114fd6876 refac 2026-02-21 15:33:21 -06:00
G30 c2172e43eb fix(ui): prevent drag-and-drop ghost cursors and text highlighting on admin and workspace navigation tabs (#21701) 2026-02-21 15:23:58 -06:00
Timothy Jaeryang Baek 1ad3656872 refac 2026-02-21 15:22:50 -06:00
Timothy Jaeryang Baek ff7f38d343 refac 2026-02-21 15:20:31 -06:00
Timothy Jaeryang Baek bc482b9cce refac 2026-02-21 15:17:36 -06:00
Timothy Jaeryang Baek 4c94f5d434 refac 2026-02-21 15:16:22 -06:00
Timothy Jaeryang Baek 4b9f821b58 enh: OAUTH_GROUP_DEFAULT_SHARE 2026-02-21 15:08:06 -06:00
Timothy Jaeryang Baek 35598b8017 enh: ENABLE_ADMIN_ANALYTICS 2026-02-21 14:56:19 -06:00
Classic298 45e23c3ad0 perf: eliminate 2 redundant full chat deserialization on every message send (#21596)
* perf: eliminate 2 redundant full chat deserialization on every message send (#162)

Problem:
Every message send triggered get_chat_by_id_and_user_id which loads the
entire Chat row — including the potentially massive JSON blob containing
the full conversation history — even when the caller only needed a
simple yes/no ownership check or a single column value.

Two call sites in the message-send hot path were doing this:

1. main.py ownership verification: loaded the entire chat object including
   all message history JSON, then checked `if chat is None`. The JSON blob
   was immediately discarded — only the existence of the row mattered.

2. middleware.py folder check: loaded the entire chat object including all
   message history JSON, then read only `chat.folder_id` — a plain column
   on the chat table that requires zero JSON parsing.

Fix:
- Added `chat_exists_by_id_and_user_id()`: uses SQL EXISTS subquery which
  returns a boolean without loading any row data. The database can satisfy
  this from the primary key index alone.

- Added `get_chat_folder_id()`: queries only the `folder_id` column via
  `db.query(Chat.folder_id)`, which tells SQLAlchemy to SELECT only that
  single column instead of the entire row.

Both new methods preserve the same error handling semantics (return
False/None on exception) and user_id filtering (ownership check) as
the original get_chat_by_id_and_user_id.

Impact:
- Best case (typical): eliminates deserializing 2 full chat JSON blobs per
  message send. For long conversations (hundreds of messages with tool
  calls, images, file attachments), this blob can be multiple megabytes.
- Worst case: no regression — the new queries are strictly cheaper than
  the old ones (less data transferred, less Python object construction,
  no Pydantic model_validate overhead).
- The 3 remaining full chat loads in process_chat_payload (load_messages_from_db,
  add_file_context, chat_image_generation_handler) are left untouched as
  they genuinely need the full history and require separate analysis.

* Address maintainer feedback: rename method and inline call (#166)

- Rename chat_exists_by_id_and_user_id -> is_chat_owner
- Remove intermediate chat_owned variable; call is_chat_owner directly in if condition
2026-02-21 14:53:31 -06:00
lazariv 5759917f54 feat: Adding You.com as a web search provider (#21599)
* Add ydc.py provider implementation

* Add PersistentConfig entry for you.com

* Add Youcom search function import

* Update you.com configuration

* Add you.com as a web search engine option in frontend

* Add YOUCOM_API_KEY to main.py
2026-02-21 14:51:56 -06:00
Classic298 d247adb60c feat: add citation sources for fetch_url tool results (#21669)
feat: add citation sources for fetch_url tool results

URL fetches now produce clickable citation sources in the UI, matching
the existing behavior of search_web and knowledge file tools. When a
model calls fetch_url during native tool calling, the fetched URL
appears as a citable source with a content preview, giving users full
transparency into what pages the model referenced.
2026-02-21 14:49:19 -06:00
G30 8c713a171d fix(backend): catch 404 http exceptions before generalized exception block in files router (#21687) 2026-02-21 14:48:51 -06:00
Timothy Jaeryang Baek 7e42d727e8 refac 2026-02-21 14:39:28 -06:00
Classic298 9f7dd31e12 feat: scroll to rich ui once rendered (#21698)
* Update Chat.svelte

* Update Chat.svelte
2026-02-21 14:35:32 -06:00
Timothy Jaeryang Baek 5d4547f934 enh: RAG_EMBEDDING_CONCURRENT_REQUESTS 2026-02-21 14:33:48 -06:00
G30 5522b91c32 fix(ui): align profile dropdown items and prevent phantom synthetic drag clicks (#21699) 2026-02-21 14:31:35 -06:00
Timothy Jaeryang Baek 3242dad8ae refac 2026-02-21 14:29:40 -06:00
G30 6d8a6e6d8b fix(model-selector): resolve virtual scroll bug when typing quickly (#21659) 2026-02-21 14:22:50 -06:00
Timothy Jaeryang Baek 8265422ba0 refac 2026-02-21 14:22:20 -06:00
Timothy Jaeryang Baek 10c13b686c refac 2026-02-21 14:19:28 -06:00
Classic298 b1dc58ddb7 feat: add sortable columns to groups admin panel (#21692)
* feat: add sortable columns to groups admin panel

Make the Group and Users column headers in the admin groups list clickable to sort groups alphabetically by name or numerically by member count. Clicking a column toggles ascending/descending order, indicated by a chevron icon. When no sort is active, the default API order (by updated_at) is preserved.

* Update Groups.svelte

* Update Groups.svelte
2026-02-21 14:18:37 -06:00
Timothy Jaeryang Baek a9312d2537 refac 2026-02-21 14:15:32 -06:00
Classic298 4228bf71c4 fix: gate model default features on global config and user permissions (#21690)
fix: gate model default features on global config and user permissions

If you disabled code interpreter globally and in user permissions but
enabled it as a default feature on a model, the code interpreter pill
still appeared in the chat input. Same issue for web search and image
generation.

The setDefaults function in Chat.svelte activated model default features
based solely on the model's capability flag, ignoring whether the feature
was globally enabled or allowed by user permissions. Added the same
global config and user permission checks already used by the integrations
menu visibility and the features object sent to the backend.
2026-02-21 13:54:47 -06:00
Classic298 ac620118c1 fix group search (#21691) 2026-02-21 13:53:47 -06:00
G30 d650c987ec fix: resolve backend execution deadlock when syncing stats with cyclic chat history (#21681) 2026-02-20 23:04:36 -05:00
Timothy Jaeryang Baek 092a358b3c refac 2026-02-20 16:55:06 -06:00
Timothy Jaeryang Baek ae05586fda refac: oauth session management 2026-02-20 16:49:43 -06:00
G30 f5e5632afc fix(chat): prevent stuck drop overlay when dragging outside window in firefox (#21664) 2026-02-20 14:01:24 -06:00
Timothy Jaeryang BaekandLeandro Ygor Loli 2a804541e0 refac
Co-Authored-By: Leandro Ygor Loli <77518998+leandroyloli@users.noreply.github.com>
2026-02-19 16:57:32 -06:00
Timothy Jaeryang Baek 8c485b260f refac 2026-02-19 16:53:21 -06:00
Classic298andTim Baek d664922feb Avoid loading full chat JSON blob for pinned/archived/shared list endpoints (#21591)
Co-authored-by: Tim Baek <tim@openwebui.com>
2026-02-19 16:48:23 -06:00
fchevallieratecna 9950cc8c28 fix(i18n): correct French typo "Analtique" → "Analytique" (#21512) 2026-02-19 16:42:57 -06:00
Classic298 3db6d49e57 Query title column directly in get_chat_title_by_id instead of loading full chat (#157) (#21590)
Previously loaded the entire ChatModel (including the full conversation JSON
blob) just to extract the title string. Now queries only the Chat.title
column directly, which is already a top-level DB column.
2026-02-19 16:41:46 -06:00
VasilyLebedev123andVasily Lebedev 6d67ac371d fix: correct unpacking order of distances, documents, and metadatas in hybrid search query (#21562)
Co-authored-by: Vasily Lebedev <Vasily.Lebedev@sapowernetworks.com.au>
2026-02-19 16:38:40 -06:00
Classic298andJordan 326599b8db Fix O(n²) performance in get_message_list by replacing insert(0) with append+reverse (#21588)
Co-authored-by: Jordan <CenteredAxis@users.noreply.github.com>
2026-02-19 16:38:01 -06:00
Classic298 c5c31ab769 fix: respect BYPASS_ADMIN_ACCESS_CONTROL in file list/search endpoints (#21595) 2026-02-19 16:36:48 -06:00
Kylapaallikko 43eb2351d2 Update fi-FI translation.json (#21538)
Added missing translations.
2026-02-19 16:36:32 -06:00
Timothy Jaeryang Baek 0a700aafe4 refac 2026-02-19 16:32:41 -06:00
Timothy Jaeryang Baek 91a0301c9e refac 2026-02-19 16:29:19 -06:00
Timothy Jaeryang Baek 6ac593209c refac 2026-02-19 16:09:54 -06:00
Shirasawa 12bea8cd88 i18n: improve Chinese translation (#21530) 2026-02-19 16:06:23 -06:00
joaoback 1dfe546b6b i18n: pt-BR - add translations for newly added UI items + consistency pass (#21527)
Translate all remaining untranslated strings to Brazilian Portuguese (pt-BR)

Translated ~100 previously untranslated entries (empty "" values) across the Open WebUI i18n JSON file. Changes include:

Translated UI labels, form fields, tooltips, and error messages
Kept brand/product names unchanged where appropriate (e.g., Bing, Brave, Gemini, OpenAI, YouTube)
Translated technical terms with context-appropriate Brazilian Portuguese equivalents (e.g., "Timeout" → "Tempo limite", "Config" → "Configuração", "Endpoint URL" → "URL do Endpoint")
Added translations for search engine integrations (Kagi, Perplexity, SerpApi, Tavily, Yacy, Yandex, etc.)
Translated plural forms for source retrieval messages (sources_one, sources_many, sources_other)
No untranslated entries remain in the file.
2026-02-19 16:06:08 -06:00
Classic298and:o ff837031e4 Update iframe sandbox attributes based on settings (#21529)
Co-authored-by: :o <52920416+gg0h@users.noreply.github.com>
2026-02-19 16:05:47 -06:00
Timothy Jaeryang Baek 139f02a9d9 refac 2026-02-19 16:04:41 -06:00
Timothy Jaeryang Baek 4bef69cc63 refac 2026-02-19 16:03:03 -06:00
Timothy Jaeryang Baek 723185c22f refac 2026-02-19 15:59:58 -06:00
Classic298 35763a352c Optimize shared chats list to use column projection (#163) (#21614)
The GET /chats/shared endpoint was loading full Chat rows including
the entire conversation history JSON blob, only to discard it and
return SharedChatResponse (id, title, share_id, timestamps). Now
uses with_entities() to select only the 5 needed columns, avoiding
deserialization of potentially large chat JSON for every shared chat.
2026-02-19 15:50:03 -06:00
Patrick MonteithandClaude Sonnet 4.6 27c76c677a fix: clamp SCIM pagination args instead of rejecting them (#21577)
RFC 7644 §3.4.2.4 specifies that out-of-range pagination values MUST be
clamped, not rejected. The previous implementation used FastAPI Query
constraints (ge=1, le=100) which caused a 422 response for values like
startIndex=0 or count=9999 — violating the spec.

For both /Users and /Groups:
- startIndex < 1 is now treated as 1 (spec: "SHALL be interpreted as 1")
- count < 0 is now treated as 0 (spec: "SHALL be interpreted as 0")
- count > 100 is clamped to the server maximum of 100

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 15:08:42 -06:00
2f1344d619 Update translation.json (#21602)
Typography issue correction

Co-authored-by: Tim Baek <tim@openwebui.com>
Co-authored-by: joaoback <156559121+joaoback@users.noreply.github.com>
2026-02-19 14:15:31 -06:00
Timothy Jaeryang Baek 8bfab327ec refac 2026-02-19 14:14:36 -06:00
Minwoo 'Charlie' Choi 56246324b2 fix: apply AIOHTTP_CLIENT_TIMEOUT to embeddings endpoint (#21558) 2026-02-19 14:13:50 -06:00
Classic298 af5661c2c8 Merge pull request #21485 from Classic298/claude/fix-mcp-ssl-check-0janH
fix: mcp ssl check
2026-02-19 14:08:15 -06:00
Timothy Jaeryang Baek f872a178bc refac 2026-02-19 14:06:24 -06:00
Timothy Jaeryang Baek 3dd44c4f19 refac 2026-02-18 15:23:50 -06:00
Timothy Jaeryang Baek 094ed0b48c fix: prompts delete 2026-02-18 14:58:39 -06:00
Timothy Jaeryang Baek 9b55343509 refac 2026-02-18 14:43:07 -06:00
Timothy Jaeryang Baek 8a7f698e9d refac 2026-02-18 14:42:00 -06:00
Timothy Jaeryang Baek 990c638f6c refac 2026-02-18 14:40:40 -06:00
Timothy Jaeryang Baek a0195cd5ae refac 2026-02-18 14:33:18 -06:00
Timothy Jaeryang Baek e9d852545c refac 2026-02-18 14:24:42 -06:00
Timothy Jaeryang Baek 49c36238d0 refac 2026-02-18 13:54:59 -06:00
Timothy Jaeryang Baek 74988189b8 refac 2026-02-18 13:06:50 -06:00
Tim Baek b8112d72b9 Merge pull request #21507 from open-webui/dev
refac
2026-02-17 01:25:39 -06:00
Timothy Jaeryang Baek 71ccedd2bf refac 2026-02-17 01:25:18 -06:00
Tim Baek 4225791313 Merge pull request #21458 from open-webui/dev
0.8.3
2026-02-17 01:19:39 -06:00
Timothy Jaeryang Baek e5cd1b479b refac 2026-02-17 01:12:57 -06:00
Timothy Jaeryang Baek 61d44aa773 refac 2026-02-17 01:12:33 -06:00
Timothy Jaeryang Baek ef036529b5 chore: format 2026-02-17 01:11:56 -06:00
Timothy Jaeryang Baek e0bdef85ab chore: format 2026-02-17 01:11:48 -06:00
Timothy Jaeryang Baek 2ce935bdb1 refac 2026-02-17 01:10:19 -06:00
Timothy Jaeryang Baek 23b1e2cca4 refac 2026-02-17 01:00:19 -06:00
Timothy Jaeryang Baek 05b8768fb9 refac 2026-02-17 00:48:49 -06:00
Timothy Jaeryang Baek 173d5631ca refac 2026-02-17 00:31:34 -06:00
Timothy Jaeryang Baek 34cd3d79e8 refac 2026-02-16 23:52:32 -06:00
Classic298 aede1b7a08 chore: Changelog updates (#21497)
* changelog: add model edit shortcut entry

* changelog: add Chinese translation updates for 0.8.3

* changelog: fix USER_EMAIL template variable

* changelog: add fix for missing function error handling in models endpoint

* changelog: startup error handling, translation updates

* changelog: PostgreSQL group query fix

* changelog: images, attachments, vision

* changelog: model edit, image handling, user_email, groups, startup

* changelog: image-edit, model-shortcut, general-improvements

* changelog: image edit API background support

* changelog: image API support, function error handling
2026-02-16 18:33:58 -06:00
Timothy Jaeryang Baek 15b893e651 refac 2026-02-16 15:32:28 -06:00
Classic298 4896d30281 chore: changelog (#21474)
* changelog: add model edit shortcut entry

* changelog: add Chinese translation updates for 0.8.3

* changelog: fix USER_EMAIL template variable

* changelog: add fix for missing function error handling in models endpoint

* changelog: startup error handling, translation updates

* changelog: PostgreSQL group query fix
2026-02-16 14:19:33 -06:00
Timothy Jaeryang Baek 9be45f49e4 refac 2026-02-16 14:13:49 -06:00
Timothy Jaeryang Baek f1053d94c7 refac 2026-02-16 14:08:35 -06:00
Timothy Jaeryang Baek 10cfddccd7 refac: styling 2026-02-16 13:39:41 -06:00
Timothy Jaeryang Baek e5e39be90f refac 2026-02-16 13:14:40 -06:00
Timothy Jaeryang Baek 337109e99c refac 2026-02-16 13:10:01 -06:00
joaoback 8adf2b33b4 i18n: (pt-BR): add translations for newly added UI items + consistency pass (#21472)
i18n: (pt-BR): add translations for newly added UI items + consistency pass
2026-02-16 13:08:38 -06:00
Classic298 1984ce42aa Update Chat.svelte (#21479) 2026-02-16 13:03:22 -06:00
Classic298andClaude 656de56a3e fix: gracefully handle missing functions when loading models (#21476)
When models reference functions (via filterIds/actionIds) that no longer
exist in the database, the /api/models endpoint crashes with a 500 error,
preventing the UI from loading chats entirely. This can happen after
upgrades when built-in functions are removed or when user-created
functions are deleted while still referenced by models.

Instead of raising an exception, log at INFO level and skip the missing
function so the rest of the models load successfully.

Fixes #21464

https://claude.ai/code/session_015JRM7m2bNeZPBBmV2Gv4Mj

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-16 13:02:54 -06:00
Shirasawa 0b2abe6cb8 I18n: improve Chinese translation (#21460)
* i18n: improve zh-CN translation

* i18n: improve zh-TW translation
2026-02-16 02:17:52 -06:00
Timothy Jaeryang Baek f364b2d205 refac 2026-02-16 02:05:19 -06:00
Timothy Jaeryang Baek 519ff40cb6 refac 2026-02-16 01:56:43 -06:00
Tim Baek 7c7fe44328 Merge pull request #21443 from open-webui/dev
0.8.2
2026-02-16 01:37:13 -06:00
Algorithm5838 15b5f97f89 fix: prevent scroll jump when editing large messages (#21402)
Save and restore scroll positions around textarea auto-resize to avoid
layout shifts. Use preventScroll on focus.
2026-02-16 01:33:51 -06:00
Shamil ca0983f76b i18n: Add missing Russian (ru-RU) translations (#21453)
* i18n: add missing Russian (ru-RU) translations

* i18n: add translations for API Keys and Asc keys
2026-02-16 01:27:23 -06:00
Timothy Jaeryang Baek ef04a704ce chore: format 2026-02-16 01:26:29 -06:00
Timothy Jaeryang Baek 5fda814669 doc: changelog 2026-02-16 01:25:40 -06:00
Timothy Jaeryang Baek c26e8110af enh: renderMarkdownInPreviews 2026-02-16 01:23:56 -06:00
Timothy Jaeryang Baek 9e85055b8b doc: changelog 2026-02-16 00:55:23 -06:00
Timothy Jaeryang Baek 6d17de6c67 chore: bump 2026-02-16 00:53:36 -06:00
Timothy Jaeryang Baek f4e99c80f6 refac: "tool_calls" finish reason support 2026-02-16 00:53:01 -06:00
Timothy Jaeryang Baek 09dc28df1e chore: format 2026-02-16 00:43:32 -06:00
Timothy Jaeryang Baek c748c3ede7 refac 2026-02-16 00:41:36 -06:00
Timothy Jaeryang Baek 33308022f0 refac 2026-02-15 23:57:40 -06:00
Timothy Jaeryang Baek f96e8f04fc refac: styling 2026-02-15 23:50:34 -06:00
Timothy Jaeryang Baek 38ae91ae23 refac 2026-02-15 23:35:40 -06:00
Timothy Jaeryang Baek 88401e91c7 refac 2026-02-15 23:28:47 -06:00
Timothy Jaeryang Baek 8c5cfa530d refac: styling 2026-02-15 22:55:50 -06:00
Timothy Jaeryang Baek d215e46315 refac: styling 2026-02-15 22:49:56 -06:00
Classic298 e10e7d056e chore: changelog (#21424)
* changelog: oauth session error handling

* changelog: task model selector validation fix

* changelog: skill content handling

* changelog: add Spanish and German translations

* changelog: move skill content to Added section

* changelog: tool call, validation fixes

* changelog: tool call message preservation links

* changelog: update per-model tool toggles entry with per-conversation chat toggle feature

* changelog: tool-server, startup, initialization

* changelog: remove empty Changed section

* changelog: fix duplicate Fixed section

* changelog: revert old entry, add new entry to 0.8.2

* changelog: move built-in tool toggles to Added section

* changelog: update date to 2026-02-16

* changelog: resource handle cleanup, file descriptors, leak fix

* changelog: image preview, file modal, modal enhancement
2026-02-15 19:33:20 -06:00
Timothy Jaeryang Baek 7a7d902238 refac 2026-02-15 19:32:22 -06:00
Varun Chawla 24179cde2f fix: preserve trailing slash in MCP server URLs (#21212)
Stop trimming trailing slashes from MCP server URLs on save. Some MCP
servers (e.g. Bitrix24) require a trailing slash; removing it triggers a
301 redirect that drops Authorization headers, resulting in 400 errors.

The trailing-slash trim is now skipped when the connection type is 'mcp',
while OpenAPI connections continue to have it stripped as before.

Fixes open-webui#21179
2026-02-15 19:09:41 -06:00
Timothy Jaeryang Baek 3ae4c618e1 refac 2026-02-15 19:07:53 -06:00
Timothy Jaeryang Baek 4a0d893995 refac 2026-02-15 19:03:08 -06:00
Timothy Jaeryang Baek b780d5c556 refac 2026-02-15 18:41:16 -06:00
Timothy Jaeryang BaekandDario Ruellan 911eecac85 fix: disabled mcp display issue
Co-Authored-By: Dario Ruellan <6965667+druellan@users.noreply.github.com>
2026-02-15 18:10:18 -06:00
Timothy Jaeryang Baek 319d3e8856 refac 2026-02-15 17:55:59 -06:00
Timothy Jaeryang Baek e1b3e7252c enh: preview image in file modal 2026-02-15 17:53:20 -06:00
Timothy Jaeryang BaekandSpootyMcSpoot f20cc6d7e6 refac
Co-Authored-By: SpootyMcSpoot <6732450+spootymcspoot@users.noreply.github.com>
2026-02-15 17:29:55 -06:00
Timothy Jaeryang Baek f1a1e64d2e refac: explicit toggle builtin tools 2026-02-15 17:20:49 -06:00
Classic298 58e923fe00 Update translation.json (#21441) 2026-02-15 16:43:50 -06:00
Timothy Jaeryang Baek f2aca781c8 refac: tool message handling 2026-02-15 16:14:47 -06:00
_00_ ce51c481b8 upd:i18n: Spanish Translation Update v.0.8.0 (#21427)
### Spanish Translation Update v.0.8.0

- Added new strings
2026-02-15 16:05:45 -06:00
Timothy Jaeryang Baek 9a2595f070 fix: task models issue 2026-02-14 22:26:03 -06:00
Timothy Jaeryang Baek 393c0071dc refac: manual skill invocation 2026-02-14 19:22:17 -06:00
Timothy Jaeryang Baek 7e224e4a53 refac 2026-02-13 18:26:03 -06:00
Tim Baek 883f1dda0f Merge pull request #21346 from open-webui/dev
0.8.1
2026-02-13 18:04:11 -06:00
Classic298 12bad452fa chore: Changelog updates (#21382)
* Add v0.8.1 release section

* changelog: knowledge menu layout fix

* changelog: knowledge tooltip z-index fix

* changelog: sync modal community sharing fix

* changelog: postgresql distinct ordering fix

* changelog: security fix public sharing bypass

* changelog: add issue ref to postgresql fix

* changelog: fix postgresql skills json compatibility

* changelog: apply new format style to 0.8.1 entries

* changelog: web search result count fix

* changelog: metadata, document, crash fix

* changelog: add channel user active status performance entry

* changelog: add model and prompt list optimization entry

* changelog: batch access control queries, channel status, model list optimization

* changelog: user list, performance, deferred loading

* Update CHANGELOG.md for 0.8.1

* Add emoji variation to Added section

* Remove empty Changed section and finalize 0.8.1 changelog

* changelog: arm, torch compatibility fix

* changelog: update database migration warning format

* changelog: ollama cloud, model naming fix

* Add SCIM externalId entry and database migration warning to 0.8.1

* Fix: move web search to Added, restore 0.8.0 headers

* Fix: SCIM above translations, 0.8.0 restored

* Remove 0.8.1 migration warning, keep 0.8.0 original

* changelog: direct model access control fix

* changelog: add commit link to direct model access control fix

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* changelog: sqlite, cascade delete, database fix

* changelog: responses, api, model-routing

* Add PR and issue links to SCIM externalId changelog entry

* Add commit link to Responses API entry, remove PR link from translation entry

* changelog: reasoning traces, performance, browser

* changelog: password, validation, regex

* Update CHANGELOG.md
2026-02-13 17:46:52 -06:00
Timothy Jaeryang Baek 5de60dc922 refac 2026-02-13 17:44:52 -06:00
Taylor Wilsdon a30b106ea3 fix issues/21399 (#21400) 2026-02-13 17:42:34 -06:00
Timothy Jaeryang Baek d33ad462aa refac 2026-02-13 17:38:57 -06:00
Timothy Jaeryang Baek 3b61562c82 refac 2026-02-13 17:26:54 -06:00
Timothy Jaeryang Baek e5d88be4f3 doc: changelog 2026-02-13 15:08:20 -06:00
Timothy Jaeryang Baek b36f8d9314 chore: format 2026-02-13 15:00:47 -06:00
Timothy Jaeryang Baek 626d236d13 chore: format 2026-02-13 15:00:39 -06:00
Timothy Jaeryang Baek 79ecbfc757 refac 2026-02-13 14:59:20 -06:00
Timothy Jaeryang Baek a9b8677cc0 refac 2026-02-13 14:59:05 -06:00
Classic298 0f3f68b0c4 enh (#21362) 2026-02-13 14:56:53 -06:00
Timothy Jaeryang BaekandJuan Calderon-Perez abc9b63093 refac
Co-Authored-By: Juan Calderon-Perez <835733+gaby@users.noreply.github.com>
2026-02-13 14:55:13 -06:00
Timothy Jaeryang Baek 64fa26bd28 refac 2026-02-13 14:48:10 -06:00
Timothy Jaeryang Baek 2487c84f1f refac: styling 2026-02-13 14:47:00 -06:00
Timothy Jaeryang Baek 163211a367 refac: styling 2026-02-13 14:43:06 -06:00
Timothy Jaeryang Baek f027a01ab2 fix: direct model access control 2026-02-13 14:39:01 -06:00
Classic298 370a677a38 fix: pin torch to prevent startup errors on ARM devices (#21385)
* fix: rpi

* Update requirements-min.txt

* Update requirements.txt

* Update pyproject.toml
2026-02-13 14:28:23 -06:00
Timothy Jaeryang Baek d1d1efe212 refac: scim 2026-02-13 14:27:11 -06:00
Timothy Jaeryang Baek b7549d2f6c refac: defer profile 2026-02-13 14:08:07 -06:00
Timothy Jaeryang Baek 589c4e64c1 refac 2026-02-13 13:56:29 -06:00
Timothy Jaeryang Baek 20de5a87da refac 2026-02-13 13:43:43 -06:00
Timothy Jaeryang Baek ca6b18ab5c refac: is_user_active 2026-02-13 13:40:59 -06:00
Classic298 97a3b1528d Update utils.py (#21105) 2026-02-13 13:37:12 -06:00
Classic298 d01b1d4880 enh: apply admin default to builtin web search (#21373) 2026-02-13 13:32:48 -06:00
Timothy Jaeryang Baek df6e38039f refac 2026-02-13 13:29:22 -06:00
Timothy Jaeryang Baek b4c3f54f96 fix: skills postgres issue 2026-02-13 11:24:08 -06:00
Classic298 73776d54b8 fix: enforce public sharing permission checks across all resource types (#21358)
The sharePublic prop in editor components (Knowledge, Tools, Skills,
Prompts, Models) incorrectly included an "|| edit" / "|| write_access"
condition, allowing users with write access to see and use the "Public"
sharing option regardless of their actual public sharing permission.
Additionally, all backend access/update endpoints only verified write
authorization but did not check the corresponding sharing.public_*
permission, allowing direct API calls to bypass frontend restrictions
entirely.
Frontend: removed the edit/write_access bypass from sharePublic in all
five editor components so visibility is gated solely by the user's
sharing.public_* permission or admin role.
Backend: added has_public_read_access_grant checks to the access/update
endpoints in knowledge.py, tools.py, prompts.py, skills.py, models.py,
and notes.py. Public grants are silently stripped when the user lacks
the corresponding permission.
Fixes #21356
2026-02-13 11:22:32 -06:00
Timothy Jaeryang BaekandEntropyYue 7bda6bf767 fix: PostgreSQL cannot use get_chat_ids_by_model_id
Co-Authored-By: EntropyYue <164553692+EntropyYue@users.noreply.github.com>
2026-02-13 11:20:26 -06:00
G30 ddcec9842f fix: ensure sync modal only triggers if community sharing is enabled (#21376) 2026-02-13 11:18:58 -06:00
G30 4d5b7b3014 fix: resolve knowledge tooltip z-index issue in model edit page (#21375) 2026-02-13 11:18:35 -06:00
G30 9886ebb97f fix: resolve knowledge collection indentation/truncation issue by correcting flex layout (#21374) 2026-02-13 11:18:14 -06:00
Timothy Jaeryang Baek 0b05b2fc7e refac 2026-02-13 00:44:01 -06:00
joaoback 49e7eade15 Merge pull request #21345 from joaoback/patch-25
i18n: Update translation.json (pt-BR)
2026-02-12 19:42:52 -06:00
Tim Baek 7a7a25766c Merge pull request #21314 from open-webui/dev
0.8.0
2026-02-12 17:42:25 -06:00
Timothy Jaeryang Baek 9fc1658085 refac 2026-02-12 17:42:15 -06:00
Timothy Jaeryang Baek 5297dceb2a chore: format 2026-02-12 17:36:19 -06:00
Timothy Jaeryang Baek 59afbd6f92 refac 2026-02-12 17:35:22 -06:00
Timothy Jaeryang Baek f7af3f010e refac 2026-02-12 17:27:38 -06:00
Timothy Jaeryang Baek bb40724d45 refac 2026-02-12 17:21:53 -06:00
Timothy Jaeryang Baek 0e64b31adb refac 2026-02-12 17:17:45 -06:00
Timothy Jaeryang Baek 850a864b02 refac 2026-02-12 16:54:32 -06:00
Timothy Jaeryang Baek 9468d92553 refac 2026-02-12 16:44:08 -06:00
Timothy Jaeryang Baek c3dc5d5984 refac 2026-02-12 16:41:57 -06:00
Timothy Jaeryang Baek 5291b3dca2 refac 2026-02-12 16:36:00 -06:00
Classic298andClassic298 87d0c112fa Update CHANGELOG.md (#20594)
* 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

* docs(changelog): update CHANGELOG.md for file upload settings fix

* docs: add RTL markdown table display fix to CHANGELOG

* changelog: add GPU usage fix for online status indicator

- Fixed high GPU usage (35-40% to near-zero) caused by animate-ping CSS
  animation on the user online status indicator

Refs: #21062, 938d1b0

* docs(changelog): add ANSI color code fix for code execution

- Fixed code execution reliability when LLMs include ANSI terminal
  color codes in their output
- Prevents random syntax errors during code execution
- Related to issue #21091

* fix: restore tool call expand/collapse functionality

A refactor moved tool call display logic into a separate component but inadvertently disabled the collapsible behavior because tool calls store content in attributes rather than body text.

Fixes #21205

* changelog: fix malformed Authorization header crash in Fixed section

- Add entry for malformed Authorization header fix (issue #20938)

The APIKeyRestrictionMiddleware now validates Authorization headers before
processing them, preventing ValueError and HTTP 500 errors.

* Add notification toast close button entry to CHANGELOG

* Update changelog date to 2026-02-07

* fix: respect ENABLE_OLLAMA_API and ENABLE_OPENAI_API flags when url_idx is provided

* Update CHANGELOG.md

* Add Latvian (lv-LV) language translation

* changelog: add relative time indicators for chat updates

* Update CHANGELOG.md

* changelog: add active chat tasks sidebar feature

* changelog: Update date to 2026-02-10 and fix tool call result decoding issue (#20600)

* changelog: Consolidate Open Responses protocol commits

* changelog: Add Irish translations

* changelog: icons, dark-mode, display

* changelog: Fix dark mode icon display issue (#21272)

* changelog: duplicate, icons, dark-mode

* changelog: access-control-ui, sharing-users, api-groups-users

* changelog: reorder access control entries for visibility

* changelog: move api entry down

* changelog: batch-file-ops, knowledge-base, performance

* changelog: add Gboard Android clipboard fix entry

* changelog: opensearch, knowledge-base, typefix

* changelog: add Gboard Android fix entry

* Update CHANGELOG.md

* changelog: expand user sharing entry

* changelog: remove emphasis word from user sharing

* changelog: reformulate user sharing entry per guidelines

* changelog: notifications, new-chat-fallback, device-scoped

* changelog: fix embeddings endpoint batch input for Ollama

* changelog: replace backticks with single quotes per style guidelines

* changelog: ssl-embeddings, aiohttp-session-ssl

* changelog: add Ollama auto-port fallback entry

* changelog: add independent access control endpoints and permission fixes

* changelog: consolidate access control entries and improve permission fix description

* changelog: model selector, virtual scrolling, performance

* changelog: update date to February 11, 2026

* changelog: pagination, model settings, admin

* changelog: add Skill standard feature entry

* changelog: add French to translations list

* changelog: action-html, iframe, rich-content

* changelog: tool call, html entities, multi-turn

* changelog: redis model cache, performance

* changelog: inline-images, context-window, fix-20916

* changelog: fix inline images context window exhaustion

* changelog: audio, timeout, api

* changelog: cache optimization, performance, ttft

* changelog: ldap, authentication, freeze

* changelog: async pipelines, event loop

* changelog: streaming, connection, cleanup

* changelog: add commits for Action Rich UI refactor and group performance

* changelog: model access control, batch queries, database optimization

* changelog: trusted-header, authentication, user-registration

* changelog: add pinned read-only models entry

* changelog: update date to February 12, 2026

* Update CHANGELOG.md

* Update CHANGELOG.md

* changelog: add issue #17058 to streaming response cleanup entry

* changelog: reorder Fixed section by importance priority

* Update CHANGELOG.md

* changelog: add warning about chat message table migration duration

* changelog: openai-api-router, model-cache-lookup, bugfix

* changelog: ollama-model-name-fix, provider-compatibility

* changelog: fix Ollama-compatible provider model name issue #21331

* changelog: OpenAI API endpoint detection fix

* changelog: model list and import performance optimization

* changelog: external tool event emitter message id support #21214

* changelog: fix large document upload worker timeout #21158

* changelog: fix RedisCluster task stopping

* changelog: add discussion link to #21158

* changelog: add commit c653e4ec to #21158 entry

* changelog: functions, chat_id, internal_tasks

* changelog: channel permissions security fix

---------

Co-authored-by: Classic298 <27028174+Classic298@users.noreply.github.com@>
2026-02-12 16:15:05 -06:00
Timothy Jaeryang Baek 2a11175f22 chore: format 2026-02-12 16:13:48 -06:00
Timothy Jaeryang Baek 3238d94a0e refac 2026-02-12 15:57:27 -06:00
Timothy Jaeryang Baek 8919d8a82a refac 2026-02-12 15:52:50 -06:00
Classic298andTim Baek ea4ef28da5 init (#20883)
Co-authored-by: Tim Baek <tim@openwebui.com>
2026-02-12 15:50:13 -06:00
Timothy Jaeryang Baek 2ffd8d9277 refac 2026-02-12 15:40:13 -06:00
Classic298 e8499ccdd1 fix: pass chat_id to internal task calls for consistent function context (#20585)
Ensure chat_id is reliably passed to function pipelines/manifolds during internal task invocations (web search query generation, RAG query generation, image prompt generation).

This allows stateful functions to maintain per-chat state without fragmentation, as they will now receive a consistent chat_id for all chat-scoped invocations including internal tasks.

Backend changes:
- Pass chat_id in generate_queries call for web search
- Pass chat_id in generate_queries call for RAG/retrieval
- Pass chat_id in generate_image_prompt call

Frontend changes:
- Add optional chat_id parameter to generateQueries API function
- Add optional chat_id parameter to generateAutoCompletion API function

Fixes #20563
2026-02-12 15:36:42 -06:00
Timothy Jaeryang Baek 0dcbd05e24 refac 2026-02-12 15:29:34 -06:00
Timothy Jaeryang Baek c653e4ec54 refac 2026-02-12 15:25:24 -06:00
Classic298andClaude 8cf32ae2a7 fix: prevent worker death during document upload by using run_coroutine_threadsafe (#21158)
* fix: prevent worker death during document upload by using run_coroutine_threadsafe

Replace asyncio.run() with asyncio.run_coroutine_threadsafe() in
save_docs_to_vector_db() to prevent uvicorn worker health check failures.

The issue: asyncio.run() creates a new event loop and blocks the thread
completely, preventing the worker from responding to health checks during
long-running embedding operations (>5 seconds default timeout).

The fix: Schedule the async embedding work on the main event loop using
run_coroutine_threadsafe(). This keeps the main loop responsive to health
check pings while the sync caller waits for the result.

Changes:
- main.py: Store main event loop reference in app.state.main_loop at startup
- retrieval.py: Use run_coroutine_threadsafe() instead of asyncio.run()

https://claude.ai/code/session_01UQSYvSTkXb57sFb7M85Kcw

* add env var

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-12 15:22:57 -06:00
Classic298 c6af296b60 add message id (#21214) 2026-02-12 15:04:27 -06:00
Timothy Jaeryang Baek 531ac70ce5 refac 2026-02-12 12:07:45 -06:00
Timothy Jaeryang Baek 423d8b1817 refac 2026-02-12 11:04:34 -06:00
EntropyYue 633505460a i18n: improve zh-CN translation (#21319) 2026-02-12 11:02:41 -06:00
Aleix Dorca c8bf390680 i18n: Update catalan translation (#21320)
* Update catalan translation.json type

* Update catalan translation.json
2026-02-12 11:02:25 -06:00
Classic298 da46c1bbd2 Update translation.json (#21324) 2026-02-12 11:02:10 -06:00
Timothy Jaeryang Baek 05ae44b98d refac 2026-02-12 11:01:46 -06:00
Timothy Jaeryang Baek a40808579f refac 2026-02-12 10:59:41 -06:00
Timothy Jaeryang Baek 4d024c91d6 refac 2026-02-11 18:33:35 -06:00
Timothy Jaeryang Baek ccb71a7322 refac 2026-02-11 18:32:14 -06:00
Classic298andMichael efe5416f83 fix: reduce TTFT by caching model lookups in chat completion (#20886)
fix: reduce TTFT by caching model lookups in chat completion

Skip expensive get_all_models() calls when models are already cached
in app.state. This significantly reduces Time To First Token (TTFT)
for chat completions and embeddings requests.

Previously, every request called get_all_models() which fetches model
lists from all configured backends. Now we check the cache first and
only call get_all_models() on cache miss.

Affected endpoints:
- openai: generate_chat_completion, embeddings
- ollama: embed, embeddings

Fixes #20069

Co-authored-by: Michael <42099345+mickeytheseal@users.noreply.github.com>
2026-02-11 18:29:10 -06:00
Timothy Jaeryang Baek a4281f6a7f refac: ldap 2026-02-11 18:25:37 -06:00
Timothy Jaeryang Baek 2372b70031 refac: async pipelines requests 2026-02-11 18:24:30 -06:00
Classic298andClaude d02e826c9d Fix idle in transaction leaks in Open WebUI (#20868)
* fix: add ScopedSession.remove() to prevent idle transaction leaks

The HTTP middleware was calling ScopedSession.commit() but not
ScopedSession.remove(), causing database connections to remain
"checked out" from the pool indefinitely. This resulted in
"idle in transaction" connections in PostgreSQL that could persist
for 30-50+ minutes.

With SQLAlchemy's scoped_session:
- commit() commits but keeps the session active
- remove() is required to return the connection to the pool

This fix adds the missing remove() call, ensuring connections are
properly returned after each HTTP request.

Also includes IDLE_TRANSACTION_ANALYSIS.md documenting the full
root cause analysis and additional recommendations.

* Delete IDLE_TRANSACTION_ANALYSIS.md

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-11 18:20:03 -06:00
Timothy Jaeryang BaekandClassic298 dddac2b0ca refac
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2026-02-11 18:19:01 -06:00
Timothy Jaeryang BaekandClassic298 0da57149ae refac
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2026-02-11 18:13:30 -06:00
Classic298 0bebb260bf fix: decode HTML entities in tool call results for multi-turn conversations (#20755) 2026-02-11 18:11:41 -06:00
Thomas Rehn 390d7663b0 fix: don't pollute model text context with encoded images (#20916) 2026-02-11 18:10:22 -06:00
Thomas Rehn ce3a615442 perf: cache OpenAI config reads to avoid redundant Redis lookups in /api/models (#21306)
Each access to request.app.state.config.<KEY> triggers a synchronous
Redis GET. In get_all_models_responses() and get_merged_models(), the
config keys OPENAI_API_BASE_URLS, OPENAI_API_KEYS, and
OPENAI_API_CONFIGS were read on every loop iteration — resulting
in some cases in 200-300 Redis round-trips for OPENAI_API_BASE_URLS alone.

Read each config value once into a local variable at the start of the
function and reuse it throughout.
2026-02-11 17:59:50 -06:00
Timothy Jaeryang BaekandJannik S. 4e0cb88583 refac: audio timeout
Co-Authored-By: Jannik S. <jannik@streidl.dev>
2026-02-11 17:56:49 -06:00
Timothy Jaeryang Baek 9b30e8f689 refac 2026-02-11 17:53:01 -06:00
Timothy Jaeryang Baek 97331bf11d refac 2026-02-11 17:26:25 -06:00
Timothy Jaeryang Baek 96c07f44a8 refac 2026-02-11 16:45:47 -06:00
Timothy Jaeryang Baek 60ada21c15 refac 2026-02-11 16:40:40 -06:00
Classic298andClaude ab20745ee5 Enable Rich UI embed support for action functions (#21294)
Action functions can now return HTMLResponse objects or (html, headers)
tuples with Content-Disposition: inline to render rich UI iframes in
chat, matching the existing tool behavior.

https://claude.ai/code/session_01KCZKQXj1uqgPjqjMd4U2NF

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-11 16:27:00 -06:00
Timothy Jaeryang Baek f376d4f378 chore: format 2026-02-11 16:24:11 -06:00
Florian 89fddcc741 update french translation.json (#21295) 2026-02-11 16:22:55 -06:00
Timothy Jaeryang Baek 773787c74c refac 2026-02-11 16:16:41 -06:00
Tim Baek 5c1c9a4dcb Merge pull request #21312 from open-webui/skills
feat: skills
2026-02-11 16:01:08 -06:00
Timothy Jaeryang Baek 9b925a115a refac 2026-02-11 16:00:19 -06:00
Timothy Jaeryang Baek e5035ea31e refac 2026-02-11 15:55:23 -06:00
Timothy Jaeryang Baek c8cbdc8f7f refac 2026-02-11 15:24:12 -06:00
Timothy Jaeryang Baek 64c37ab968 refac 2026-02-11 15:12:37 -06:00
Timothy Jaeryang Baek f7c5965a70 feat: $ skills mention 2026-02-11 15:08:59 -06:00
Timothy Jaeryang Baek 46aa54b7dc refac 2026-02-11 14:35:45 -06:00
Timothy Jaeryang Baek 27944cf7ca refac 2026-02-11 14:26:56 -06:00
Timothy Jaeryang Baek 1973115678 feat: skills frontend 2026-02-11 14:22:26 -06:00
Timothy Jaeryang Baek a38ad8fc42 refac 2026-02-11 14:09:55 -06:00
Timothy Jaeryang Baek c2207887b3 feat: skills backend 2026-02-11 14:00:34 -06:00
Timothy Jaeryang Baek 3fabc085cc feat: skills migration 2026-02-11 03:58:28 -06:00
Timothy Jaeryang Baek 2f584c9f88 refac 2026-02-11 02:31:17 -06:00
Timothy Jaeryang Baek ba18d6250a refac 2026-02-11 02:26:01 -06:00
Timothy Jaeryang Baek 4331029926 refac 2026-02-11 02:20:07 -06:00
Timothy Jaeryang Baek 3e56261c5e refac 2026-02-11 02:06:43 -06:00
Timothy Jaeryang Baek 30f72672fa refac 2026-02-10 15:57:08 -06:00
Timothy Jaeryang Baek 4aedfdc547 refac 2026-02-10 15:47:21 -06:00
Timothy Jaeryang Baek e3a8257690 refac 2026-02-10 15:41:11 -06:00
Timothy Jaeryang Baek a73cdf4288 refac: dmr fallback for ollama 2026-02-10 15:38:21 -06:00
Timothy Jaeryang Baek c259c87806 refac 2026-02-10 15:30:16 -06:00
Timothy Jaeryang Baek 0044902c08 refac 2026-02-10 15:15:17 -06:00
Timothy Jaeryang Baek cd31b8301b refac 2026-02-10 12:44:31 -06:00
Timothy Jaeryang Baek 8fd5c06e5b refac 2026-02-10 12:23:08 -06:00
Classic298andClaude 97afe3bc58 enh: skip chat notifications on other devices if temporary chat (#21292)
* Merge pull request #20581 from Classic298/fix/db-pool-memory-update (#150)

Co-authored-by: Claude <noreply@anthropic.com>
Fixes #21290

* Update +layout.svelte

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-10 13:21:57 -05:00
Timothy Jaeryang Baek 3567054325 refac 2026-02-09 18:22:28 -06:00
Classic298 f236192fe1 fix: resolve N+1 query in knowledge batch file add (#21006) 2026-02-09 16:17:30 -06:00
Varun Chawla 9b1fd86aa7 fix: use keyword argument for IndicesClient.refresh() for opensearch-py 3.x (#21248)
In opensearch-py >= 3.0.0, IndicesClient.refresh() no longer accepts the
index name as a positional argument. This causes a TypeError when
uploading documents to knowledge bases with OpenSearch backend.

Changes positional arguments to keyword arguments (index=...) in all
three refresh() calls in the OpenSearch vector DB client.

Fixes #20649
2026-02-09 16:16:44 -06:00
Classic298 55169e69c0 chore: Dep bump (#21261)
* Update pyproject.toml

* Update requirements-min.txt

* Update package versions in requirements.txt

Updated various package versions in requirements.txt to latest releases.
2026-02-09 16:15:39 -06:00
Algorithm5838 e3e4e1d9d3 fix: handle Gboard clipboard strip multi-line paste via beforeinput (#21265)
Gboard's clipboard suggestion strip sends multi-line pastes as
'insertText' in beforeinput rather than a standard paste event,
causing ProseMirror to drop the text content and insert only a
newline. Add a beforeinput handler that intercepts multi-line
insertText events on Android and manually inserts with hard breaks
to preserve the formatting.
2026-02-09 16:15:08 -06:00
Timothy Jaeryang Baek c2f5cb542e refac 2026-02-09 14:03:35 -06:00
Timothy Jaeryang Baek 1034b74abd refac 2026-02-09 13:48:48 -06:00
Timothy Jaeryang Baek 0a44d80252 refac 2026-02-09 13:42:27 -06:00
Tim Baek 48a0abb40f Merge pull request #21277 from open-webui/acl
refac: acl
2026-02-09 13:34:36 -06:00
Timothy Jaeryang Baek 68c77295bd refac 2026-02-09 13:31:59 -06:00
Timothy Jaeryang Baek 3c7f9aa6a4 refac 2026-02-09 13:28:21 -06:00
Timothy Jaeryang Baek f7406ff576 refac 2026-02-09 13:28:14 -06:00
65904b867e i18n: Updated Irish translation (#21253)
Co-authored-by: Tim Baek <tim@openwebui.com>
Co-authored-by: joaoback <156559121+joaoback@users.noreply.github.com>
2026-02-09 14:25:28 -05:00
Tim Baek e2d09ac361 refac 2026-02-09 09:06:48 +04:00
Timothy Jaeryang Baek 3ae44d11a5 refac 2026-02-08 22:37:13 -06:00
Tim Baek aa8c2959ca refac 2026-02-09 08:07:33 +04:00
Timothy Jaeryang Baek b147616080 refac 2026-02-08 21:32:12 -06:00
Timothy Jaeryang Baek 9747b07ca5 refac 2026-02-08 21:24:38 -06:00
Timothy Jaeryang Baek 0f78451c2b refac 2026-02-08 21:24:20 -06:00
Timothy Jaeryang Baek 42763cbbd8 enh: access grants table 2026-02-08 21:21:25 -06:00
Timothy Jaeryang Baek d193c143a5 refac 2026-02-08 21:18:38 -06:00
Tim Baek 26460917c4 refac 2026-02-08 07:20:28 +04:00
Tim Baek fd97ae9bd7 refac 2026-02-08 06:48:41 +04:00
Tim Baek c3fa0f30fe refac 2026-02-08 06:45:01 +04:00
Tim Baek 4852227158 refac 2026-02-08 06:22:56 +04:00
Timothy Jaeryang Baek 9be85b6d3c refac: access control migration 2026-02-07 19:07:49 -06:00
Tim Baek 59b98ab730 refac 2026-02-08 03:42:57 +04:00
Tim Baek 690686f3c7 refac 2026-02-08 03:40:54 +04:00
Tim Baek 691a04f0dd refac 2026-02-08 03:39:29 +04:00
4fa6123c04 i18n: Add Latvian translation (#21234)
Co-authored-by: Tim Baek <tim@openwebui.com>
Co-authored-by: joaoback <156559121+joaoback@users.noreply.github.com>
Co-authored-by: Edgaras0x4E <edgaras217t@gmail.com >
2026-02-08 03:33:34 +04:00
Classic298 494cf8b3ef fix (#21226) 2026-02-08 03:19:26 +04:00
Tim Baek 73bb600034 refac 2026-02-06 23:35:21 +04:00
Tim Baek 9cf4d34832 refac 2026-02-06 23:21:09 +04:00
Tim Baek 284b97bd84 refac 2026-02-06 22:46:56 +04:00
Tim Baek 7e79f8d1c6 refac 2026-02-06 22:39:20 +04:00
Tim Baek 258454276e fix: files settings save issue 2026-02-06 22:33:49 +04:00
Tim Baek 938d1b0743 refac 2026-02-06 22:29:16 +04:00
Tim Baek b1737040a7 refac 2026-02-06 22:25:18 +04:00
Timothy Jaeryang Baek 26286625f4 refac 2026-02-05 23:40:58 -06:00
Tim Baek a214ec40ea fix 2026-02-06 03:34:21 +04:00
Tim Baek 2c37daef86 refac 2026-02-06 03:23:37 +04:00
Tim Baek 8e79b3d0bc refac 2026-02-06 03:03:34 +04:00
Tim Baek 6c0f886cdf refac 2026-02-06 02:48:35 +04:00
Shirasawa 225036863f I18n: improve Chinese translation (#21181)
* i18n: improve zh-CN translation

* i18n: improve zh-TW translation
2026-02-06 02:28:37 +04:00
G30 cac5dd12e9 fix: handle null data in model_response_handler (#21112)
Fix `AttributeError` in `model_response_handler` when processing channel messages with `null` data field. The function iterates over thread messages to build conversation history, but some messages may have `data=None` causing a crash when accessing `thread_message.data.get()`. Added null check using `(thread_message.data or {}).get("files", [])` to safely handle messages without data.
2026-02-05 15:15:34 -05:00
Classic298 f751c0b46c Update builtin.py (#21115) 2026-02-05 15:14:58 -05:00
Classic298 9ed8f50d40 fix: bundle NLTK punkt_tab in Docker image for airgapped environments (#21165)
Pre-download NLTK punkt_tab during Docker build instead of at runtime.
This fixes document extraction failures in offline/airgapped environments
where the container cannot download the tokenizer data after restarts.
Fixes #21150
2026-02-05 15:11:00 -05:00
Tim Baek d3f2cf7474 refac 2026-02-05 10:24:12 +04:00
Tim Baek 62750b8980 feat: queue messages 2026-02-05 10:06:37 +04:00
Timothy Jaeryang Baek e62649f940 enh: analytics 2026-02-05 00:00:49 -06:00
Timothy Jaeryang Baek 0e60c757ce refac 2026-02-04 23:47:16 -06:00
Timothy Jaeryang Baek 68a1e87b66 enh: analytics model modal 2026-02-04 23:42:46 -06:00
Timothy Jaeryang Baek e8a36f033b refac 2026-02-04 23:05:08 -06:00
Timothy Jaeryang Baek 2cf2565e80 refac 2026-02-04 22:57:46 -06:00
Timothy Jaeryang Baek 5669d1062c refac 2026-02-04 21:54:45 -06:00
Timothy Jaeryang Baek 3ace75820e refac 2026-02-04 21:46:20 -06:00
Timothy Jaeryang Baek 020cb0d4bf refac 2026-02-04 21:09:05 -06:00
Timothy Jaeryang Baek 8b75d34a8a refac 2026-02-04 21:07:30 -06:00
Timothy Jaeryang Baek 6320a9aaa9 refac 2026-02-04 20:47:21 -06:00
Timothy Jaeryang Baek 6cd35b185d refac 2026-02-04 00:13:05 -06:00
Timothy Jaeryang Baek a1ea854b38 refac 2026-02-04 00:08:46 -06:00
Timothy Jaeryang Baek 405dc26cc6 refac 2026-02-04 00:04:09 -06:00
Timothy Jaeryang Baek fe681abd33 refac: tags 2026-02-03 23:58:11 -06:00
Timothy Jaeryang Baek 43c68468f7 refac 2026-02-03 23:38:03 -06:00
Timothy Jaeryang Baek ecf3fa2feb refac 2026-02-03 23:36:15 -06:00
EntropyYue 4aacaeb9b8 fix: Shortcut i18n (#21138) 2026-02-03 16:52:38 -06:00
Timothy Jaeryang Baek afc56b9746 refac 2026-02-03 16:34:29 -06:00
Timothy Jaeryang Baek 0a61666197 refac 2026-02-03 16:33:12 -06:00
Thomas Rehn a9e0462e57 feat: add keyboard shortcut for opening the model selector (#21130) 2026-02-03 14:09:02 -06:00
Tim Baek a2b9986a75 Merge pull request #21129 from alpha-pet/fix-redis-cluster-otel
fix: OTEL instrumentation for redis in cluster mode
2026-02-03 14:07:51 -06:00
Thomas Rehn 654172d757 fix: redis clustermode instrumentation 2026-02-03 15:25:37 +01:00
Timothy Jaeryang Baek 527d48efa9 refac 2026-02-02 10:04:13 -06:00
Tim Baek cda08aaed4 Merge pull request #21106 from open-webui/chat-message-rebased
feat: analytics
2026-02-02 09:34:18 -06:00
Tim Baek cfd30581d5 Merge branch 'dev' into chat-message-rebased 2026-02-02 09:33:41 -06:00
Timothy Jaeryang Baek 3c0313f41b refac 2026-02-02 09:31:52 -06:00
Timothy Jaeryang Baek d938eb0e76 refac 2026-02-02 09:23:46 -06:00
Classic298 60f2f8c1c4 Update pyproject.toml (#21087) 2026-02-02 09:21:39 -06:00
Timothy Jaeryang Baek b0c5f7b668 refac 2026-02-02 09:02:34 -06:00
Timothy Jaeryang Baek 767343dc5b refac 2026-02-02 08:57:26 -06:00
Timothy Jaeryang Baek c22bb4f853 refac 2026-02-02 08:55:13 -06:00
Timothy Jaeryang Baek 117c091b95 refac 2026-02-01 20:07:11 -06:00
Timothy Jaeryang Baek 6719558150 refac 2026-02-01 20:06:38 -06:00
Timothy Jaeryang Baek 6ffce4bccd refac 2026-02-01 20:00:21 -06:00
Timothy Jaeryang Baek ea9c58ea80 feat: experimental responses api support 2026-02-01 19:39:28 -06:00
Tim Baek b2c2f1bd49 refac 2026-02-01 10:24:04 +04:00
Tim Baek 679e56c494 feat: token analytics 2026-02-01 10:19:59 +04:00
Tim Baek 3da4323ef3 refac 2026-02-01 10:11:13 +04:00
Tim Baek 75e5a485d2 refac 2026-02-01 10:06:52 +04:00
Tim Baek 7bb3a827bb refac 2026-02-01 08:10:25 +04:00
Tim Baek 96f106319e refac 2026-02-01 07:59:02 +04:00
Tim Baek a4ad34841b feat: analytics frontend dashboard
- Add Dashboard with summary stats, model/user tables
- Add ChartLine component with multi-model support
- Interactive hover tooltips and model breakdown
- Hourly granularity for 24h, daily for 7d+
2026-02-01 07:04:23 +04:00
Tim Baek 599cd2eeeb feat: analytics backend API with chat_message table
- Add chat_message table for message-level analytics with usage JSON field
- Add migration to backfill from existing chats
- Add /analytics endpoints: summary, models, users, daily
- Support hourly/daily granularity for time-series data
- Fill missing days/hours in date range
2026-02-01 07:04:13 +04:00
Tim Baek ee5fd1246c refac 2026-02-01 03:26:59 +04:00
Timothy Jaeryang Baek 1441d0d735 refac 2026-01-31 17:03:29 -06:00
Timothy Jaeryang Baek e5dbfc420d refac 2026-01-31 17:03:24 -06:00
Classic298 643c661a6f chore: Updates minor/patch versions for 21 Python backend dependencies (#21059) 2026-01-31 16:51:08 -06:00
Timothy Jaeryang Baek ab5dfbda54 refac 2026-01-30 11:12:54 -05:00
Timothy Jaeryang Baek 94302de49b feat: images playground 2026-01-30 10:54:04 -05:00
Timothy Jaeryang Baek 45d7486485 refac 2026-01-30 10:43:40 -05:00
Classic298 ee27fd8de1 perf: replace double lookup with single JOIN in authenticate_user_by_email (#21010)
* perf: replace double lookup with single JOIN in authenticate_user_by_email

* Update auths.py
2026-01-30 10:22:07 -05:00
Classic298 89f154630f fix 404 (#21047) 2026-01-30 10:15:40 -05:00
Timothy Jaeryang Baek a6ed0ef9f4 refac 2026-01-30 10:13:14 -05:00
Classic298 aac98120c8 perf: batch fetch filter functions to eliminate N+1 queries (#21018) 2026-01-30 00:50:04 +04:00
Timothy Jaeryang Baek 44e36e5b0d refac 2026-01-30 00:46:56 +04:00
Timothy Jaeryang BaekandHsienz f9ab66f51a refac
Co-Authored-By: Hsienz <55347238+hsienz@users.noreply.github.com>
2026-01-30 00:46:42 +04:00
Classic298 ce8ed5b5ec feat(files): add shift+click quick delete to File Manager (#21044)
* feat(files): add shift+click quick delete to File Manager

Add shift+click functionality to FilesModal for rapid file deletion without confirmation dialogs.
Changes:
- Track Shift key state via keyboard event listeners
- When Shift is held, delete button bypasses confirmation and deletes immediately
- Visual feedback: delete icon turns red when Shift is held
- Optimized delete to remove file from local array instead of re-fetching entire list, enabling rapid successive deletions without UI flicker
This matches the quick delete pattern used in other workspace components like Tools, Prompts, and Models.

* Update FilesModal.svelte
2026-01-30 00:18:39 +04:00
Classic298 baef422a28 perf: eliminate redundant query after memory update (#21013)
## Summary
Eliminates redundant database query in update_memory_by_id_and_user_id. Previously, after modifying the memory object, it called get_memory_by_id which opened a new session and queried again.
## Changes
models/memories.py update_memory_by_id_and_user_id:
- Replace self.get_memory_by_id(id) with db.refresh(memory)
- Return the same memory object that was already modified
## Performance Impact
Before: 2 queries (get + get_memory_by_id)
After: 1 query + refresh on same session
2026-01-29 21:45:09 +04:00
Classic298 68e257849d perf: optimize database queries in functions, feedbacks, and groups (#21019) 2026-01-29 21:44:50 +04:00
Classic298 e686554392 fix: resolve N+1 query in SCIM group_to_scim user lookup (#21005) 2026-01-29 21:43:33 +04:00
Timothy Jaeryang Baek 96a9696383 enh: admin models 2026-01-29 21:33:56 +04:00
ShirasawaandTim Baek fa84ff5e12 I18n: improve Chinese translation (#20902)
* i18n: improve zh-CN translation

* i18n: improve zh-TW translation

---------

Co-authored-by: Tim Baek <tim@openwebui.com>
2026-01-29 20:58:26 +04:00
7. Sun 66359d5815 fix: clean up all event listeners in +layout.svelte onMount (#20913)
Extract anonymous touch event handlers into named functions and add
proper cleanup for all event listeners (message, touchstart, touchmove,
touchend, visibilitychange) in the onMount return function.

Previously only the resize listener was being cleaned up, causing
memory leaks during navigation and hot-reloads.
2026-01-29 20:50:25 +04:00
Classic298 c111fa0837 fix: add lang="ts" to Svelte script tags using TypeScript syntax (#21001)
fix: add lang="ts" to Svelte script tags using

TypeScript syntax
Two Svelte components were using TypeScript type annotations (ReturnType<typeof setTimeout>)
but their script tags were missing the lang="ts" attribute. This caused Vite/Svelte to parse
them as plain JavaScript, resulting in parse errors.

Fixed files:
- src/lib/components/admin/Users/UserList.svelte
- src/lib/components/channel/ChannelInfoModal/UserList.svelte
2026-01-29 20:49:25 +04:00
Timothy Jaeryang Baek 6e182940e2 refac 2026-01-29 20:30:22 +04:00
Timothy Jaeryang Baek bc90463ea6 refac 2026-01-29 20:15:23 +04:00
Timothy Jaeryang Baek 93ed4ae2cd enh: files data controls 2026-01-29 19:50:06 +04:00
Timothy Jaeryang Baek a10ac774ab enh: manage shared chats 2026-01-29 18:51:02 +04:00
Timothy Jaeryang Baek 26a5d8f75d refac 2026-01-29 18:05:07 +04:00
Classic298 5749f78262 fix: reduce triple query to single fetch in model toggle (#21009) 2026-01-29 17:24:09 +04:00
Classic298 1eaae9d934 perf: eliminate redundant SELECT after UPDATE in users.py (#21011) 2026-01-29 17:22:28 +04:00
Cevat Batuhan Tolon 567b0776cd REDIS_RECONNECT_DELAY as positive float for handle retry delay on timeout/connection errors (#21021) 2026-01-29 17:13:03 +04:00
72f330133a add slovak translations (#21032)
Co-authored-by: Tim Baek <tim@openwebui.com>
Co-authored-by: joaoback <156559121+joaoback@users.noreply.github.com>
Co-authored-by: Tomáš Lokša <loksa@kros.sk>
2026-01-29 17:11:35 +04:00
Timothy Jaeryang Baek 665f95eda3 refac 2026-01-28 01:18:39 +04:00
Tim Baek 8e2b0b6fd2 enh: playground export 2026-01-27 16:17:17 -05:00
Timothy Jaeryang Baek ce50d9bac4 refac 2026-01-28 01:14:22 +04:00
Timothy Jaeryang Baek 934bebd8cd refac 2026-01-28 01:02:10 +04:00
Timothy Jaeryang Baek 171940869b refac 2026-01-28 00:49:12 +04:00
7. Sun 33020d826f perf: parallelize image loading in image_edits endpoint (#20911)
Use asyncio.gather() to load multiple images concurrently instead of
sequentially, significantly reducing latency for multi-image edit
operations.
2026-01-28 00:35:25 +04:00
Classic298 2c12278444 perf: Debounce various Database Endpoints for less Database Queries and better Backend performance (#20982)
* Update KnowledgeSelector.svelte

* Update KnowledgeSelector.svelte

* Update Users.svelte

* Update MemberSelector.svelte

* Update MemberSelector.svelte

* Update Knowledge.svelte

* Update Knowledge.svelte

* Update Notes.svelte

* Update Knowledge.svelte

* Update Prompts.svelte

* Update Tools.svelte

* Update Tools.svelte

* Update Prompts.svelte

* Update Prompts.svelte

* Update Prompts.svelte

* Update Functions.svelte

* Update UserList.svelte

* Update Functions.svelte

* Update Prompts.svelte

* Update UserList.svelte
2026-01-28 00:33:23 +04:00
Timothy Jaeryang Baek 57a2024c58 refac 2026-01-27 23:55:49 +04:00
Classic298 d67fe02263 Update translation.json (#20959) 2026-01-27 23:40:17 +04:00
Timothy Jaeryang Baek 57ec2aa088 refac 2026-01-27 23:34:23 +04:00
Timothy Jaeryang Baek fa859de460 refac 2026-01-27 23:21:31 +04:00
Timothy Jaeryang Baek 36766f157d refac: prompts pagination 2026-01-27 23:01:56 +04:00
Timothy Jaeryang Baek 683438b418 refac 2026-01-27 21:37:20 +04:00
Timothy Jaeryang Baek 4a55167759 refac 2026-01-26 18:04:58 +04:00
c5c4aef7b1 Yandex web search (#20922)
Co-authored-by: Tim Baek <tim@openwebui.com>
Co-authored-by: joaoback <156559121+joaoback@users.noreply.github.com>
2026-01-26 07:31:44 -05:00
Peter Dave Hello 533c7b27eb i18n: Improve zh-TW Traditional Chinese translation (#20920) 2026-01-26 07:30:56 -05:00
Tim Baek 82af218790 Merge pull request #20945 from open-webui/prompt_versioning
enh: prompts
2026-01-26 16:27:28 +04:00
Classic298 b272ca5e88 fix: remove invalid expunge call on Pydantic FileModel (#20931)
fix: remove invalid expunge call on Pydantic FileModel
Files.get_file_by_id() returns a Pydantic FileModel, not an SQLAlchemy
ORM object. Calling db.expunge() on a Pydantic model fails with
UnmappedInstanceError since it lacks _sa_instance_state.
The expunge was also unnecessary because subsequent DB updates already
use fresh sessions via get_db() context manager.
Fixes #20925
2026-01-26 07:24:53 -05:00
Timothy Jaeryang Baek 10ba2accf7 refac 2026-01-26 16:18:49 +04:00
Timothy Jaeryang Baek 4c8d4e6dbd enh: prompt tags 2026-01-26 16:11:10 +04:00
Tim Baek 6359628bc3 refac 2026-01-26 07:01:05 -05:00
Classic298 25fd342261 Update retrieval.py (#20930) 2026-01-26 15:29:15 +04:00
Timothy Jaeryang Baek f199c486a2 refac 2026-01-24 04:29:56 +04:00
Timothy Jaeryang Baek 1f205a8441 refac 2026-01-24 04:19:51 +04:00
Timothy Jaeryang Baek b1d5b3b28e refac 2026-01-24 04:17:11 +04:00
Timothy Jaeryang Baek 32810b4152 refac 2026-01-24 04:13:19 +04:00
Timothy Jaeryang Baek b7e9992d78 refac 2026-01-24 03:25:29 +04:00
Timothy Jaeryang Baek 6c76983999 refac 2026-01-24 03:21:47 +04:00
Timothy Jaeryang Baek 8bf46dcc5d refac 2026-01-24 03:16:34 +04:00
Timothy Jaeryang Baek 5510fa178e refac 2026-01-24 03:10:29 +04:00
Timothy Jaeryang Baek 5ad593e465 refac: prompt endpoints 2026-01-24 03:08:48 +04:00
Timothy Jaeryang Baek dff0141160 refac 2026-01-24 02:58:23 +04:00
Timothy Jaeryang Baek 44da9c6523 refac 2026-01-24 02:57:08 +04:00
Timothy Jaeryang Baek 6ab7d54982 refac 2026-01-24 02:51:44 +04:00
Timothy Jaeryang Baek 0c79a566ac feat: prompt history frontend 2026-01-24 02:40:17 +04:00
Timothy Jaeryang Baek 34773e795b wip: prompt history backend integration 2026-01-24 02:39:48 +04:00
Timothy Jaeryang Baek 66daa15722 wip: prompt history models 2026-01-24 02:39:29 +04:00
Timothy Jaeryang Baek db80dd2692 feat: prompt history table 2026-01-24 02:38:57 +04:00
Tim Baek 0dc74a8a2e refac 2026-01-23 08:33:21 -05:00
Tim Baek 90a057f400 refac 2026-01-23 08:25:19 -05:00
Timothy Jaeryang Baek 78f856e204 enh: quick prompt copy 2026-01-23 16:40:11 +04:00
Tim Baek d2c695eb11 feat: add convert_output_to_messages for OR-aligned message building 2026-01-22 19:54:05 -05:00
Timothy Jaeryang Baek 46cf40ec82 refac 2026-01-23 04:31:39 +04:00
Timothy Jaeryang Baek 655420fd25 feat: ENABLE_OAUTH_TOKEN_EXCHANGE 2026-01-23 04:25:39 +04:00
Timothy Jaeryang Baek 52c73390f8 refac 2026-01-23 01:44:48 +04:00
Timothy Jaeryang Baek c46ef3b63b enh: builtin tools model editor 2026-01-23 01:40:18 +04:00
G30 443908b14c fix: enforce Allow Chat Valves permission in integrations menu (#20691)
- The "Allow Chat Valves" permission was only hiding valves in the Chat Controls sidebar, but users could still bypass this by accessing valves through the integrations menu in the chat input field. This fix adds the permission check to hide Valves buttons for both tools and filters/functions in the integrations menu.
2026-01-22 16:25:00 -05:00
Timothy Jaeryang Baek 3bec320bb9 refac 2026-01-23 01:23:34 +04:00
Mikhail GubenkoandCopilot fa6f238777 docs: update pr template with critical checks and improve guidelines (#20872)
* docs: update pull request template with critical checks and improved guidelines

* Update .github/pull_request_template.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* docs: solidify pr checklist definitions and statements

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-22 16:20:18 -05:00
Tim Baek 86e6b2b68b refac: audit 2026-01-22 16:18:16 -05:00
Timothy Jaeryang Baek 14e51e0977 refac 2026-01-23 01:02:41 +04:00
Timothy Jaeryang Baek 4c6f100b5f refac 2026-01-23 00:56:50 +04:00
Classic298 5a0488bb18 init (#20881) 2026-01-22 20:30:07 +04:00
Timothy Jaeryang Baek 9af40624c5 refac 2026-01-22 18:58:00 +04:00
Timothy Jaeryang Baek 0df561c33c refac 2026-01-22 18:40:54 +04:00
Timothy Jaeryang Baek c7f996d593 refac: AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL for mcp connections 2026-01-22 18:30:16 +04:00
Timothy Jaeryang Baek 907dba4517 feat: easter eggs flag 2026-01-22 18:27:42 +04:00
G30 9e5d6069fe feat: Sort Tools and Functions dropdowns alphabetically (#20871)
Alphabetically sort Tools and Functions selection dropdowns in Chat Controls sidebar for easier navigation and better user experience.
2026-01-22 07:07:07 -05:00
Timothy Jaeryang Baek 14f6747dfc refac 2026-01-22 15:07:14 +04:00
Timothy Jaeryang Baek 68b2872ed6 fix/refac: file batch process issue 2026-01-22 15:03:31 +04:00
Timothy Jaeryang Baek 1a4bdd2b30 refac 2026-01-22 14:59:15 +04:00
Timothy Jaeryang Baek 886c12c566 refac 2026-01-22 14:54:00 +04:00
Timothy Jaeryang Baek a3600e8b21 refac 2026-01-22 04:12:57 +04:00
Timothy Jaeryang Baek 5d48e48e15 fix: ENABLE_PERSISTENT_CONFIG redis issue 2026-01-22 03:59:00 +04:00
Timothy Jaeryang Baek 474427c67e enh: dynamic select options valve 2026-01-22 03:55:07 +04:00
Classic298 00b3583dc2 fix: fix reindex not working due to unnecessary dupe check (#20857)
* Update retrieval.py

* Update knowledge.py

* Update retrieval.py

* Update knowledge.py
2026-01-21 18:36:08 -05:00
Timothy Jaeryang Baek 4d9a7cc6c0 refac: fr-FR priority 2026-01-22 03:25:23 +04:00
Timothy Jaeryang Baek 509bd2bebb refac/fix: default oauth2.1 mcp tool 2026-01-22 03:20:59 +04:00
Timothy Jaeryang Baek 8c70453b2e enh: password valve type 2026-01-22 03:18:38 +04:00
Timothy Jaeryang Baek 8eebc2aea6 fix: mcp get_discovery_urls 2026-01-22 03:11:33 +04:00
Timothy Jaeryang Baek a9a0ce6bea refac 2026-01-22 03:09:04 +04:00
Timothy Jaeryang Baek ecbdef732b enh: PDF_LOADER_MODE 2026-01-21 23:51:36 +04:00
Timothy Jaeryang Baek 4615e8f92b refac 2026-01-20 22:28:10 +04:00
Timothy Jaeryang Baek 91faa9fd5a refac 2026-01-20 16:42:20 +04:00
Timothy Jaeryang Baek 85e92fe3b0 refac 2026-01-20 16:41:46 +04:00
Classic298 38bf0b6eec feat: Add new ENV VAR for custom error message on error on signup / password change due to password not meeting requirements (#20650)
* add env var for custom auth pw message

* Update auth.py

* Update auth.py
2026-01-19 14:00:48 +04:00
G30 6ae3ddd66b fix: truncate long model names and IDs in UI (#20696)
- Added line-clamp-1 truncation and tooltips to long model names and IDs in the Evaluations and Models admin sections to prevent visual overflow. Model names now display truncated with full name and ID available on hover.
2026-01-19 13:59:44 +04:00
G30 98cb2d3411 feat: Add MATLAB syntax highlighting support for code blocks (#20773)
* feat: Add MATLAB syntax highlighting support for code blocks

Add MATLAB syntax highlighting support by fixing issues in both CodeEditor (editable) and CodeBlock (read-only) components.

Changes:
- CodeEditor.svelte: Added 'matlab' alias to CodeMirror's Octave language (MATLAB-compatible syntax)
- CodeBlock.svelte: Fixed highlight.js usage to use highlight() directly when language is recognized, falling back to highlightAuto() only for unknown languages

* revert: not needed
2026-01-19 13:58:48 +04:00
Timothy Jaeryang Baek be75bc506a refac 2026-01-19 13:49:12 +04:00
7a42efec53 i18n: Update translation.json (ja-JP) (#20772)
* i18n: Update Japanese translation

* i18n: Update Japanese translation

---------

Co-authored-by: Tim Baek <tim@openwebui.com>
Co-authored-by: joaoback <156559121+joaoback@users.noreply.github.com>
2026-01-19 13:42:58 +04:00
G30 e9926694c3 fix: add username search support to workspace and admin pages (#20780)
This fix restores and extends the username/email search functionality across workspace pages that was originally added in PR #14002. The issue was that:

1. The backend search functions for Models and Knowledge only searched `User.name` and `User.email`, but not `User.username`

2. The Functions admin page lacked user search entirely

Changes made:

Added User.username to backend search conditions for Models and Knowledge pages
Added complete user search (name, email, username) to the Functions admin page client-side filter
2026-01-19 13:42:33 +04:00
Timothy Jaeryang Baek 5cfb7a08cb refac 2026-01-17 21:52:12 +04:00
9d642f6354 Added support for connecting to self hosted weaviate deployments using connect_to_custom replacing connect_to_local, which is better suited for cases where HTTP and GRPC are hosted on different ingresses. (#20620)
Co-authored-by: Tim Baek <tim@openwebui.com>
Co-authored-by: joaoback <156559121+joaoback@users.noreply.github.com>
Co-authored-by: rohithshenoyg@gmail.com <rohithshenoyg@gmail.com>
2026-01-17 21:48:52 +04:00
Classic298 716f2986b9 dep bump (#20735) 2026-01-17 21:44:32 +04:00
Timothy Jaeryang Baek 409f565f09 refac 2026-01-17 21:41:48 +04:00
Classic298 26e95f2a92 fix-csv-export (#20688) 2026-01-17 21:39:24 +04:00
Kylapaallikko 711a2cd738 Update translation.json (#20741)
Added missing translations and minor changes to wording of the translations.
2026-01-17 21:37:14 +04:00
Classic298 1c1f72f05c Update builtin.py (#20705) 2026-01-16 00:15:02 +04:00
1d343aeae4 enh: Make builtin search web tools asynchronous (#20630)
Co-authored-by: Tim Baek <tim@openwebui.com>
Co-authored-by: joaoback <156559121+joaoback@users.noreply.github.com>
2026-01-15 10:46:00 +04:00
Kailey Wong e26f6acc3b fix: use proper X-Api-Key header format when docling api key provided (#20652) 2026-01-15 10:44:35 +04:00
G30 1555252c4a fix: handle undefined model in createMessagePair function (#20663)
- Add a null check when looking up model to prevent JavaScript error when trying to add a message pair in a chat with an invalid/corrupt model ID.
2026-01-15 10:43:25 +04:00
Timothy Jaeryang Baek de0cbb9073 refac 2026-01-12 21:56:02 +04:00
Timothy Jaeryang Baek 5a075a2c83 fix: members only groups 2026-01-12 21:53:41 +04:00
Timothy Jaeryang Baek 7da37b4f66 refac 2026-01-12 21:41:23 +04:00
6f80cb6b65 i18n: Update Japanese translation (#20605)
Co-authored-by: Tim Baek <tim@openwebui.com>
Co-authored-by: joaoback <156559121+joaoback@users.noreply.github.com>
2026-01-12 20:34:32 +04:00
Shirasawa 9617df04ae I18n: improve Chinese translation (#20613)
* i18n: improve zh-CN translation

* i18n: improve zh-TW translation
2026-01-12 20:34:13 +04:00
joaoback 01d5f42755 Update translation.json (pt-BR) (#20599)
translation of the new items recently added
2026-01-12 14:04:09 +04:00
Classic298 84d76cccde Update translation.json (#20593) 2026-01-12 00:41:28 +04:00
Classic298 af584b46f4 feat: code-interpreter native (#20592)
* code-interpreter native

* Update tools.py

* Update builtin.py

* Update builtin.py

* Update tools.py

* Update builtin.py

* Update builtin.py

* Update builtin.py

* Update builtin.py

* Update builtin.py

* Update builtin.py

* Update builtin.py

* Update builtin.py

* Update builtin.py

* Update builtin.py
2026-01-12 00:18:41 +04:00
_00_ 0fb4cceec1 UPD: i18n: Spanish es-ES Translation v0.7.0 (#20564)
### UPD: i18n: Spanish es-ES Translation v0.7.0

- Added new strings
2026-01-11 23:39:52 +04:00
Classic298 1dc353433a fix(db): release connection before embedding in memory /query (#20579)
Remove Depends(get_session) from POST /query endpoint to prevent database connections from being held during embedding API calls (1-5+ seconds).

The Memories.get_memories_by_user_id() function manages its own short-lived session internally, releasing the connection before the slow EMBEDDING_FUNCTION() call begins.
2026-01-11 23:37:47 +04:00
Classic298 33e8a09880 fix(db): release connection before embedding in knowledge /create (#20575)
Remove Depends(get_session) from POST /create endpoint to prevent database connections from being held during embedding API calls (1-5+ seconds).

The has_permission() and Knowledges.insert_new_knowledge() functions manage their own short-lived sessions internally, releasing connections before the slow embed_knowledge_base_metadata() call begins.
2026-01-11 23:37:05 +04:00
Classic298 1cb751d184 fix(db): release connection before embedding in knowledge /{id}/update (#20574)
Remove Depends(get_session) from POST /{id}/update endpoint to prevent database connections from being held during embedding API calls (1-5+ seconds).

All database operations (get_knowledge_by_id, has_access, has_permission, update_knowledge_by_id, get_file_metadatas_by_id) manage their own short-lived sessions internally, releasing connections before and after the slow embed_knowledge_base_metadata() call.
2026-01-11 23:36:36 +04:00
Classic298 9e596f8616 fix(db): release connection before LLM call in Ollama /v1/completions (#20570)
Remove Depends(get_session) from the /v1/completions endpoint to prevent database connections from being held during the entire duration of LLM calls.

Previously, the database session was acquired at request start and held until the response completed. Under concurrent load, this exhausted the connection pool, causing QueuePool timeout errors.

The fix allows Models.get_model_by_id() and has_access() to manage their own short-lived sessions internally, releasing the connection immediately after authorization checks complete.
2026-01-11 23:35:46 +04:00
Classic298 24044b42ea fix(db): release connection before LLM call in Ollama /v1/chat/completions (#20569)
Remove Depends(get_session) from the /v1/chat/completions endpoint to prevent database connections from being held during the entire duration of LLM calls.

Previously, the database session was acquired at request start and held until the streaming response completed. Under concurrent load, this exhausted the connection pool, causing QueuePool timeout errors.

The fix allows Models.get_model_by_id() and has_access() to manage their own short-lived sessions internally, releasing the connection immediately after authorization checks complete.
2026-01-11 23:35:38 +04:00
Aleix Dorca 84263fc6a6 i18n: Updated the Catalan translation file (#20566)
* Update translation.json

* Update catalan translation.json
2026-01-11 23:35:11 +04:00
2faab409d3 i18n(pl-PL): Add missing keys and update existing translations (#20562)
* Added lacking translations to Polish locale and improved some existing ones for better clarity.

* fix: add missing newline at EOF

---------

Co-authored-by: Tim Baek <tim@openwebui.com>
Co-authored-by: joaoback <156559121+joaoback@users.noreply.github.com>
2026-01-11 23:35:01 +04:00
Classic298 0b5aa6dd60 fix(db): release connection before LLM call in Ollama /api/chat (#20571)
Remove Depends(get_session) from the /api/chat endpoint to prevent database connections from being held during the entire duration of LLM calls (30-60+ seconds for streaming responses).

Previously, the database session was acquired at request start and held until the streaming response completed. Under concurrent load, this exhausted the connection pool, causing QueuePool timeout errors for other database operations.

The fix allows Models.get_model_by_id() and has_access() to manage their own short-lived sessions internally, releasing the connection immediately after the quick authorization checks complete - before the slow external LLM API call begins.
2026-01-11 23:34:23 +04:00
Classic298 d0c2bfdbff fix(db): release connection before LLM call in OpenAI /chat/completions (#20572)
Remove Depends(get_session) from the /chat/completions endpoint to prevent database connections from being held during the entire duration of LLM calls (30-60+ seconds for streaming responses).

Previously, the database session was acquired at request start and held until the streaming response completed. Under concurrent load, this exhausted the connection pool, causing QueuePool timeout errors for other database operations.

The fix allows Models.get_model_by_id() and has_access() to manage their own short-lived sessions internally, releasing the connection immediately after the quick authorization checks complete - before the slow external LLM API call begins.
2026-01-11 23:34:11 +04:00
Classic298 242625782f fix(db): release connection before embedding in memory /add (#20578)
Remove Depends(get_session) from POST /add endpoint to prevent database connections from being held during embedding API calls (1-5+ seconds).

The Memories.insert_new_memory() function manages its own short-lived session internally, releasing the connection before the slow EMBEDDING_FUNCTION() call begins.
2026-01-11 23:33:17 +04:00
Classic298 826e9ab317 fix(db): release connection before embeddings in knowledge /metadata/reindex (#20577)
Remove Depends(get_session) from POST /metadata/reindex endpoint to prevent database connections from being held during N embedding API calls.

This endpoint is CRITICAL as it loops through ALL knowledge bases and calls embed_knowledge_base_metadata() for each one. With the original code, a single connection would be held for the entire duration (potentially minutes for large deployments), completely exhausting the pool.

The Knowledges.get_knowledge_bases() function manages its own short-lived session, releasing the connection before the embedding loop begins.
2026-01-11 23:33:04 +04:00
Classic298 182d5e8591 fix(db): release connection before embedding in process_files_batch (#20576)
Remove Depends(get_session) from POST /process/files/batch endpoint to prevent database connections from being held during batch embedding API calls (5-60+ seconds for large batches).

The save_docs_to_vector_db() function makes external embedding API calls. Post-embedding file updates (Files.update_file_by_id) manage their own short-lived sessions internally, releasing connections promptly.
2026-01-11 23:32:56 +04:00
Classic298 3fc866117d fix(db): CRITICAL - prevent pool exhaustion in memory /reset (#20580)
Remove Depends(get_session) from POST /reset to prevent catastrophic connection pool exhaustion.

This endpoint was holding a SINGLE database connection while executing N PARALLEL embedding API calls via asyncio.gather(). For a user with 100 memories, this meant one connection blocked for potentially MINUTES (100 calls * 1-5 seconds each, even in parallel due to rate limits).

A single user triggering /reset could completely starve the connection pool, causing QueuePool timeout errors across the entire application.

The Memories.get_memories_by_user_id() function now manages its own short-lived session, releasing the connection immediately before the massive parallel embedding operation begins.
2026-01-11 23:32:40 +04:00
Classic298 b464b48f53 Merge pull request #20581 from Classic298/fix/db-pool-memory-update
fix(db): release connection before embedding in memory /{memory_id}/update
2026-01-11 23:32:27 +04:00
Tim Baek 2b26355002 Merge pull request #20560 from open-webui/dev
0.7.2
2026-01-11 01:00:01 +04:00
Timothy Jaeryang Baek d81a36310c chore: bump 2026-01-11 00:58:57 +04:00
Timothy Jaeryang Baek d56bb2c383 refac 2026-01-11 00:52:43 +04:00
Timothy Jaeryang Baek 2dd09223f2 refac/fix: duplicated api calls for evaluation 2026-01-10 23:58:40 +04:00
Timothy Jaeryang Baek 0c369d195b chore: format 2026-01-10 23:50:18 +04:00
Timothy Jaeryang Baek ab99d3b112 fix: prompt creation issue 2026-01-10 23:40:07 +04:00
Classic298 3f133fad56 fix: release database connections immediately after auth instead of holding during LLM calls (#20545)
fix: release database connections immediately after auth instead of holding during LLM calls

Authentication was using Depends(get_session) which holds a database connection
for the entire request lifecycle. For chat completions, this meant connections
were held for 30-60 seconds while waiting for LLM responses, despite only needing
the connection for ~50ms of actual database work.

With a default pool of 15 connections, this limited concurrent chat users to ~15
before pool exhaustion and timeout errors:

    sqlalchemy.exc.TimeoutError: QueuePool limit of size 5 overflow 10 reached,
    connection timed out, timeout 30.00

The fix removes Depends(get_session) from get_current_user. Each database
operation now manages its own short-lived session internally:

    BEFORE: One session held for entire request
    ──────────────────────────────────────────────────
    │ auth │ queries │ LLM wait (30s) │ save │
    │         CONNECTION HELD ENTIRE TIME            │
    ──────────────────────────────────────────────────

    AFTER: Short-lived sessions, released immediately
    ┌──────┐ ┌───────┐                 ┌──────┐
    │ auth │ │ query │   LLM (30s)     │ save │
    │ 10ms │ │ 20ms  │  NO CONNECTION  │ 20ms │
    └──────┘ └───────┘                 └──────┘

This is safe because:
- User model has no lazy-loaded relationships (all simple columns)
- Pydantic conversion (UserModel.model_validate) happens while session is open
- Returned object is pure Pydantic with no SQLAlchemy ties

Combined with the telemetry efficiency fix, this resolves connection pool
exhaustion for high-concurrency deployments, particularly on network-attached
databases like AWS Aurora where connection hold time is more impactful.
2026-01-10 15:34:36 +04:00
Classic298 41d1ccd39c Update channels.py (#20546) 2026-01-10 15:34:12 +04:00
Classic298 7839d043ff fix: use efficient COUNT queries in telemetry metrics to prevent connection pool exhaustion (#20542)
fix: use efficient COUNT queries in telemetry metrics to prevent connection pool exhaustion

This fixes database connection pool exhaustion issues reported after v0.7.0,
particularly affecting PostgreSQL deployments on high-latency networks (e.g., AWS Aurora).

## The Problem

The telemetry metrics callbacks (running every 10 seconds via OpenTelemetry's
PeriodicExportingMetricReader) were using inefficient queries that loaded entire
database tables into memory just to count records:

    len(Users.get_users()["users"])  # Loads ALL user records to count them

On high-latency network-attached databases like AWS Aurora, this would:
1. Hold database connections for hundreds of milliseconds while transferring data
2. Deserialize all records into Python objects
3. Only then count the list length

Under concurrent load, these long-held connections would stack up and drain the
connection pool, resulting in:

    sqlalchemy.exc.TimeoutError: QueuePool limit of size 5 overflow 10 reached,
    connection timed out, timeout 30.00

## The Fix

Replace inefficient full-table loads with efficient COUNT(*) queries using
methods that already exist in the codebase:

- `len(Users.get_users()["users"])` → `Users.get_num_users()`
- Similar changes for other telemetry callbacks as needed

COUNT(*) queries use database indexes and return a single integer, completing in
~5-10ms even on Aurora, versus potentially 500ms+ for loading all records.

## Why v0.7.1's Session Sharing Disable "Helped"

The v0.7.1 change to disable DATABASE_ENABLE_SESSION_SHARING by default appeared
to fix the issue, but it was masking the root cause. Disabling session sharing
causes connections to be returned to the pool faster (more connection churn),
which reduced the window for pool exhaustion but didn't address the underlying
inefficient queries.

With this fix, session sharing can be safely re-enabled for deployments that
benefit from it (especially PostgreSQL), as telemetry will no longer hold
connections for extended periods.

## Impact

- Telemetry connection usage drops from potentially seconds to ~30ms total per
  collection cycle
- Connection pool pressure from telemetry becomes negligible (~0.3% utilization)
- Enterprise PostgreSQL deployments (Aurora, RDS, etc.) should no longer
  experience pool exhaustion under normal load
2026-01-10 15:33:42 +04:00
G30 9b9e6ce2ab fix: correct empty STT_ENGINE handling and improve TTS error response (#20534)
- Remove incorrect 403 check that blocked STT when ENGINE="" (local whisper)
- Change TTS empty ENGINE check from 403 to 404 for proper semantics
2026-01-10 15:32:22 +04:00
Classic298 81510e9d8f fix(files): prevent connection pool exhaustion in file status streaming (#20547)
Refactored the file processing status streaming endpoint to avoid holding
a database connection for the entire stream duration (up to 2 hours).
Changes:
- Each status poll now creates its own short-lived database session instead
  of capturing the request's session in the generator closure
- Increased poll interval from 0.5s to 1s, halving database queries with
  negligible UX impact
This prevents a single file status stream from blocking a connection pool
slot for hours, which could contribute to pool exhaustion under load.
2026-01-10 15:23:48 +04:00
EntropyYue c0ff925c2a fix: Settings tab i18n (#20526) 2026-01-10 12:41:06 +04:00
joaoback 2da661fed1 Update translation.json (pt-BR) (#20527)
Translations of the new items added in the latest versions.
2026-01-10 12:37:59 +04:00
G30 59b128bbda Update ChannelItem.svelte (#20535) 2026-01-10 12:37:42 +04:00
Tim Baek f2a360cb87 Merge pull request #20523 from open-webui/dev
chore
2026-01-10 00:57:39 +04:00
Tim Baek 8deef788c1 chore 2026-01-09 15:57:06 -05:00
Tim Baek f9b0534e0c Merge pull request #20522 from open-webui/dev
0.7.1
2026-01-10 00:44:36 +04:00
Timothy Jaeryang Baek c4de5ea50c chore: bump 2026-01-10 00:40:37 +04:00
Timothy Jaeryang Baek 8646aebaab refac/fix: DATABASE_ENABLE_SESSION_SHARING env var 2026-01-10 00:16:04 +04:00
Tim Baek 6adde203cd Merge pull request #20394 from open-webui/dev
0.7
2026-01-09 22:51:34 +04:00
Timothy Jaeryang Baek 5990c51ab5 chore: format 2026-01-09 22:27:53 +04:00
Timothy Jaeryang Baek 3c986adeda enh: kb metadata search 2026-01-09 22:21:00 +04:00
Timothy Jaeryang Baek eff772562b chore: format 2026-01-09 21:19:27 +04:00
Timothy Jaeryang Baek 7bb11f9ef0 refac: wording 2026-01-09 21:05:37 +04:00
Timothy Jaeryang Baek 592a6213b9 doc: changelog 2026-01-09 21:00:30 +04:00
Timothy Jaeryang Baek db02d802a7 refac 2026-01-09 20:48:16 +04:00
Timothy Jaeryang Baek 7a7a0c423b chore: format 2026-01-09 20:44:31 +04:00
Timothy Jaeryang Baek 74c4af6e11 refac 2026-01-09 20:25:51 +04:00
Timothy Jaeryang Baek 9496e8f7b5 feat: model evaluation activity chart 2026-01-09 20:19:51 +04:00
Timothy Jaeryang Baek 636ba171a4 chore: format 2026-01-09 19:46:32 +04:00
Classic298 f4d99135e4 chore: CHANGELOG (#20261)
* 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
2026-01-09 19:44:32 +04:00
Timothy Jaeryang Baek a7b4b6e51a enh: WHISPER_MULTILINGUAL 2026-01-09 19:42:13 +04:00
Classic298 83dce80e30 Update translation.json (#20513) 2026-01-09 19:26:48 +04:00
Timothy Jaeryang Baek 869108a3e1 refac: tts split 2026-01-09 19:08:35 +04:00
Timothy Jaeryang Baek bb6188abf0 feat: per model tts voice 2026-01-09 19:05:09 +04:00
Timothy Jaeryang Baek 401c1949a0 refac 2026-01-09 18:51:38 +04:00
Timothy Jaeryang Baek 10838b3654 refac/fix: feedback leaderboard 2026-01-09 18:24:09 +04:00
Timothy Jaeryang Baek 3a57233dd4 chore: aiohttp 2026-01-09 18:10:27 +04:00
Tim Baek daccf0713e enh: file context model setting 2026-01-09 03:41:43 -05:00
Timothy Jaeryang Baek 1138929f4d feat: headless admin creation 2026-01-09 12:01:36 +04:00
Timothy Jaeryang Baek 9a9a824fe9 refac 2026-01-09 03:31:27 +04:00
Timothy Jaeryang Baek b2a1f71d92 refac: get feedback ids 2026-01-09 03:06:24 +04:00
Timothy Jaeryang Baek ffbd6ec7f2 refac 2026-01-09 03:03:25 +04:00
joaobackandTim Baek 15934d208e Update translation.json (pt-BR) (#20493)
Translations of the newly added items

Co-authored-by: Tim Baek <tim@openwebui.com>
2026-01-09 02:47:30 +04:00
Timothy Jaeryang Baek b377e5ff4c chore: format 2026-01-09 02:46:04 +04:00
Timothy Jaeryang Baek 924af3d908 chore: format 2026-01-09 02:45:50 +04:00
Timothy Jaeryang Baek 9e9616b670 fix: input quick note 2026-01-09 02:44:08 +04:00
Timothy Jaeryang Baek 00837d8f99 refac 2026-01-09 02:30:36 +04:00
Timothy Jaeryang Baek cd296fcf0d feat: channel webhooks 2026-01-09 02:30:15 +04:00
Timothy Jaeryang Baek 48bdb3f266 refac 2026-01-09 01:58:31 +04:00
Timothy Jaeryang Baek ef9cd0e0ad refac 2026-01-09 01:47:51 +04:00
Timothy Jaeryang Baek 8e7ebdbf63 refac 2026-01-09 01:25:40 +04:00
Classic298 9451b13dc6 feat: add ENABLE_USER_STATUS toggle for admin-controlled user status visibility (#20488)
* feat: add ENABLE_USER_STATUS toggle for admin-controlled user status visibility

feat: add ENABLE_USER_STATUS toggle for admin-controlled user status visibility

Add a new admin panel toggle (Admin > Settings > General) called "User Status" that allows administrators to globally enable or disable user status functionality.

When disabled:
- User status API endpoints return 403 Forbidden
- Status emoji, message, and "Update your status" button are hidden from the user menu

The setting:
- Defaults to True (enabled)
- Can be overridden via ENABLE_USER_STATUS environment variable
- Persists across restarts using PersistentConfig

Files modified:
- backend/open_webui/config.py - Added ENABLE_USER_STATUS PersistentConfig
- backend/open_webui/main.py - App state init and features dict
- backend/open_webui/routers/auths.py - AdminConfig model and endpoints
- backend/open_webui/routers/users.py - 403 guards on status endpoints
- src/lib/components/admin/Settings/General.svelte - Toggle UI
- src/lib/components/layout/Sidebar/UserMenu.svelte - Conditional status display

* Update UserMenu.svelte

feat: add ENABLE_USER_STATUS toggle for admin-controlled user status visibility

Add a new admin panel toggle (Admin > Settings > General) called "User Status" that allows administrators to globally enable or disable user status functionality.

When disabled:
- User status API endpoints return 403 Forbidden
- Active/Away indicator with blinking dot is hidden from the user menu
- Status emoji, message, and "Update your status" button are hidden from the user menu

The setting:
- Defaults to True (enabled)
- Can be overridden via ENABLE_USER_STATUS environment variable
- Persists across restarts using PersistentConfig

Files modified:
- backend/open_webui/config.py - Added ENABLE_USER_STATUS PersistentConfig
- backend/open_webui/main.py - App state init and features dict
- backend/open_webui/routers/auths.py - AdminConfig model and endpoints
- backend/open_webui/routers/users.py - 403 guards on status endpoints
- src/lib/components/admin/Settings/General.svelte - Toggle UI
- src/lib/components/layout/Sidebar/UserMenu.svelte - Conditional status display

* nuke the indicator

* fix
2026-01-09 00:55:57 +04:00
Timothy Jaeryang Baek 9223efaff0 fix: native function calling system prompt duplication 2026-01-08 23:08:47 +04:00
Shirasawa 7aee9c4783 feat: improve Chinese translation (#20487)
* feat: improve zh-CN translation

* i18n: improve zh-TW translation
2026-01-08 22:59:06 +04:00
Timothy Jaeryang Baek c1147578c0 feat: export kb to zip 2026-01-08 12:49:45 +04:00
Timothy Jaeryang Baek 9b06fdc8fe refac 2026-01-08 03:37:11 +04:00
Timothy Jaeryang BaekandClassic298 a9a979fb3d refac: files search
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2026-01-08 03:08:11 +04:00
G30 4b4743b497 feat: enforce permissions in backend (#20471)
* feat: enforce image generation permissions in backend

* feat: enforce web search permissions in backend

* feat: enforce audio (tts/stt) permissions in backend
2026-01-08 02:48:35 +04:00
Timothy Jaeryang Baek e7207c8f55 refac: styling 2026-01-08 02:43:19 +04:00
Classic298 ae56bca7c5 Update translation.json (#20472) 2026-01-08 02:29:18 +04:00
Timothy Jaeryang Baek 700349064d chore: format 2026-01-08 01:55:56 +04:00
Timothy Jaeryang Baek c417fdd94d refac 2026-01-08 01:38:40 +04:00
Timothy Jaeryang Baek b6cef30bfc refac 2026-01-08 01:24:31 +04:00
Timothy Jaeryang Baek aa084f804d refac 2026-01-08 00:53:21 +04:00
Timothy Jaeryang Baek e67891a374 refac 2026-01-08 00:42:29 +04:00
G30 b73d30b6df feat: implement global memories toggle and permissions (#20462) 2026-01-07 23:50:04 +04:00
Classic298 48f1b2d547 fix(channels): eliminate N+1 query in get_pinned_messages endpoint (#20459)
Replaced per-message user lookup with batch fetch using SQL IN clause.

Changes:
- Fetch all message user_ids in a single pass
- Use Users.get_users_by_user_ids() for batch lookup
- Build user mapping to avoid DB calls in loop
- Add early return for empty message lists

Performance: Reduces N+1 queries to 2 queries (messages + users)
2026-01-07 23:36:38 +04:00
Classic298 b8fb9e815b fix(channels): eliminate N+1 query in get_channel_messages endpoint (#20458)
Replaced per-message user lookup with batch fetch using SQL IN clause.

Changes:
- Fetch all message user_ids in a single pass
- Use Users.get_users_by_user_ids() for batch lookup
- Build user mapping to avoid DB calls in loop
- Add early return for empty message lists

Performance: Reduces N+1 queries to 2 queries (messages + users)
2026-01-07 23:36:12 +04:00
Classic298 74974334a5 fix: More n+1 channel perf fixes (#20460) 2026-01-07 23:34:19 +04:00
Timothy Jaeryang BaekandClassic298 084f0ef6a5 fix(notes): eliminate N+1 query in get_notes endpoint
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2026-01-07 23:32:56 +04:00
G30 0e1edc4c01 feat: add search bar to admin settings sidebar (#20434) 2026-01-07 23:27:25 +04:00
Tim Baek 0654df7bdb refac 2026-01-07 10:25:13 -05:00
Tim Baek 35d385e9cc refac 2026-01-07 10:21:05 -05:00
Tim BaekandJannik S. ab400e3eae enh: native tool citations
Co-Authored-By: Jannik S. <jannik@streidl.dev>
2026-01-07 10:14:45 -05:00
Tim Baek 961136413f refac 2026-01-07 09:46:07 -05:00
Tim Baek c8622adcb0 feat: builtin kb tools 2026-01-07 08:58:58 -05:00
Tim Baek 2789f6a24d enh: builtin tools 2026-01-07 07:00:32 -05:00
Tim Baek 60e916d6c0 enh: built-in tools toggle in model editor 2026-01-07 06:22:17 -05:00
Classic298 502813c6c8 chore: aggregation (#20430)
* Update EditGroupModal.svelte

* Update Permissions.svelte

* Create permissions.ts
2026-01-06 23:01:10 +04:00
Classic298 732d9b484d fix: resolve N+1 query pattern in users endpoint (#20427)
## Summary

Fixed N+1 query pattern in the `/api/v1/users` endpoint where groups were being fetched for each user individually.

### Problem

The `GET /api/v1/users` endpoint called `Groups.get_groups_by_member_id()` for each user, resulting in:
- 1 query for users
- N queries for groups (one per user)

### Solution

Added a new `Groups.get_groups_by_member_ids()` method that fetches groups for multiple users in a single query using SQL `IN` clause and `JOIN`.

### Changes

- **[groups.py](open_webui/models/groups.py)**: Added `get_groups_by_member_ids()` method
- **[users.py](open_webui/routers/users.py)**: Updated endpoint to use bulk method

### Result

- Before: 1 + N queries
- After: 2 queries total (1 for users, 1 for all groups)
2026-01-06 21:26:14 +04:00
72698a0465 i18n: Comprehensive improvements to Polish (pl-PL) translation (#20425)
* Updated pl-PL lang

* Updated pl-PL

* polish translation reverted

* updated polish translation

* updated polish translation

* Updated translation

---------

Co-authored-by: Tim Baek <tim@openwebui.com>
Co-authored-by: joaoback <156559121+joaoback@users.noreply.github.com>
2026-01-06 21:20:06 +04:00
Classic298 cc046695e6 feat: add interface user permission (#20424)
* feat: add interface user permission

Adds admin configurable user / group permissions for the interface settings section

* rename
2026-01-06 20:23:46 +04:00
Timothy Jaeryang Baek 17490381f5 refac 2026-01-06 19:11:56 +04:00
Jannik S. 87ebbbe2f4 fix: explicit casting client errors with some datatypes in websearch settings (#20422) 2026-01-06 18:45:11 +04:00
Timothy Jaeryang Baek 1148d1c927 fix/refac: images 2026-01-06 03:25:39 +04:00
Timothy Jaeryang Baek 927a765641 refac 2026-01-06 03:24:08 +04:00
Timothy Jaeryang Baek 1cfbb823ec chore: lock sync 2026-01-06 03:04:09 +04:00
Classic298 c87031e9a6 feat: Tools Atomic PR of #20243 (#20370)
* feat: Add read-only access support for Tools

- Backend: Add write_access field to ToolAccessResponse
- Backend: Update /tools/list to return tools with write_access
- Frontend: Display Read Only badge in Tools list
- Frontend: Disable inputs and save button when no write access
- Frontend: Add readOnly prop to CodeEditor component

* Update Tools.svelte

* fix: Return write_access from getToolById endpoint

fix: Return write_access from getToolById endpoint

- Use ToolAccessResponse instead of raw dict
- Remove inefficient getToolList call in edit page

* refactor: Rename write_access to disabled in ToolkitEditor

- Rename prop from write_access to disabled
- Invert logic where needed
- Update edit page to pass disabled instead of write_access

* rem

* Update +page.svelte

* fix

* Update ToolkitEditor.svelte

* Update CodeEditor.svelte

* Update ToolkitEditor.svelte
2026-01-06 03:00:48 +04:00
Timothy Jaeryang Baek 5921a19519 refac 2026-01-06 02:19:57 +04:00
Timothy Jaeryang Baek 2543252bb0 refac 2026-01-06 02:00:30 +04:00
Timothy Jaeryang Baek 619f260466 refac 2026-01-06 01:53:00 +04:00
Timothy Jaeryang Baek 119fc21257 refac 2026-01-06 01:42:49 +04:00
Timothy Jaeryang Baek 40c45ffe1f feat: text fragment url in citations 2026-01-06 00:49:32 +04:00
Timothy Jaeryang Baek 4cf901f519 refac 2026-01-06 00:05:52 +04:00
Timothy Jaeryang Baek de9cff6b4e refac 2026-01-06 00:01:07 +04:00
Classic298 713a65ee31 fix: inject full context knowledge into system message for KV prefix caching (#20317)
* Update middleware.py

* Update middleware.py

* env var

* address

* upd
2026-01-05 23:58:53 +04:00
Classic298 cd5a38a694 feat: Models Atomic PR of #20243 (#20369)
* feat: Add read-only access support for Models

- Backend: Add write_access field to ModelAccessResponse
- Backend: Update /models/list to return ModelAccessListResponse
- Frontend: Display Read Only badge in Models list
- Frontend: Disable inputs and save button when no write access
- Frontend: Hide action buttons for read-only models

* fix: Handle ModelAccessListResponse format in getModels API

- Backend returns {items, total} instead of {data}
- Update getModels API to handle both formats for backward compatibility

* fix: Show read-only shared models in workspace list

- Backend: Change search_models permission from 'write' to 'read' to include shared models
- Backend: Keep user_id filter to only show owned/shared models (not all public)
- Frontend: Handle ModelAccessListResponse format in getModels API

* fix: Align Read Only badge inline with model name

* fix: Correct badge placement and fix syntax error

* fix: Resolve badge truncation in Models list

- Add w-full to flex container for proper spacing
- Wrap Badge in div to prevent truncation
- Match Knowledge.svelte badge pattern

* fix: Align Read Only badge with Knowledge.svelte pattern

- Match Knowledge.svelte structure for badge placement
- Actions only show when write_access or admin
- Remove w-full from container to prevent right-overflow

* fix: Return write_access from getModelById endpoint

fix: Return write_access from getModelById endpoint

- Use ModelAccessResponse instead of raw dict
- Remove inefficient getModels call in edit page

* revert

* fix

* fix

* fix
2026-01-05 23:37:41 +04:00
Timothy Jaeryang Baek 8ef0f7743b refac 2026-01-05 23:13:05 +04:00
Timothy Jaeryang Baek 8d568cc8c5 refac 2026-01-05 22:41:13 +04:00
Classic298 f5455d48c4 Chore: dup bump for NPM and PIP (#20386)
* Update pyproject.toml

* Update aiohttp version to 3.13.3

* Update aiohttp version to 3.13.3

* Update pyproject.toml

* Update requirements.txt

* Update package.json
2026-01-05 22:34:25 +04:00
Timothy Jaeryang Baek ab7b97e21f refac 2026-01-05 22:12:49 +04:00
Timothy Jaeryang Baek b1d30673b6 refac 2026-01-05 21:54:33 +04:00
Timothy Jaeryang Baek bb821ab654 fix: revert model profile image cache 2026-01-05 21:49:54 +04:00
460ab07f39 chore: y-protocols dep
Co-Authored-By: G30 <50341825+silentoplayz@users.noreply.github.com>
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2026-01-05 19:26:27 +04:00
Timothy Jaeryang Baek 1d08376860 refac 2026-01-05 18:55:44 +04:00
Timothy Jaeryang Baek d3ab9f4b96 fix: failed hash in files 2026-01-05 18:21:00 +04:00
G30 9c5773d0af feat: add chat:message:favorite event for action functions (#20375)
* feat: add chat:message:favorite event for action functions

* Update Chat.svelte

* Update Chat.svelte
2026-01-05 18:15:00 +04:00
Timothy Jaeryang Baek 00af37bb4e enh: model gif/webp support 2026-01-05 17:56:41 +04:00
Timothy Jaeryang Baek a3270648d8 refac 2026-01-05 17:50:40 +04:00
Timothy Jaeryang Baek 646835d767 feat: builtin native tools 2026-01-05 17:45:39 +04:00
Timothy Jaeryang Baek e27fb3e291 refac 2026-01-05 17:44:44 +04:00
Timothy Jaeryang Baek 3f577c0c3f refac: notes organization issue 2026-01-05 17:34:29 +04:00
Timothy Jaeryang Baek ca514cd3ed refac: group share to settings 2026-01-05 05:32:56 +04:00
Classic298 6b5643c786 feat: Add read-only access support for Knowledge Bases (#20371)
- Backend: Add BYPASS_ADMIN_ACCESS_CONTROL check to write_access calculation
- Frontend: Knowledge already has Read Only badge and disabled inputs
2026-01-05 04:45:48 +04:00
Timothy Jaeryang Baek 5c1d52231a feat: native function calling for built-in tools 2026-01-05 04:45:17 +04:00
Classic298 1f059fe730 feat: Prompts Atomic PR of #20243 (#20368)
* feat: Add read-only access support for Prompts

- Backend: Add write_access field to PromptAccessResponse
- Backend: Update /prompts/list to return prompts with write_access
- Frontend: Display Read Only badge in Prompts list
- Frontend: Disable inputs and save button when no write access

* feat: Add read-only visual indicators for Prompts workspace

* fix: Return write_access from getPromptByCommand endpoint

- Backend returns write_access directly in response
- Frontend extracts write_access from getPromptByCommand response
- Remove inefficient getPromptList call in edit page

* fix: Align Read Only badge to right in Prompts.svelte

- Title and command stay on left
- Badge pushed to right by justify-between

* fix: Use PromptAccessResponse in get_prompt_by_command endpoint

fix: Use PromptAccessResponse in get_prompt_by_command endpoint

- Return PromptAccessResponse Pydantic model instead of raw dict
- Properly type the response with response_model
2026-01-05 04:36:13 +04:00
Timothy Jaeryang Baek b55a46ae99 refac 2026-01-05 03:46:46 +04:00
Timothy Jaeryang Baek 2bb13d5dbc refac: get_db_context 2026-01-05 03:46:40 +04:00
Classic298 614cb56420 feat: Add configurable DDGS backend selection with UI support (#20366)
* init

* Update WebSearch.svelte

* reorder
2026-01-05 03:05:56 +04:00
Timothy Jaeryang Baek e754940c03 refac 2026-01-05 03:05:10 +04:00
Timothy Jaeryang Baek dc2c2f2295 refac 2026-01-03 19:48:37 +04:00
Timothy Jaeryang BaekandClassic298 c324359580 feat: chunk min size target for md header splitter
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2026-01-03 19:47:29 +04:00
Timothy Jaeryang Baek e55bf2c2ac refac 2026-01-03 18:47:12 +04:00
Timothy Jaeryang Baek bd07ef87ab refac 2026-01-03 18:43:12 +04:00
Classic298 1c82506bfd fix: Missing field for entering the Mineru Cloud API key (#20328)
* Update Documents.svelte

* Update Documents.svelte
2026-01-03 18:31:59 +04:00
Classic298 a2f8e41fbc chore: dep bump (#20315)
* dep bump

* update
2026-01-03 18:14:25 +04:00
G30 8712446cc1 fix: resolve undefined knowledge base name in file reference tooltip (#20333) 2026-01-03 18:13:19 +04:00
G30 bf42535d12 feat: add support for passing 'steps' in image generation requests (#20337) 2026-01-03 18:11:13 +04:00
G30 cfecca8eda fix: handle undefined file.url in UserMessage to prevent TypeError (#20343)
Add optional chaining to file.url.startsWith() calls to safely handle cases where file.url is undefined, preventing 'can't access property startsWith' TypeError in user messages with file attachments.
2026-01-03 18:10:03 +04:00
Timothy Jaeryang Baek ccd3295a5d refac 2026-01-02 16:56:13 +04:00
Timothy Jaeryang Baek f0829ba6e6 refac: deprecate generate image action button 2026-01-01 16:09:29 +04:00
Timothy Jaeryang Baek e4a5b06ca6 enh: embedding_batch_size for local embedding engine 2026-01-01 16:06:42 +04:00
Timothy Jaeryang Baek d3ee3fd23e refac 2026-01-01 14:10:11 +04:00
Timothy Jaeryang Baekandjamie-dit 89565c58c6 refac/fix: oauth discovery urls
Co-Authored-By: jamie-dit <80016430+jamie-dit@users.noreply.github.com>
2026-01-01 14:01:18 +04:00
Timothy Jaeryang Baek f981843852 refac 2026-01-01 13:45:37 +04:00
Timothy Jaeryang Baek c144122f60 refac/fix: reply to message recursion issue 2026-01-01 03:10:09 +04:00
Timothy Jaeryang Baek b67796465e refac 2026-01-01 03:03:10 +04:00
Timothy Jaeryang Baek a1036e544d enh: folder_max_file_count 2026-01-01 02:51:35 +04:00
Timothy Jaeryang Baek 4eadf84e1f refac 2026-01-01 02:23:42 +04:00
Timothy Jaeryang Baek f7f8a263b9 feat: JINA_API_BASE_URL 2026-01-01 02:17:47 +04:00
Jan Kessler 6c7f966f2a properly handle async-generator Redis methods in SentinelRedisProx to fix changed YDocManager's remove_user_from_all_documents (#20145) 2025-12-31 17:15:24 -05:00
Timothy Jaeryang Baek 89ad1c68d1 enh: FIRECRAWL_TIMEOUT 2026-01-01 02:07:22 +04:00
Timothy Jaeryang Baekandlif b619a157bc refac/fix: rtl support
Co-Authored-By: lif <19658300+majiayu000@users.noreply.github.com>
2026-01-01 01:58:53 +04:00
Timothy Jaeryang Baek fdae5644e3 refac 2026-01-01 01:51:37 +04:00
Timothy Jaeryang BaekandShamray Alexander bf2b296239 fix: oauth server_metadata_url issue
Co-Authored-By: Shamray Alexander <843002+imsamurai@users.noreply.github.com>
2026-01-01 01:37:38 +04:00
Timothy Jaeryang Baek dfc5dad631 enh: REQUESTS_VERIFY 2026-01-01 01:27:07 +04:00
Classic298 431632d530 fix: normalize local CrossEncoder reranking scores for relevance threshold (#20228)
* Update utils.py

* Update retrieval.py

* Update utils.py

* Update retrieval.py

* add env var

* rename to SENTENCE_TRANSFORMERS_CROSS_ENCODER_SIGMOID_ACTIVATION_FUNCTION
2025-12-31 15:48:31 -05:00
G30 4f918e7aa4 fix: resolve duplicate key and null message errors in chat (#20268)
* fix: resolve duplicate key and null message errors in chat

* fix: resolve duplicate key and null message errors in chat

* chore: visitedIds -> visitedMessageIds
2025-12-31 15:44:02 -05:00
G30 8b3a4c1e2f fix: rename 'Feedbacks' to 'Feedback' in UI and URL (#20296) 2025-12-31 15:43:29 -05:00
Timothy Jaeryang Baek 71ca25c8ac refac 2025-12-31 20:23:32 +04:00
Classic298 6c203fc7df fix: prevent getChatList crash on null API response (#20269)
Add null check before calling .map() on the API response in getChatList(). When the fetch fails silently or returns null, the function now gracefully returns an empty array instead of crashing with 'Cannot read property map of null'.
2025-12-31 17:41:27 +04:00
Classic298 0bd295b10b fix: prevent getOpenAIUrls and getOpenAIKeys crash on null response (#20272)
Use optional chaining and nullish coalescing when accessing res.OPENAI_API_BASE_URLS and res.OPENAI_API_KEYS. Returns empty array instead of crashing with 'Cannot read property of null'.
2025-12-31 17:40:56 +04:00
G30 252a983091 fix: cross-chat data corruption and tiptap editor access errors (#20266)
* fix: prevent cross-chat corruption and editor access errors

* fix: safety check in Messages.svelte and cleanup in RichTextInput
2025-12-31 17:40:35 +04:00
_00_ 5a556b00c3 UPD: i18n: es-ES language v.0.6.44 (#20265)
### i18n: Update es-ES language v.0.6.44

Added new strings translation.
2025-12-31 17:39:46 +04:00
Classic298 9195d7aeb0 fix: prevent delete_chat_by_id crash when chat is None (#20270)
Add null check after Chats.get_chat_by_id() in both admin and user code paths. When chat doesn't exist, now raises HTTP 404 instead of crashing with AttributeError when accessing chat.meta.
2025-12-31 17:39:33 +04:00
Classic298 f4670b3add fix: prevent get_user_ids_from_room crash on missing session (#20271)
Add null check in list comprehension before accessing session['id']. When a session_id exists in the room but has been removed from SESSION_POOL, the function now skips it instead of crashing with TypeError.
2025-12-31 17:39:22 +04:00
Classic298 048692c068 fix: prevent insert_shared_chat_by_chat_id crash when chat is None (#20273)
Add null check after db.get(Chat, chat_id) before accessing chat.share_id. Returns None instead of crashing with AttributeError when chat doesn't exist.
2025-12-31 17:38:57 +04:00
Classic298 b91e8b73ab fix: properly raise exceptions instead of returning them in chat.py (#20276)
Change 'return Exception(...)' to 'raise Exception(...)' in chat_completed() and chat_action() functions. Returning an exception object instead of raising it causes errors to be silently swallowed, breaking error propagation.
2025-12-31 17:38:47 +04:00
Classic298 201c38a08a fix: prevent delete_entries_from_collection crash when file is None (#20274)
Add null check after Files.get_file_by_id() before accessing file.hash. Raises HTTP 404 instead of crashing with AttributeError when file doesn't exist.
2025-12-31 02:31:26 -05:00
Classic298 46f867cda6 fix: prevent save_docs_to_vector_db crash on empty result.ids (#20275)
Add check that result.ids exists and has length > 0 before accessing result.ids[0]. Prevents IndexError when query returns empty results.
2025-12-31 02:31:05 -05:00
Classic298 6d087202ad fix: prevent invalidate_token crash when decode_token returns None (#20277)
Add null check after decode_token() before calling decoded.get(). Invalid/expired tokens now gracefully exit instead of crashing with AttributeError.
2025-12-31 02:30:45 -05:00
Classic298 07cbc52c9a fix: prevent update_user_settings_by_id crash when user is None (#20278)
Get user first and check for None before accessing .settings attribute. Returns None gracefully instead of crashing with AttributeError.
2025-12-31 02:30:31 -05:00
Classic298 2c0ebbcced fix: prevent null crashes in users and notes routers (#20279)
Add null checks for ui settings in update_user_settings_by_session_user() and for user in get_notes(). Prevents AttributeError when ui dict is None or when note's user has been deleted.
2025-12-31 02:30:14 -05:00
Classic298 a2e9263af0 fix: add explicit HTTPException for access control failures (#20280)
Fix implicit None returns in get_model_by_id, get_knowledge_by_id, get_tools_by_id, and get_prompt_by_command. Now properly returns 401 for access denied and 404 for not found instead of silently returning None.
2025-12-31 02:28:59 -05:00
Tim Baek 6351077958 refac 2025-12-31 02:27:55 -05:00
Classic298 2acee80829 fix: normalize Windows CRLF line endings in clipboard text (#20283)
On Windows, clipboard text uses CRLF (\r\n) line endings which can cause newlines to be lost or displayed incorrectly when pasted via the {{CLIPBOARD}} variable.

Fixed by converting CRLF to LF before inserting clipboard text.

Fixes #19370
2025-12-31 02:22:13 -05:00
Classic298 450421b189 fix: correct pending_user_overlay type definition in Config (#20285)
The Config type incorrectly defined 'pending_user_overlay_description'
but the backend returns 'pending_user_overlay_content' and the
AccountPending.svelte component correctly accesses that property.

This fixes the type mismatch to align with the actual backend response.

Fixes #20284
2025-12-31 02:21:59 -05:00
Timothy Jaeryang Baek c2c389f722 refac 2025-12-30 20:07:15 +04:00
G30 b997d00947 fix: prevent crash when regenerating message with missing parent (#20264) 2025-12-30 20:05:55 +04:00
Timothy Jaeryang Baek fe84afd09a enh: delta annotations support 2025-12-30 20:05:31 +04:00
Timothy Jaeryang Baek 08bf4670ec refac 2025-12-30 19:38:45 +04:00
Timothy Jaeryang Baek 18a33a079b refac 2025-12-30 19:33:30 +04:00
Timothy Jaeryang Baek d3a682759f enh: ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER 2025-12-30 19:31:59 +04:00
Timothy Jaeryang Baek 61e25dc2dc refac 2025-12-30 18:28:57 +04:00
Classic298 697e94e935 fix: prevent crash when invalid OpenAPI spec is loaded for tool servers (#20257)
* enh

* fix
2025-12-30 18:02:56 +04:00
Shirasawa 935808f5ea feat: Dynamically load mammoth to speed up page loading (#20202) 2025-12-30 17:51:50 +04:00
Classic298 464846d4a3 fix: retry Brave Search on HTTP 429 rate limit with 1s delay (#20255)
* Update brave.py

* Update brave.py
2025-12-30 17:37:44 +04:00
Classic298 95ce70c4fd Update Chat.svelte (#20256) 2025-12-30 17:31:13 +04:00
Classic298 214dd50ce9 feat: make changelog modal X button persist dismissal like "Okay, Let's Go!" button (#20258)
* Update ChangelogModal.svelte

* function
2025-12-30 17:29:38 +04:00
Classic298 4750a7cab0 feat: expose ADMIN_EMAIL setting in Admin Panel (#20260) 2025-12-30 17:28:25 +04:00
Classic298 3b6601631b fix: Update ResponseMessage.svelte (#20210)
* Update ResponseMessage.svelte

* Update ResponseMessage.svelte
2025-12-30 14:06:23 +04:00
Classic298 d5ccd95293 fix: Update CitationModal.svelte (#20209)
* Update CitationModal.svelte

* Update CitationModal.svelte
2025-12-30 14:06:11 +04:00
Classic298 b3371033be chore(deps): update and synchronize backend dependencies (#20225)
* chore(deps): update and synchronize backend dependencies

- Updated dependencies in requirements files and pyproject.toml to latest versions.
- Preserved pinned versions for av, pinecone, and pyarrow.
- Added missing dependencies to pyproject.toml to match requirements.txt.
- Ensured all dependency versions are synchronized across files.

* Update pyproject.toml
2025-12-30 14:05:56 +04:00
Timothy Jaeryang BaekandPaul aaea9a5956 refac/fix: comfyui filter output node type
Co-Authored-By: Paul <239564541+mirrordna-reflection-protocol@users.noreply.github.com>
2025-12-30 12:24:48 +04:00
Classic298 c02451178c init (#20212) 2025-12-30 12:15:31 +04:00
Timothy Jaeryang Baek fe3047d53c refac 2025-12-29 02:05:55 +04:00
Timothy Jaeryang Baek 5649a668fa refac 2025-12-29 01:42:13 +04:00
Timothy Jaeryang Baek 2453b75ff0 refac 2025-12-29 01:31:27 +04:00
Timothy Jaeryang Baek 5d1459df16 refac 2025-12-29 01:20:04 +04:00
Timothy Jaeryang Baek 88dbc14abc refac 2025-12-29 00:55:53 +04:00
Classic298 ca176c5c03 Update KnowledgeSelector.svelte (#20213) 2025-12-29 00:48:51 +04:00
Shirasawa 87e8d39a90 feat: Dynamically load xlsx to speed up page loading (#20203) 2025-12-29 00:47:37 +04:00
G30 9a9b5ef699 feat: add sidebar toggle to workspace and admin models pages (#20176) 2025-12-29 00:45:45 +04:00
Shirasawa 3994d88c90 chore: remove vega import to reduce build size (#20200) 2025-12-29 00:44:44 +04:00
Shirasawa cc602abcb2 feat: Dynamically load yaml to speed up page loading (#20204) 2025-12-29 00:44:22 +04:00
Timothy Jaeryang Baek 267cc14d2d chore: rm google-generativeai 2025-12-29 00:42:49 +04:00
Timothy Jaeryang Baek 26af1f92e2 enh: WHISPER_COMPUTE_TYPE env var 2025-12-29 00:41:47 +04:00
Timothy Jaeryang Baek 475dd91ed7 refac 2025-12-29 00:39:43 +04:00
Timothy Jaeryang Baek 9405628e46 chore: dep-min 2025-12-29 00:24:49 +04:00
Shirasawa 93db0a4e57 I18n: improve chinese translation (#20197)
* i18n: improve zh-CN translation

* i18n: improve zh-TW translation
2025-12-29 00:23:28 +04:00
Timothy Jaeryang Baek b1d0f00d8c refac/enh: db session sharing 2025-12-29 00:21:18 +04:00
Timothy Jaeryang Baek 6dd0f99b90 rm: outdated tests 2025-12-28 23:35:09 +04:00
Timothy Jaeryang Baek eb4b085b86 refac 2025-12-28 23:23:56 +04:00
Timothy Jaeryang Baek c79fb7db74 refac 2025-12-28 22:40:28 +04:00
Timothy Jaeryang Baek 145c7516f2 refac 2025-12-28 22:26:35 +04:00
Timothy Jaeryang Baek 2041ab483e refac/enh: db session sharing 2025-12-28 22:00:44 +04:00
Timothy Jaeryang Baek d4de26bd05 refac 2025-12-27 22:31:18 +04:00
Timothy Jaeryang Baek 7b6f8b49e0 refac 2025-12-27 22:16:24 +04:00
Timothy Jaeryang Baek 37b15604de refac 2025-12-27 22:16:10 +04:00
Timothy Jaeryang Baek a9609585af refac 2025-12-27 01:41:06 +04:00
Timothy Jaeryang Baek ac0ae2ae20 refac 2025-12-27 01:06:21 +04:00
Timothy Jaeryang Baek 3b0d25ad2b refac 2025-12-27 00:40:08 +04:00
Timothy Jaeryang Baek adf463059d refac 2025-12-27 00:02:03 +04:00
Timothy Jaeryang Baek 5f20102b33 refac 2025-12-26 23:55:00 +04:00
Timothy Jaeryang Baek d08f9ed556 refac 2025-12-26 19:09:05 +04:00
Dechao Sun 25db8225f8 openWebUI supports openGauss vector store (#20179) 2025-12-26 18:32:05 +04:00
Timothy Jaeryang Baek 9c2f5148d9 refac 2025-12-26 18:30:50 +04:00
landerrosette d29271184e fix: extract correct MIME type from base64 profile images (#20171) 2025-12-26 18:27:18 +04:00
Timothy Jaeryang Baek f5c07e317e refac 2025-12-26 16:59:14 +04:00
Timothy Jaeryang Baek 39a2b9789c refac 2025-12-26 16:16:10 +04:00
Timothy Jaeryang Baek 51dd488417 refac 2025-12-26 16:09:51 +04:00
Timothy Jaeryang Baek 6c14ba1fb4 refac 2025-12-26 15:57:19 +04:00
Timothy Jaeryang Baek 44faf00fac refac 2025-12-26 15:46:32 +04:00
Timothy Jaeryang Baek 4a17a7c20a refac 2025-12-26 15:44:41 +04:00
Timothy Jaeryang Baek 459cfde1f8 refac 2025-12-26 15:36:56 +04:00
Timothy Jaeryang Baek 85bbed3ec5 enh: sync stats 2025-12-26 15:13:50 +04:00
Timothy Jaeryang Baek 9c61e95ecb refac 2025-12-26 14:05:16 +04:00
Tim Baek df106099a1 refac 2025-12-25 18:32:13 -05:00
Tim Baek 4be99174be refac 2025-12-25 18:11:17 -05:00
Timothy Jaeryang Baek c0ec04935b refac: citation 2025-12-26 02:05:03 +04:00
joaoback 2e9e46a9f1 Update translation.json (pt-BR) (#20131)
Correcting a translation that had an incomplete variable.
2025-12-23 11:28:53 +04:00
Timothy Jaeryang Baek 5995771583 fix: channel model mention 2025-12-23 10:42:33 +04:00
Timothy Jaeryang Baek 758325a203 infra: dockerfile base image 2025-12-23 10:15:05 +04:00
Timothy Jaeryang Baek 9824f0e333 enh: ENABLE_DB_MIGRATIONS 2025-12-23 10:14:54 +04:00
Timothy Jaeryang Baek 117b1bfa65 refac 2025-12-22 19:06:15 +04:00
Tim Baek e9706cb1e5 Merge pull request #20104 from ShirasawaSama/i18n/improve-chinese-translation
i18n: improve Chinese translation
2025-12-22 19:04:56 +04:00
Timothy Jaeryang Baek 769110f290 refac: styling 2025-12-22 17:21:31 +04:00
Timothy Jaeryang Baek d053600096 refac: styling 2025-12-22 17:20:19 +04:00
Shirasawa 42b6cf32bc i18n: improve zh-TW translation 2025-12-22 18:39:38 +08:00
Shirasawa 8f37a1b721 i18n: improve zh-CN translation 2025-12-22 18:38:18 +08:00
Tim Baekandjoaoback a7271532f8 0.6.43 (#20093)
* refac

* fix: python pip install dep issue

* Merge pull request #20085 from joaoback/patch-19

Update translation.json (pt-BR)

* fix/refac: stt default content type

* fix/refac: temp chat image handling

* fix/refac: image action button

* chore: bump

---------

Co-authored-by: joaoback <156559121+joaoback@users.noreply.github.com>
2025-12-22 01:03:34 -05:00
Timothy Jaeryang Baek 5088583721 chore: bump 2025-12-22 10:02:57 +04:00
Timothy Jaeryang Baek 0a78323406 fix/refac: image action button 2025-12-22 09:56:44 +04:00
Timothy Jaeryang Baek 423983f5a7 fix/refac: temp chat image handling 2025-12-22 09:48:53 +04:00
Timothy Jaeryang Baek 4ab917c74b fix/refac: stt default content type 2025-12-22 09:45:55 +04:00
joaoback 7f0ca8c4b7 Merge pull request #20085 from joaoback/patch-19
Update translation.json (pt-BR)
2025-12-22 00:43:25 -05:00
Timothy Jaeryang Baek 5c5f87a63c fix: python pip install dep issue 2025-12-22 09:42:50 +04:00
Timothy Jaeryang Baek def8a0027d refac 2025-12-22 09:41:12 +04:00
Tim Baek d95f533214 Merge pull request #19729 from open-webui/dev
0.6.42
2025-12-21 16:08:58 -05:00
Timothy Jaeryang Baek de97f797ce refac 2025-12-22 01:05:54 +04:00
Timothy Jaeryang Baek f0a04427e2 refac 2025-12-22 01:02:50 +04:00
Timothy Jaeryang Baek 088f62f1e1 refac: hf github action 2025-12-22 01:00:52 +04:00
Timothy Jaeryang Baek cfa8b3a396 chore: format 2025-12-22 01:00:20 +04:00
Classic298 a8bb000616 chore: CHANGELOG: 0.6.42 (#19827)
* Update CHANGELOG for version 0.6.42

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG for version 0.6.42

Updated release date for version 0.6.42 and added detailed notes on new features.

* 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
2025-12-21 15:57:32 -05:00
Timothy Jaeryang Baek 837f7022d5 doc 2025-12-22 00:56:53 +04:00
Timothy Jaeryang Baek 67fd2ec759 doc: banner 2025-12-22 00:53:49 +04:00
Timothy Jaeryang Baek baf0f51827 doc: demo image 2025-12-22 00:46:41 +04:00
Classic298 f7d5750210 Update translation.json (#20083) 2025-12-21 15:42:47 -05:00
Timothy Jaeryang Baek 446cc0ac60 refac 2025-12-22 00:39:05 +04:00
Timothy Jaeryang Baek 01e88c6ac2 chore: format 2025-12-21 23:34:08 +04:00
Timothy Jaeryang Baek 45e3237756 fix/refac: shared chat files behaviour 2025-12-21 23:29:54 +04:00
Timothy Jaeryang Baek bd7f1869ce refac 2025-12-21 23:22:14 +04:00
Timothy Jaeryang Baek f1bf4f20c5 feat: chat_file table 2025-12-21 23:17:53 +04:00
Timothy Jaeryang Baek a3458f492c fix/refac: note editor file upload behaviour 2025-12-21 21:28:43 +04:00
Timothy Jaeryang Baek 96ec66a89c refac 2025-12-21 20:49:17 +04:00
Timothy Jaeryang Baek a1155dfeb7 refac 2025-12-21 20:48:37 +04:00
Timothy Jaeryang Baek fa924946b0 refac 2025-12-21 20:37:51 +04:00
Timothy Jaeryang BaekandClassic298 b35aeb8f46 feat: custom model base model fallback
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2025-12-21 20:22:37 +04:00
Classic298 60c93b4ccc chore: dep bump (#20077)
* Update pyproject.toml

* Update requirements-min.txt

* Update requirements.txt
2025-12-21 10:52:06 -05:00
Classic298 ef43e81f9a fix: MCP OAuth 2.1 token exchange and multi-node propagation (#20076)
* sequential

* zero default

* fix

* fix: preserve absolute paths in sqlite+sqlcipher URLs

Previously, the connection logic incorrectly stripped the leading slash
from `sqlite+sqlcipher` paths, forcibly converting absolute paths
(e.g., `sqlite+sqlcipher:////app/data.db`) into relative paths
(which became `app/data.db`). This caused database initialization failures
when using absolute paths, such as with Docker volume mounts.
This change removes the slash-stripping logic, ensuring that absolute
path conventions (starting with `/`) are respected while maintaining
support for relative paths (which do not start with `/`).

* fix: MCP OAuth 2.1 token exchange and multi-node propagation

Fix two MCP OAuth 2.1 bugs affecting tool server authentication:

1. Token exchange failing with duplicate credentials (#19823)
   - Removed explicit client_id/client_secret passing in handle_callback()
   - Authlib already has credentials configured during add_client(),
     passing them again caused concatenation (e.g., "ID1,ID1") and 401 errors
   - Added token validation to detect missing access_token and provide
     clear error messages instead of cryptic database constraint errors

2. OAuth clients not propagating across multi-node setups (#19901)
   - Updated get_client() and get_client_info() to auto-lazy-load
     OAuth clients from the Redis-synced TOOL_SERVER_CONNECTIONS config
   - Clients are now instantiated on-demand on any node that needs them

Fixes #19823, #19901

* Update db.py

* Update wrappers.py
2025-12-21 10:51:52 -05:00
Timothy Jaeryang Baek f826d3ed75 refac/enh: sidebar section expand behaviour 2025-12-21 18:40:40 +04:00
Classic298 b3904b6ecb fix: Fix handling of absolute paths for SQLCipher databases (#20074)
* sequential

* zero default

* fix

* fix: preserve absolute paths in sqlite+sqlcipher URLs

Previously, the connection logic incorrectly stripped the leading slash
from `sqlite+sqlcipher` paths, forcibly converting absolute paths
(e.g., `sqlite+sqlcipher:////app/data.db`) into relative paths
(which became `app/data.db`). This caused database initialization failures
when using absolute paths, such as with Docker volume mounts.
This change removes the slash-stripping logic, ensuring that absolute
path conventions (starting with `/`) are respected while maintaining
support for relative paths (which do not start with `/`).
2025-12-21 09:18:20 -05:00
Timothy Jaeryang Baek c96549eaa7 refac 2025-12-21 18:08:36 +04:00
Timothy Jaeryang Baek 7746e9f4b8 refac 2025-12-21 17:04:39 +04:00
Classic298 4fd790f7dd feat: Apply WEB_SEARCH_CONCURRENT_REQUESTS to all search engines using semaphore (#20070)
* sequential

* zero default

* fix
2025-12-21 07:18:00 -05:00
Timothy Jaeryang Baek ae203d8952 refac 2025-12-21 16:15:28 +04:00
Classic298 48ccb1e170 fix: consolidate psql cleanup logic and fix web add with cleanup (#20072)
* sequential

* consolidate logic and fix for web add

* Update WebSearch.svelte

* Update retrieval.py

* Update retrieval.py

* Update WebSearch.svelte
2025-12-21 07:14:29 -05:00
Timothy Jaeryang Baek 5077676d33 refac 2025-12-21 16:10:51 +04:00
Timothy Jaeryang BaekandClassic298 0dd2cfe1f2 enh: models endpoint optimization
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>

#20010
2025-12-21 15:43:02 +04:00
Timothy Jaeryang Baek 53c1ca64b7 enh: feedback ids endpoint 2025-12-21 15:33:06 +04:00
Timothy Jaeryang Baek 870e29e373 fix: multimodel messages 2025-12-21 15:27:33 +04:00
Timothy Jaeryang Baek 7e4848e10b refac: usage in model editor 2025-12-21 14:42:32 +04:00
Timothy Jaeryang Baek 6993b0b40b enh: temp chat docx file support 2025-12-21 14:37:55 +04:00
Timothy Jaeryang Baek 360289db21 refac: file item modal 2025-12-21 14:24:53 +04:00
G30 0eeda79048 fix: user group pagination reset and model image caching (#19959)
- Changed default sort order in 'Edit Group' modal to 'created_at' (newest first).
  Previously it sorted by group membership, causing toggled users to invisible jump
  to the top of the list (often changing pages). using a stable sort fixes this UX issue.
- Removed unnecessary `page = 1` reset in toggleMember function so admins
  don't lose their place when selecting multiple users.

Bug 2 - Model Image Display Bug:
- Added `Cache-Control: no-cache, must-revalidate` headers to the model profile
  image endpoint. This ensures that when a model avatar is updated, the browser
  fetches the new image instead of serving a stale cached version (favicon).

Fixes #19885
2025-12-21 05:04:45 -05:00
G30 8eddff83cb fix(utils): add safety check for attributes.result in processDetails (#19923) 2025-12-21 04:59:21 -05:00
Timothy Jaeryang Baek 28b2fcab0c refac 2025-12-21 13:58:49 +04:00
0b0962a2ae fix: Show DefaultFiltersSelector for global toggleable filters (#123) (#20066)
Co-authored-by: Tim Baek <tim@openwebui.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-21 04:50:36 -05:00
Timothy Jaeryang Baek 90c0c5da00 refac: PASSWORD_VALIDATION_REGEX_PATTERN 2025-12-21 13:49:54 +04:00
Timothy Jaeryang Baek fe653a1336 refac 2025-12-20 18:12:03 +04:00
Timothy Jaeryang Baek 4c2e5c93e9 refac: gpt-image support 2025-12-20 17:56:46 +04:00
Timothy Jaeryang Baek d4bd938a77 refac: ollama web 2025-12-20 17:54:13 +04:00
okamototk 37085ed42b chore: update langchain 1.2.0 (#19991)
* chore: update langchain 1.2.0

* chore: format
2025-12-20 08:50:44 -05:00
2e7c7d635d fix: prevent ExternalReranker from blocking event loop during RAG queries (#20049)
* fix: prevent ExternalReranker from blocking event loop during RAG queries (#120)

Co-authored-by: Tim Baek <tim@openwebui.com>
Co-authored-by: Claude <noreply@anthropic.com>
Fixes #19900

* Merge pull request open-webui#19030 from open-webui/dev (#122)

Co-authored-by: Tim Baek <tim@openwebui.com>
Co-authored-by: Claude <noreply@anthropic.com>
Fixes #19900

---------

Co-authored-by: Tim Baek <tim@openwebui.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-20 08:43:40 -05:00
Timothy Jaeryang Baek b0d8372d31 refac 2025-12-20 17:41:57 +04:00
Timothy Jaeryang Baek afaa404fe4 enh: mineru api timeout 2025-12-20 17:39:33 +04:00
G30 8890555dab Fix: Make meta optional in FileMetadataResponse to prevent batch add errors (#20022) 2025-12-20 08:25:03 -05:00
Timothy Jaeryang Baekandzengxy b5bd8704fe fix: bocha filter list
Co-Authored-By: zengxy <11961641+zengxy@users.noreply.github.com>
2025-12-20 17:24:24 +04:00
823b9a6dd9 chore/perf: Remove old SRC level log env vars with no impact (#20045)
* Update openai.py

* Update env.py

* Merge pull request open-webui#19030 from open-webui/dev (#119)

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

---------

Co-authored-by: Tim Baek <tim@openwebui.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-20 08:16:14 -05:00
G30 d65116282c fix: enforce global ENABLE_CHANNELS check on all channel endpoints (#19957)
* fix: enforce global ENABLE_CHANNELS check on all channel endpoints

When channels are disabled globally (ENABLE_CHANNELS=false), users with
channel permissions could still fetch channels via API endpoints. This
fix adds a get_enabled_channels dependency to all 22 channel endpoints
that returns 403 Forbidden when channels are globally disabled.

Fixes #19914

* refac

* refac
2025-12-20 08:15:29 -05:00
G30 e61724d2b1 feat: Enhanced File Viewer Modal (Excel, CSV, Markdown & Code) (#20035)
* feat: Add Excel file viewer to FileItemModal

* feat: Add CSV file viewer to FileItemModal

* feat: Add Markdown and Code syntax highlighting to file viewer

* chore: add dependency

* fix: default to raw text view for Excel/Code/MD files

* fix: only show rows count in preview tab for excel files
2025-12-20 08:15:01 -05:00
Classic298 cd170735c5 chore: dep bump (#20012)
* Update requirements-min.txt

* Update pyproject.toml

* Update requirements.txt

* Update pyproject.toml

* Update requirements.txt

* Update requirements-min.txt
2025-12-20 07:30:30 -05:00
3d5aaa9ead feat: Align conditional fetching with conditional rendering for API Keys and Channels (#118) (#20043)
Fixes #19967

Co-authored-by: Tim Baek <tim@openwebui.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-20 07:29:51 -05:00
149bb9dae2 feat/perf: Add ENABLE_PUBLIC_ACTIVE_USERS_COUNT environment variable (#20027)
* Merge pull request open-webui#19030 from open-webui/dev (#115)

Co-authored-by: Tim Baek <tim@openwebui.com>
Co-authored-by: Claude <noreply@anthropic.com>
Resolves #13026

* Claude/find active user count 1ct t1 (#116)

Co-authored-by: Tim Baek <tim@openwebui.com>
Co-authored-by: Claude <noreply@anthropic.com>
Resolves #13026

* Claude/find active user count 1ct t1 (#117)

Co-authored-by: Tim Baek <tim@openwebui.com>
Co-authored-by: Claude <noreply@anthropic.com>
Resolves #13026

---------

Co-authored-by: Tim Baek <tim@openwebui.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-20 05:38:16 -05:00
Cyp ed8bb028a0 improve Korean translation (#20021) 2025-12-20 05:34:30 -05:00
Shirasawa ffbd774d32 I18n: improve Chinese translation (#20055)
* i18n: improve zh-CN translation

* i18n: improve zh-TW translation
2025-12-20 05:32:12 -05:00
G30 05a3aae59c fix: preserve access_control when cloning prompts (#19960)
* fix: preserve access_control when cloning prompts

Cloned prompts now inherit the original prompt's access_control settings
instead of defaulting to null/public. This ensures that permission and
sharing restrictions are carried over with clones as expected.

If the original prompt doesn't have access_control set, defaults to {}
(private) which matches the behavior of newly created prompts.

Fixes #19360

* fix: clone access control
2025-12-16 13:56:28 -05:00
Timothy Jaeryang Baek f509f5542d refac: channel name 2025-12-16 13:51:39 -05:00
Timothy Jaeryang Baek 0eb33e8e12 refac: logit bias handling 2025-12-16 13:49:00 -05:00
Timothy Jaeryang Baek 59d6eb2bad refac 2025-12-16 13:45:11 -05:00
Timothy Jaeryang Baek 7bd43f00b1 refac: channel name styling 2025-12-16 13:44:15 -05:00
Timothy Jaeryang Baek cad4c61a25 refac: readme 2025-12-16 13:20:29 -05:00
Classic298 dce52f33c0 chore: small dep bump (#19972)
* Update requirements.txt

* Update pyproject.toml
2025-12-15 13:31:40 -05:00
G30 0413c973e7 fix: add functional null safety checks and boolean casting to Chat.svelte (#19921)
* fix(chat): add functional null safety checks and boolean casting

* Update Chat.svelte
2025-12-14 15:13:39 -05:00
Boris Bocquet bc681f8258 feat : new environment variable SEARXNG_LANGUAGE , in the persistent config, that you can also edit in Admin > Web Search pannel in case you choose Searxng. This is used in the request to searxng as the "search language" (arguement "language"). Before this feature, it was set to en-US only. Now default is "all". (#19909) 2025-12-14 12:38:47 -05:00
Taylor Wilsdon 195801f74a ensure from config (#19902) 2025-12-14 12:38:19 -05:00
G30 07d28ad781 fix: enable COMFYUI_WORKFLOW_NODES and IMAGES_EDIT_COMFYUI_WORKFLOW_NODES configuration via env vars (#19918)
* fix: enable COMFYUI_WORKFLOW_NODES loading from environment variables

* fix: enable IMAGES_EDIT_COMFYUI_WORKFLOW_NODES loading from environment variables
2025-12-14 12:15:20 -05:00
Kylapaallikko 8f7f5db1fd Update fi-FI translation.json (#19926)
Added missing translations.
2025-12-14 12:14:38 -05:00
G30 06038cd73d fix: resolve NPE in Node.svelte when model is undefined (#19935) 2025-12-14 12:13:49 -05:00
G30 e11c6dca75 fix(model-editor): add null safety for tools, functions, and file input (#19939) 2025-12-14 12:12:57 -05:00
Leo Wang a1bde74e97 fix(prompt): preserve non-ASCII characters in tool specs serialization (#19942)
Use json.dumps with ensure_ascii=False to avoid converting Chinese
descriptions into Unicode escape sequences.

This makes logs easier to read and significantly improves LLM tool
selection accuracy (from ~48% to ~67%) by preserving the original
natural language context.
2025-12-14 12:12:14 -05:00
Classic298 1c62be4406 chore: dep bump (#19937)
* Update requirements.txt

* Update requirements-min.txt

* Update pyproject.toml
2025-12-13 14:42:08 -05:00
MichaelandTim Baek 67290e1c2a i18n(de): Optimize and standardize German translations (#19911)
* i18n(de): Optimize and standardize German translations for consistency and tone

- Unified user addressing to the formal "Sie" (polite form) throughout the UI, retaining "Du" only for specific persona contexts
- Fixed context-specific translation errors (e.g., "Call" from "Anrufen" to "Aufrufen", "Generation" from "Generation" to "Generierung", "Dokomentenintelligenz" typo)
- Corrected hyphenation for compound nouns (e.g., "JSON-Datei", "API-Schlüssel", "Prompt-Variable") and general grammar
- Improved tone and clarity of error messages and prompts for better readability and a more professional UI tone
- Standardized technical concepts across all translation files

* Update translations based on the feedback provided

---------

Co-authored-by: Tim Baek <tim@openwebui.com>
2025-12-12 11:16:07 -05:00
ZyfaxandZyfax 64dbc71643 fix: reload ollama models list after model deletion (#19912)
Co-authored-by: Zyfax <kemon@hey.com>
2025-12-12 11:15:26 -05:00
Joseph Roberts 90209a30a8 fix: respect RTL text direction in file attachment containers (#19891)
Fixes #19742 where file attachment containers did not inherit the chat direction setting, causing layout issues when typing in RTL languages like Persian.

Added dir={$settings?.chatDirection ?? 'auto'} to file container divs in:
- MessageInput.svelte (chat input file attachments)
- UserMessage.svelte (user message file display)
- ResponseMessage.svelte (response message file display)
- channel/Messages/Message.svelte (channel message file display)

The fix ensures file containers automatically detect and respect RTL text direction, maintaining proper layout alignment for RTL languages while preserving LTR behavior for Latin text.
2025-12-11 20:51:40 -05:00
G30 18e6cfb1fd fix: add null checks to chat iterators in ArchivedChats and FolderPlaceholder (#19898) 2025-12-11 20:34:39 -05:00
Joseph Low 8e661a4e73 Fix: Use SCAN instead of KEYS for Redis cluster compatibility (#19871) 2025-12-11 14:30:51 -05:00
Timothy Jaeryang Baek 379f888c9d enh/refac: ldap handle multiple usernames 2025-12-11 14:26:35 -05:00
Timothy Jaeryang Baek 4c4b9d19a1 refac: dockerfile PYTHONUNBUFFERED 2025-12-11 14:05:34 -05:00
ZyfaxandZyfax 7364b67455 fix: regenerate response shortcut (#19875)
* fix: regenerate shortcut

* Refactor shortcut handling for regenerate response

* refac

---------

Co-authored-by: Zyfax <kemon@hey.com>
2025-12-11 14:02:16 -05:00
Jeppe Kuhlmann Andersen 3418f53d07 Updated Danish translations (#19881) 2025-12-11 14:01:51 -05:00
Timothy Jaeryang Baek 3b3e12b43a refac 2025-12-11 01:09:14 -05:00
Timothy Jaeryang Baek 4d9a51ba33 refac 2025-12-11 00:11:12 -05:00
Timothy Jaeryang Baek 4b4241273d refac: styling 2025-12-11 00:07:32 -05:00
Timothy Jaeryang Baek db95e96688 chore: dep 2025-12-10 23:59:52 -05:00
Shirasawa 99c820d607 fix: fixed the issue of mismatched spaces in audio MIME types (#17771) 2025-12-10 23:59:10 -05:00
Timothy Jaeryang Baek 282c541427 refac 2025-12-10 23:56:20 -05:00
Timothy Jaeryang BaekandALiNew b364cf43d3 feat: resizable sidebar
Co-Authored-By: ALiNew <42788336+sukjinkim@users.noreply.github.com>
2025-12-10 23:54:36 -05:00
Timothy Jaeryang Baek b9676cf36f refac: styling 2025-12-10 23:35:46 -05:00
G30andTim Baek 258caaeced fix: resolve layout shift in knowledge items with long names (#19832)
Co-authored-by: Tim Baek <tim@openwebui.com>
2025-12-10 23:34:36 -05:00
Timothy Jaeryang Baek 6e99b10163 refac 2025-12-10 23:31:11 -05:00
Timothy Jaeryang Baek a2a9a9bcf4 refac 2025-12-10 23:28:40 -05:00
Timothy Jaeryang Baek 0addc1ea46 refac 2025-12-10 23:28:33 -05:00
Timothy Jaeryang Baek 6812d3b9d1 refac 2025-12-10 23:20:38 -05:00
Timothy Jaeryang Baek ceae3d48e6 enh/refac: kb pagination 2025-12-10 23:19:19 -05:00
Timothy Jaeryang Baek 3ed1df2e53 refac: search notes db query 2025-12-10 21:06:53 -05:00
Timothy Jaeryang Baek 68219d84a9 refac 2025-12-10 17:08:31 -05:00
Timothy Jaeryang Baek 6068e23590 refac 2025-12-10 17:08:18 -05:00
Timothy Jaeryang Baek d7467a86e2 refac 2025-12-10 17:03:51 -05:00
Timothy Jaeryang Baek d098c57d4d refac 2025-12-10 17:00:28 -05:00
Timothy Jaeryang Baek 693636d971 enh/refac: show read only kbs 2025-12-10 16:58:53 -05:00
Timothy Jaeryang Baek a6ef82c5ed refac: styling 2025-12-10 16:43:43 -05:00
Timothy Jaeryang Baek 79cfe29bb2 refac: channel_file and knowledge table migration 2025-12-10 16:41:22 -05:00
Timothy Jaeryang Baek d1d42128e5 refac/fix: channel files 2025-12-10 15:53:45 -05:00
Timothy Jaeryang Baek 2bccf8350d enh: channel files 2025-12-10 15:48:42 -05:00
Timothy Jaeryang Baek c15201620d refac: kb files 2025-12-10 15:48:27 -05:00
Andreas f31ca75892 Fix typo in user permission environment variables (#19860) 2025-12-10 15:09:15 -05:00
Timothy Jaeryang Baek a7993f6f4e refac 2025-12-10 12:22:40 -05:00
Timothy Jaeryang Baek ae47101dc6 refac 2025-12-10 11:07:41 -05:00
Timothy Jaeryang Baek cf6a1300ca enh: experimental chat usage stats endpoint 2025-12-10 02:00:00 -05:00
Timothy Jaeryang Baek a934dc997e refac: drop legacy kb support 2025-12-10 01:07:12 -05:00
Timothy Jaeryang Baek ed2db0d04b refac 2025-12-10 00:58:08 -05:00
Timothy Jaeryang Baek 4ecacda28c refac 2025-12-10 00:55:31 -05:00
Timothy Jaeryang Baek 94a8439105 feat/enh: kb file pagination 2025-12-10 00:53:41 -05:00
Timothy Jaeryang Baek 7b0b16ebbd refac 2025-12-09 23:57:46 -05:00
Timothy Jaeryang Baek 49d54c5821 refac 2025-12-09 23:33:48 -05:00
Timothy Jaeryang Baek 0eafc09965 refac: styling 2025-12-09 22:28:38 -05:00
Timothy Jaeryang Baek 6a75620fcb refac: styling 2025-12-09 21:53:34 -05:00
Timothy Jaeryang Baek 205c711120 enh: expand input 2025-12-09 21:50:27 -05:00
Timothy Jaeryang Baek 3af96c9d4e refac: styling 2025-12-09 21:11:49 -05:00
Timothy Jaeryang Baek 6e0badde67 refac: styling 2025-12-09 21:10:50 -05:00
Timothy Jaeryang Baek b29e7fd0be refac/fix: styling 2025-12-09 21:01:39 -05:00
Timothy Jaeryang Baek 02df867843 refac 2025-12-09 20:52:18 -05:00
Timothy Jaeryang Baek 00c2b6ca40 feat/enh: create note from input 2025-12-09 20:49:46 -05:00
Timothy Jaeryang Baek 65d4b22c7c refac 2025-12-09 19:47:38 -05:00
Timothy Jaeryang Baek a4fe823893 refac: styling 2025-12-09 19:45:13 -05:00
Timothy Jaeryang Baek 103ff0c5e4 refac 2025-12-09 18:03:01 -05:00
Timothy Jaeryang Baek 4363df175d enh: read only notes 2025-12-09 17:57:15 -05:00
Timothy Jaeryang Baek 307b37d5e2 refac 2025-12-09 17:19:42 -05:00
Timothy Jaeryang Baek 9b24cddef6 enh/refac: notes 2025-12-09 16:45:08 -05:00
Shirasawa 1ea555a5ac i18n: improve Chinese translation (#19830) 2025-12-09 15:29:43 -05:00
Shirasawa c24b1207a0 fix: fixed missing text in the explanation feature (#19829) 2025-12-09 15:29:27 -05:00
Classic298 44e41806f2 chore: dep bump across many dependencies (#19850)
* Update pyproject.toml (#101)

* Update pyproject.toml

* Update requirements.txt

* Update requirements-min.txt

* Upgrade Playwright version to 1.57.0

* Update langchain-community version to 0.3.29

* Update requirements.txt

* Update requirements-min.txt
2025-12-09 15:28:21 -05:00
Timothy Jaeryang Baek 2b1a29d44b enh: display user groups in user preview 2025-12-08 12:45:52 -05:00
Timothy Jaeryang BaekandJan Kessler f5fbbaf060 refac: redis config log
Co-Authored-By: Jan Kessler <Ithanil@users.noreply.github.com>
2025-12-08 12:18:25 -05:00
Timothy Jaeryang BaekandJan Kessler ba158d378f feat: REDIS_SOCKET_CONNECT_TIMEOUT
Co-Authored-By: Jan Kessler <Ithanil@users.noreply.github.com>
2025-12-08 11:59:45 -05:00
Timothy Jaeryang Baek b02397e460 feat: WEB_LOADER_TIMEOUT 2025-12-08 11:49:27 -05:00
Timothy Jaeryang Baek bcd50ed8f1 refac 2025-12-08 11:30:38 -05:00
_00_ 8cea0cf746 FIX: Pipeline save settings - Handle undefined valves property (#19791)
### FIX: Pipeline save settings - Handle undefined valves property

When a Pipeline valve have a null value the settings isn't saved.
The error occurs because the code tries to call `.split()` on a `null` value when saving pipeline valves.
This happens when you set a valve to "None" (null) and then click save.

This PR Fix this issue.
2025-12-08 10:09:53 -05:00
Timothy Jaeryang Baek ce945a9334 refac 2025-12-07 23:56:37 -05:00
Timothy Jaeryang Baek 3c8f1cf8e5 fix: source citations user message display issue 2025-12-07 23:53:46 -05:00
Timothy Jaeryang Baek 4d4ed743ae fix: styling 2025-12-07 18:13:28 -05:00
Timothy Jaeryang Baek aa9c0389c3 refac 2025-12-07 18:06:06 -05:00
bitsofinfo 492c8bac09 feat: new OAUTH_AUDIENCE config (#19768)
* feat: new config AUTH0_AUDIENCE

* feat: OAUTH_AUDIENCE config
2025-12-06 10:45:34 -05:00
Classic298 0617ede0eb Upgrade Playwright image and command to v1.56.0 (#19774) 2025-12-06 09:27:45 -05:00
Timothy Jaeryang Baek 7e6b8a9a71 refac/fix: docling auth 2025-12-06 08:06:42 -05:00
Classic298 dcdbafa5ae chore: bump pymilvus (#19773)
* Update pymilvus version to 2.6.5

* Update pyproject.toml
2025-12-06 08:03:42 -05:00
Classic298 cdd75ade50 BREAKING/CAUTION: chore: chromadb dep bump - needs testing (#19780)
* Update requirements.txt

* Update requirements-min.txt

* Update pyproject.toml
2025-12-06 08:03:04 -05:00
Timothy Jaeryang Baek 508fde70ff fix: temp chat perm 2025-12-06 08:02:12 -05:00
Timothy Jaeryang Baek 2a8a2f1ba3 refac 2025-12-04 20:40:52 -05:00
_00_andTim Baek d90d036d23 UPD: i18n es-ES Translation v.0.6.41 (#19745)
### UPD: i18n es-ES Translation v.0.6.41

Added new strings

Co-authored-by: Tim Baek <tim@openwebui.com>
2025-12-04 15:00:15 -05:00
James Westbrook 2f68ac33b3 chore: remove very outdated kubernetes configs (#19731)
* cleanup: remove very outdated kubernetes configs

* chore: remove INSTALLATION.md that references k8s manifests

* chore: remove kubernetes/ from .prettierignore
2025-12-04 14:59:30 -05:00
Aleix Dorca 01284b92db Update catalan translation.json (#19741) 2025-12-04 14:59:20 -05:00
Classic298 1779090bdb fix: add missing env var parameter pass through for enable async embedding (#19748)
* Add enable_async parameter to embedding function

* Add enable_async parameter to RAG configuration
2025-12-04 14:59:09 -05:00
Classic298 d27e019af3 chore: update qdrant dependency to fix Startup Warning (#19757)
* Update pyproject.toml

* Update requirements.txt
2025-12-04 14:58:04 -05:00
Timothy Jaeryang Baek 5c2df97f04 fix: user channels issue 2025-12-04 14:43:23 -05:00
Timothy Jaeryang Baek 6fe737bf8f refac 2025-12-03 19:31:23 -05:00
Timothy Jaeryang Baek 7b126b23d5 enh: channels video file upload behaviour 2025-12-03 19:15:30 -05:00
Timothy Jaeryang Baek 22f1b764a7 refac/perf: channel image upload behaviour 2025-12-03 19:06:02 -05:00
Timothy Jaeryang Baek 39f778c275 refac 2025-12-03 18:22:00 -05:00
Timothy Jaeryang Baek 54b7ec56d6 enh/refac: channels message lazy load data 2025-12-03 17:52:44 -05:00
G30 133618aaf0 fix: UI scale buttons (- and +) (#19699) 2025-12-03 17:17:05 -05:00
Shirasawa b560775713 fix: do not display the move button when no folders (#19705) 2025-12-03 17:16:38 -05:00
Shirasawa 964333ed65 i18n: improve Chinese translation (#19703) 2025-12-03 17:16:03 -05:00
joaoback 85b1e21765 Update translation.json (pt-BR) (#19700)
Translations of the new items added in the latest version.
2025-12-03 17:15:47 -05:00
Timothy Jaeryang Baek c86fcb75b7 chore 2025-12-02 19:59:14 -05:00
Tim Baek 6f1486ffd0 Merge pull request #19466 from open-webui/dev
0.6.41
2025-12-02 17:28:46 -05:00
Timothy Jaeryang Baek 73f7e91dec chore: format 2025-12-02 17:16:12 -05:00
Classic298 8361f73ca6 chore: 0.6.41 Changelog (#19473)
* 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
2025-12-02 17:15:19 -05:00
Timothy Jaeryang Baek 11efb982c1 refac 2025-12-02 17:14:45 -05:00
Timothy Jaeryang Baek 9d87688ecc chore: bump 2025-12-02 16:53:05 -05:00
Classic298 4f9677ffcf Update translation.json (#19697)
* Update translation.json

* Update translation.json
2025-12-02 16:48:11 -05:00
a49e1d87ad fix: Default Group ID assignment on SSO/OAUTH and LDAP (#19685)
* fix (#99)

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

* Update auths.py

* unified logic

* PUSH

* remove getattr

* rem getattr

* whitespace

* Update oauth.py

* trusted header group sync

Added default group re-application after trusted header group sync

* not apply after syncs

* .

* rem

---------

Co-authored-by: Tim Baek <tim@openwebui.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 16:48:00 -05:00
Timothy Jaeryang Baek 9a65ed2260 chore: format 2025-12-02 16:06:57 -05:00
Classic298 864d54095f Update translation.json (#19696) 2025-12-02 16:06:06 -05:00
Classic298 b29fdc2a0c Update milvus_multitenancy.py (#19695) 2025-12-02 15:38:06 -05:00
Classic298andTim Baek 12f237ff80 fix: Update milvus.py (#19602)
* Update milvus.py

* Update milvus.py

* Update milvus.py

* Update milvus.py

* Update milvus.py

---------

Co-authored-by: Tim Baek <tim@openwebui.com>
2025-12-02 15:30:31 -05:00
Timothy Jaeryang Baek 192c2af7ba refac 2025-12-02 15:17:47 -05:00
Matthew Kusz 17bfd38696 Fix dropdown backgrounds (#19693) 2025-12-02 15:16:36 -05:00
Henne a7e614ca4c feat: Adds document intelligence model configuration (#19692)
* Adds document intelligence model configuration

Enables the configuration of the Document Intelligence model to be used by the RAG pipeline.

This allows users to specify the model they want to use for document processing, providing flexibility and control over the extraction process.

* Added Titel to Document Intelligence Model Config

Added Titel to Document Intelligence Model Config
2025-12-02 14:41:09 -05:00
Timothy Jaeryang Baek e5c6b739c2 refac 2025-12-02 11:43:00 -05:00
Timothy Jaeryang Baek 34169b3581 refac 2025-12-02 11:31:23 -05:00
Timothy Jaeryang Baek 01868e856a enh: group members endpoint 2025-12-02 11:24:23 -05:00
Timothy Jaeryang Baek e301d1962e refac/perf: has_access_to_file optimization 2025-12-02 11:11:17 -05:00
Timothy Jaeryang Baek 9f6c91987f refac 2025-12-02 11:00:34 -05:00
Timothy Jaeryang Baek d19023288e feat/enh: kb files db migration 2025-12-02 10:53:32 -05:00
Timothy Jaeryang Baek 29236aefe8 refac 2025-12-02 10:25:38 -05:00
Timothy Jaeryang Baek 6ce9afd95d refac 2025-12-02 09:21:03 -05:00
Timothy Jaeryang Baek 39f7575b64 refac: show connection type for custom models 2025-12-02 06:19:48 -05:00
Timothy Jaeryang Baek 954aaa6bdc refac: styling 2025-12-02 05:47:05 -05:00
Timothy Jaeryang Baek aa589fcbd9 refac 2025-12-02 05:36:45 -05:00
Timothy Jaeryang Baek 9f42b9369f refac 2025-12-02 05:29:34 -05:00
Timothy Jaeryang Baek 143d3fbce2 refac 2025-12-02 04:18:19 -05:00
PocciaandTim Baek 6e531679f4 fix/adjust web search to properly block domains (#19670)
Co-authored-by: Tim Baek <tim@openwebui.com>
2025-12-02 04:17:32 -05:00
Timothy Jaeryang Baek 562f22960c refac 2025-12-02 04:07:02 -05:00
Timothy Jaeryang Baek 5388cc1bc6 refac 2025-12-02 04:03:44 -05:00
Classic298 0a14196afb Update milvus_multitenancy.py (#19680) 2025-12-02 03:57:14 -05:00
Timothy Jaeryang Baek 7b16637043 feat: signin rate limit 2025-12-02 03:52:38 -05:00
Timothy Jaeryang Baek 734c04ebf0 refac 2025-12-02 02:53:49 -05:00
Classic298andTim Baek 4f50571b53 Chore: dep bump (#19667)
* Update pyproject.toml

* Update requirements-min.txt

* Update requirements.txt

---------

Co-authored-by: Tim Baek <tim@openwebui.com>
2025-12-02 02:34:57 -05:00
Timothy Jaeryang Baek 52ccab8fc0 refac 2025-12-01 13:52:09 -05:00
Timothy Jaeryang Baek f5e8d4d5a0 refac 2025-12-01 13:34:57 -05:00
Timothy Jaeryang Baek 51621ba91a feat/enh: user status 2025-12-01 13:18:59 -05:00
Timothy Jaeryang Baek dba86bc980 fix: audit 2025-12-01 10:59:01 -05:00
Shirasawa 21f3411692 i18n: improve Chinese translation (#19651) 2025-12-01 10:58:06 -05:00
Timothy Jaeryang Baek 91473c788c chore: otel bump 2025-12-01 10:57:00 -05:00
Timothy Jaeryang Baek 25f0c26b25 chore: otel bump 2025-12-01 10:29:20 -05:00
Timothy Jaeryang Baek 9791c9bd8b refac 2025-11-30 15:56:42 -05:00
Timothy Jaeryang Baek c62609faba refac 2025-11-30 14:51:44 -05:00
Timothy Jaeryang Baek 88decab9be refac 2025-11-30 14:23:08 -05:00
Timothy Jaeryang Baek d499c3aed8 refac 2025-11-30 14:17:54 -05:00
Timothy Jaeryang Baek 277f3a91f1 refac 2025-11-30 14:06:16 -05:00
joaoback 1818f2b3d9 Update translation.json (pt-BR) (#19603)
translations of the new items that have been included
2025-11-30 11:09:16 -05:00
Timothy Jaeryang Baek a0826ec9fe feat/enh: dm from user profile preview 2025-11-30 11:04:06 -05:00
Timothy Jaeryang Baek 3c846617cd refac 2025-11-30 10:45:54 -05:00
Timothy Jaeryang Baek 39645102d1 refac 2025-11-30 10:40:24 -05:00
Timothy Jaeryang Baek 3f1d9ccbf8 feat/enh: add/remove users from group channel 2025-11-30 10:33:50 -05:00
Timothy Jaeryang Baek 781aeebd2a refac 2025-11-30 08:28:19 -05:00
Timothy Jaeryang Baek f589b7c189 feat/enh: group channel 2025-11-30 08:24:27 -05:00
Timothy Jaeryang Baek 696f356881 refac 2025-11-30 08:12:22 -05:00
Timothy Jaeryang Baek 515f85fe1c refac 2025-11-30 05:17:52 -05:00
Timothy Jaeryang Baek 4d74e6cefa refac: styling 2025-11-30 05:01:41 -05:00
Timothy Jaeryang Baek 3ebb3e2143 refac: styling 2025-11-30 03:56:12 -05:00
Timothy Jaeryang Baek 69b82edd63 refac 2025-11-30 03:50:14 -05:00
Timothy Jaeryang Baek 9d39b9b42c refac: styling 2025-11-30 03:47:34 -05:00
Timothy Jaeryang Baek e65d92fc6f refac 2025-11-30 02:32:34 -05:00
Timothy Jaeryang Baek f3c8c7045d refac 2025-11-29 14:19:55 -05:00
Timothy Jaeryang Baek c9185aaf44 refac 2025-11-29 14:18:22 -05:00
Timothy Jaeryang Baek 05e79bdd0c enh: message reaction user names 2025-11-29 13:54:52 -05:00
Timothy Jaeryang Baek fb6b18faef refac: knowledge file delete behaviour 2025-11-29 13:19:06 -05:00
Tim Baek b56adf01e3 Merge pull request #19584 from Classic298/patch-1
refac: improve weaker model's ability of understanding that the image was created successfully
2025-11-29 13:09:45 -05:00
Timothy Jaeryang Baek 356e982d30 refac 2025-11-29 12:57:17 -05:00
Classic298 bb4b547574 Update middleware.py 2025-11-29 11:11:21 +01:00
Timothy Jaeryang Baek 20340c3e4e refac 2025-11-29 01:05:39 -05:00
Timothy Jaeryang Baek 6c53bf7175 refac: styling 2025-11-28 23:10:01 -05:00
Timothy Jaeryang Baek ff121413da refac 2025-11-28 22:54:00 -05:00
Timothy Jaeryang Baek c1d760692f refac: db group 2025-11-28 22:48:58 -05:00
Timothy Jaeryang Baek a7c7993bbf refac/fix: temp chat image generation 2025-11-28 11:11:56 -05:00
Timothy Jaeryang Baek 0f3156651c refac/fix: ollama model delete 2025-11-28 11:01:22 -05:00
Timothy Jaeryang Baek c8071a3180 refac 2025-11-28 10:51:30 -05:00
Timothy Jaeryang Baek 25994dd3da refac/enh: channel message 2025-11-28 10:45:48 -05:00
Timothy Jaeryang Baek b9e849f17d refac: styling 2025-11-28 10:08:37 -05:00
Timothy Jaeryang Baek 80fbb29ccc refac: styling 2025-11-28 10:07:57 -05:00
Timothy Jaeryang Baek 7b1895ec8a refac 2025-11-28 10:04:06 -05:00
Timothy Jaeryang Baek aae2fce173 feat/enh: pinned messages in channels 2025-11-28 09:58:44 -05:00
Timothy Jaeryang Baek 451907cc92 refac 2025-11-28 09:32:37 -05:00
Timothy Jaeryang Baek 1b095d12ff refac: admin user list active indicator 2025-11-28 08:45:31 -05:00
Timothy Jaeryang Baek 0518749d51 refac: pin icons 2025-11-28 08:01:42 -05:00
Tim Baek fc06c16dd4 Merge pull request #19573 from open-webui/update-user-table
refac/db: update user table
2025-11-28 07:55:00 -05:00
Timothy Jaeryang Baek 33b59adf27 refac 2025-11-28 07:42:45 -05:00
Timothy Jaeryang Baek 70948f8803 enh/refac: deprecate USER_POOL 2025-11-28 07:39:02 -05:00
Timothy Jaeryang Baek c2634d45ad refac 2025-11-28 07:27:55 -05:00
Timothy Jaeryang Baek 8ef482a52a refac: user oauth display 2025-11-28 06:59:59 -05:00
Timothy Jaeryang Baek dcf50c4758 refac: api_key table migration 2025-11-28 06:49:10 -05:00
Timothy Jaeryang Baek 742832a850 refac 2025-11-28 06:41:41 -05:00
Timothy Jaeryang Baek 0a4358c3d1 refac: oauth_sub -> oauth migration 2025-11-28 06:39:36 -05:00
Timothy Jaeryang Baek 369298a83e refac: user table db migration 2025-11-28 06:29:41 -05:00
Timothy Jaeryang Baek b99c9b277a refac: styling 2025-11-28 04:29:50 -05:00
Timothy Jaeryang Baek 4b6773885c enh: dm active user indicator 2025-11-28 04:24:25 -05:00
Timothy Jaeryang Baek d232e433e8 refac: profile preview 2025-11-28 04:22:54 -05:00
Timothy Jaeryang Baek 848f3fd4d8 refac: hide active user count in sidebar user menu 2025-11-28 03:40:16 -05:00
Timothy Jaeryang Baek 453ea9b9a1 refac/fix: db migration issue 2025-11-28 03:10:48 -05:00
Timothy Jaeryang Baek 6ee50770cd refac 2025-11-28 02:44:36 -05:00
Timothy Jaeryang Baek 15dc607779 refac: rm print 2025-11-28 02:34:25 -05:00
Timothy Jaeryang Baek 32c888c280 refac 2025-11-28 01:40:52 -05:00
Timothy Jaeryang Baek 99a7823e01 refac: db 2025-11-28 01:17:43 -05:00
RomualdYT 022f9ff3a5 Update french translation.json (#19547) 2025-11-27 09:21:27 -05:00
Timothy Jaeryang Baek ad86707605 refac 2025-11-27 08:20:14 -05:00
Timothy Jaeryang Baek 289801b608 refac: styling 2025-11-27 08:12:06 -05:00
Timothy Jaeryang Baek 6bb204eb80 refac 2025-11-27 08:07:39 -05:00
Timothy Jaeryang Baek 560702a8f7 refac 2025-11-27 08:04:41 -05:00
Timothy Jaeryang Baek 6752772c1d chore: format 2025-11-27 07:57:31 -05:00
Timothy Jaeryang Baek d645cdbaf3 refac 2025-11-27 07:49:19 -05:00
Timothy Jaeryang Baek 3b4d7d568b refac 2025-11-27 07:43:10 -05:00
Timothy Jaeryang Baek d5d0e72590 refac 2025-11-27 07:39:00 -05:00
Timothy Jaeryang Baek f1a7de94ba refac 2025-11-27 07:33:33 -05:00
Timothy Jaeryang Baek acccb9afdd feat: dm channels 2025-11-27 07:27:32 -05:00
Timothy Jaeryang Baek f2c56fc839 refac 2025-11-27 06:03:22 -05:00
Timothy Jaeryang Baek dd6b808e69 refac 2025-11-27 05:25:38 -05:00
Timothy Jaeryang Baek 7a374ca2a5 refac 2025-11-27 05:11:17 -05:00
Timothy Jaeryang Baek 421aba7cd7 refac: hide channel add button for users 2025-11-27 04:49:29 -05:00
Timothy Jaeryang Baek 09b6ea38c5 feat/enh: group export endpoint 2025-11-27 04:44:01 -05:00
Timothy Jaeryang Baek 28659f6af5 refac/fix: files batch/add endpoint 2025-11-27 04:35:12 -05:00
Timothy Jaeryang Baek 64b4d5d9c2 feat/enh: channels unread messages count 2025-11-27 04:31:04 -05:00
Aleix Dorca c7a48c50a3 Update catalan translation.json (#19536) 2025-11-27 03:02:08 -05:00
Timothy Jaeryang Baekandcw.a b5e5617a41 enh: redis dict for internal models state
Co-Authored-By: cw.a <57549718+acwoo97@users.noreply.github.com>
2025-11-27 01:33:52 -05:00
Timothy Jaeryang Baek ff4b1b9824 refac: chat history data structure 2025-11-27 00:10:53 -05:00
stevessr 86cdcda29a fix: button without type (#19534) 2025-11-27 00:01:36 -05:00
Timothy Jaeryang Baek 5a32ea9b49 refac 2025-11-26 23:54:55 -05:00
Timothy Jaeryang Baek 457af65df6 enh/feat: toggle folders & user perm 2025-11-26 22:47:48 -05:00
Tobias Genannt 04b337323a fix: correct role check on OAuth login (#19476)
When a users role is switched from admin to user in the OAuth provider
their groups are not correctly updated when ENABLE_OAUTH_GROUP_MANAGEMENT
is enabled.
2025-11-26 21:48:06 -05:00
Timothy Jaeryang Baek 384753c6ca refac/enh: drop profile_image_url field in responses 2025-11-26 21:47:20 -05:00
Timothy Jaeryang Baek 3fe5a47050 refac/enh: knowledge base name on icon hover 2025-11-26 21:33:27 -05:00
Timothy Jaeryang Baek d1bbf6ba92 refac 2025-11-26 21:29:52 -05:00
Timothy Jaeryang Baek 9f89cc5adc refac 2025-11-26 21:28:32 -05:00
Shirasawa fa0efae4d5 i18n: improve Chinese translation (#19497) 2025-11-26 17:42:56 -05:00
gerhardj-b f2d6a425de feat: also consider OAUTH_ROLES_SEPARATOR for string claims themselves (#19514) 2025-11-26 17:38:26 -05:00
Classic298 d071cdf7d4 chore: update transformers dependency to fix issue #19512 (#19513)
* Update pyproject.toml

* Update requirements.txt

* Update requirements.txt

* Update pyproject.toml
2025-11-26 16:41:56 -05:00
Classic298 4b21704498 chore: Update pymilvus dep (#19507)
* Update requirements.txt

* Update pyproject.toml
2025-11-26 16:41:20 -05:00
Classic298 9fca4969db chore: dep bump pypdf to ver 6.4.0 (#19508)
* Update pyproject.toml

* Update requirements.txt
2025-11-26 16:41:12 -05:00
Timothy Jaeryang Baek 4370dee79e fix: async save docs to vector db 2025-11-25 17:19:33 -05:00
Classic298 c631659327 i18n: de-de (#19471) 2025-11-25 16:31:38 -05:00
Classic298 4df5b7eb2e fix: update dependency to prevent rediss:// failure (#19488)
* Update pyproject.toml

* Update requirements.txt

* Update requirements-min.txt
2025-11-25 16:28:58 -05:00
Timothy Jaeryang Baek 8b2015a97b refac 2025-11-25 16:28:06 -05:00
Timothy Jaeryang Baek 477097c2e4 refac 2025-11-25 16:27:27 -05:00
Timothy Jaeryang Baek c5b73d7184 refac/fix: function name filter type 2025-11-25 16:25:40 -05:00
Timothy Jaeryang Baek c7eb713689 fix: user preview profile image 2025-11-25 08:00:30 -05:00
Aleix Dorca 1bfe2c92ba Merge pull request #19464 from aleixdorca/dev
i18n: Update Catalan translation.json
2025-11-25 07:03:49 -05:00
Timothy Jaeryang Baek 69722ba973 fix/refac: workspace shared model list 2025-11-25 06:32:27 -05:00
Tim Baek 140605e660 Merge pull request #19462 from open-webui/dev
0.6.40
2025-11-25 06:01:33 -05:00
Timothy Jaeryang Baek f3547568e4 refac: channel user list order by 2025-11-25 05:53:31 -05:00
Classic298 15c6860a49 Update CHANGELOG.md (#19463)
* Update CHANGELOG.md

* Update CHANGELOG.md
2025-11-25 05:50:39 -05:00
Timothy Jaeryang Baek 363ef194d8 chore: bump python-socketio==5.14.0 2025-11-25 05:49:30 -05:00
Timothy Jaeryang Baek 33a52628e6 chore: bump 2025-11-25 05:48:12 -05:00
Timothy Jaeryang Baek 35ab6b7667 fix: postgres user list issue 2025-11-25 05:47:04 -05:00
Timothy Jaeryang Baek 97ba5b8436 fix: changelog 2025-11-25 05:42:18 -05:00
Tim Baek 9899293f05 Merge pull request #19448 from open-webui/dev
0.6.39
2025-11-25 05:31:34 -05:00
Timothy Jaeryang Baek 3fa484f290 doc: changelog 2025-11-25 05:29:16 -05:00
Timothy Jaeryang Baek 03dc4d7182 refac/enh: copy formatted table 2025-11-25 05:25:49 -05:00
Classic298 82a5f11b72 CHANGELOG: 0.6.39 (#19446)
* 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
2025-11-25 05:13:23 -05:00
Timothy Jaeryang Baek 4847bdcc9b chore: format 2025-11-25 05:12:45 -05:00
Timothy Jaeryang Baek 0fa97bde00 fix: i18n 2025-11-25 05:11:37 -05:00
Timothy Jaeryang Baek d5c3e9ea42 refac 2025-11-25 05:10:08 -05:00
Timothy Jaeryang Baek 6235243b62 refac 2025-11-25 05:07:53 -05:00
Timothy Jaeryang Baek 63ca0a3519 refac 2025-11-25 04:56:26 -05:00
Classic298 6a095099d5 chore: add chardet (#19458)
* Update pyproject.toml

* Update requirements-min.txt

* Update requirements.txt

* Update requirements-min.txt

* Update requirements.txt

* Update pyproject.toml
2025-11-25 04:52:25 -05:00
Timothy Jaeryang Baek f22d92e102 refac: styling 2025-11-25 04:52:03 -05:00
Timothy Jaeryang Baek 84ca2258be refac 2025-11-25 04:45:52 -05:00
Timothy Jaeryang Baek e6d8f89850 chore: version bump 2025-11-25 04:38:13 -05:00
Timothy Jaeryang Baek c0e1203538 feat: user list in channels 2025-11-25 04:38:07 -05:00
Timothy Jaeryang Baek baa1e07aec refac 2025-11-25 04:37:58 -05:00
Timothy Jaeryang Baek f2ee70cbfc fix: ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION env var 2025-11-25 04:15:41 -05:00
Timothy Jaeryang Baek 3b5710d0cd feat/enh: show user count in channels 2025-11-25 03:46:30 -05:00
Timothy Jaeryang Baek a7ee36266a refac: styling 2025-11-25 03:15:40 -05:00
Timothy Jaeryang Baek f0c7bd3f79 refac 2025-11-25 03:12:21 -05:00
Timothy Jaeryang Baek 743199f2d0 feat/enh: tool server function name filter list 2025-11-25 02:31:34 -05:00
Timothy Jaeryang Baek 488631db98 refac 2025-11-25 02:05:27 -05:00
Timothy Jaeryang BaekandClassic298 2328dc284e feat/enh: async embedding processing setting
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2025-11-25 01:55:43 -05:00
Timothy Jaeryang Baek b1c1e68e56 refac/fix: group member user list 2025-11-25 01:37:33 -05:00
Timothy Jaeryang Baek 38c6b0bff6 fix: inline citations 2025-11-24 23:04:01 -05:00
Timothy Jaeryang Baek 9c19d0abd4 refac/breaking: docling params 2025-11-24 16:01:13 -05:00
Classic298andTim Baek b875a438f0 Update translation.json (#19445)
Co-authored-by: Tim Baek <tim@openwebui.com>
2025-11-24 15:39:53 -05:00
Timothy Jaeryang Baek f0d75e3a48 refac/fix: db operations 2025-11-24 15:39:13 -05:00
Classic298 0a687980ee Update knowledge.py (#19434) 2025-11-24 15:22:23 -05:00
Alexandr Promakh a7b611c0e5 fix: "No connection adapters were found" routers/images.py (#19435) 2025-11-24 14:51:52 -05:00
Tim Baek e567f42020 Merge pull request #19428 from joaoback/patch-16
Update translation.json (pt-BR)
2025-11-24 14:51:17 -05:00
joaoback 3b23b96a27 Update translation.json (pt-BR)
New translations of the items added in the latest version.
2025-11-24 12:11:22 -03:00
Tim Baek e3faec62c5 Merge pull request #19416 from open-webui/dev
0.6.38
2025-11-24 07:00:31 -05:00
Timothy Jaeryang Baek 0f8729dea2 refac 2025-11-24 06:42:12 -05:00
Timothy Jaeryang Baek 7ad549b4fb Update docker-build.yaml 2025-11-24 06:27:52 -05:00
Timothy Jaeryang Baek 5183eaab4d doc: changelog 2025-11-24 06:16:17 -05:00
Timothy Jaeryang Baek b004a4a2c1 chore: bump 2025-11-24 06:13:21 -05:00
Timothy Jaeryang Baek b2a6597617 fix: tool server save error handling 2025-11-24 06:10:21 -05:00
Timothy Jaeryang Baek 286a5ad0db refac/fix: oauth 2025-11-24 06:03:19 -05:00
Timothy Jaeryang Baek 06f0bfd9f5 fix 2025-11-24 05:58:22 -05:00
Timothy Jaeryang Baek 662a1fac47 fix: hybrid search 2025-11-24 05:52:18 -05:00
Tim Baek cea99175ca Merge pull request #19407 from ShirasawaSama/i18n/improve-chinese-translation
i18n: improve Chinese translation
2025-11-24 03:06:27 -05:00
Shirasawa a470f6149f i18n: improve Chinese translation 2025-11-24 07:11:52 +00:00
Tim Baek fc05e0a6c5 Merge pull request #19405 from open-webui/dev
chore: format
2025-11-23 22:16:33 -05:00
Timothy Jaeryang Baek 819668b42d chore: format 2025-11-23 22:16:06 -05:00
Tim Baek fe6783c166 Merge pull request #19030 from open-webui/dev
0.6.37
2025-11-23 22:10:05 -05:00
Timothy Jaeryang Baek a91663c504 refac 2025-11-23 22:09:14 -05:00
Timothy Jaeryang Baek 1e01836c08 refac: styling 2025-11-23 22:08:17 -05:00
Timothy Jaeryang Baek 97f5c21485 refac 2025-11-23 21:24:36 -05:00
Timothy Jaeryang Baek 9d14bc2a8d refac: styling 2025-11-23 21:21:08 -05:00
Timothy Jaeryang Baek 4857c69bd3 chore: format 2025-11-23 21:18:35 -05:00
Timothy Jaeryang Baek de889f5ec7 refac: styling 2025-11-23 21:17:14 -05:00
Timothy Jaeryang Baek 0259312626 refac 2025-11-23 21:12:01 -05:00
Timothy Jaeryang Baek 5af5e1d3e4 refac 2025-11-23 21:10:38 -05:00
Timothy Jaeryang BaekandClassic298 2e1ddf823b refac: prompt suggestions component
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2025-11-23 21:08:13 -05:00
Timothy Jaeryang Baek 5b1fa9dd0d refac: styling 2025-11-23 20:37:29 -05:00
Timothy Jaeryang Baek fbbfa5aa79 refac 2025-11-23 20:35:50 -05:00
Timothy Jaeryang Baek 42fa92ee64 refac 2025-11-23 20:24:42 -05:00
Classic298 7d3f45eff9 chore: CHANGELOG 0.6.37 (#19126)
* 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
2025-11-23 20:21:38 -05:00
Timothy Jaeryang Baek 0eb1246c87 refac 2025-11-23 20:19:44 -05:00
Timothy Jaeryang Baek 48d1e67e79 chore: format 2025-11-23 20:15:52 -05:00
Timothy Jaeryang Baek cd008eeb50 refac: folder page chat list 2025-11-23 20:11:55 -05:00
Timothy Jaeryang Baek e29c262394 refac/fix: refresh folder chat list 2025-11-23 20:04:54 -05:00
Timothy Jaeryang Baek 5148970ef5 refac 2025-11-23 19:47:21 -05:00
Timothy Jaeryang Baek a5c359aede refac 2025-11-23 19:42:59 -05:00
Timothy Jaeryang Baek 4ad7a9bb9c fix: kokorojs tts 2025-11-23 19:23:15 -05:00
Timothy Jaeryang Baek 66ffd77f2c refac 2025-11-23 19:19:22 -05:00
Timothy Jaeryang Baek 46bbf760e8 enh: group members selector 2025-11-23 18:56:50 -05:00
Timothy Jaeryang Baek 103f92c8dd refac 2025-11-23 18:39:01 -05:00
Timothy Jaeryang Baek 9b7c3ff999 refac 2025-11-23 18:31:59 -05:00
Timothy Jaeryang Baek ec45d77ce9 refac: sources and citations 2025-11-23 18:27:57 -05:00
Timothy Jaeryang Baek b0491886bc refac: disable single tilde 2025-11-23 17:16:10 -05:00
Classic298 a4c3fa70c1 chore: Update README (#19398) 2025-11-23 16:49:55 -05:00
Timothy Jaeryang Baek 50813fcce4 chore: google-genai bump 2025-11-23 16:49:21 -05:00
Classic298 99f0fe7f32 chore: user header forward minimize code changes throughout codebase (#19392)
* Update external.py

* remove unused imports

* Update ollama.py

* Update ollama.py

* Update ollama.py

* Update openai.py
2025-11-23 16:48:03 -05:00
Timothy Jaeryang Baek 14baf6955d refac 2025-11-23 16:47:21 -05:00
Timothy Jaeryang Baek 288947a648 refac 2025-11-23 16:09:37 -05:00
Timothy Jaeryang Baek 682013cee3 refac 2025-11-23 16:08:03 -05:00
Timothy Jaeryang Baek f5809165d7 refac 2025-11-23 15:28:07 -05:00
_00_ e2ef3d9647 Upd:i18n es-ES_Spanish Translation_v0.6.37 (#19388)
* Upd:i18n es-ES_Spanish Translation_v0.6.37

### es-ES Spanish Translation v0.6.37

Added new strings.

* Corrected string
2025-11-23 04:40:18 -05:00
Classic298 60dbde7e19 chore (#19389) 2025-11-23 04:40:05 -05:00
Timothy Jaeryang Baek 7cf07b7e97 refac: rm folder id on chat archive 2025-11-23 00:05:27 -05:00
Timothy Jaeryang Baekandexpruc e6951e804a feat/enh: move chats in folder on delete
Co-Authored-By: expruc <25387342+expruc@users.noreply.github.com>
2025-11-23 00:01:49 -05:00
Timothy Jaeryang Baek b2034861ae refac: models workspace optimization 2025-11-22 23:20:51 -05:00
Timothy Jaeryang Baek f9c96d03ad refac 2025-11-22 22:57:27 -05:00
Timothy Jaeryang Baek 9bfc414d26 refac 2025-11-22 21:33:14 -05:00
Classic298andClaude 902c6cfbea perf: 50x performance improvement for external embeddings (#19296)
* Update utils.py (#77)

Co-authored-by: Claude <noreply@anthropic.com>

* refactor: address code review feedback for embedding performance improvements (#92)

Co-authored-by: Claude <noreply@anthropic.com>

* fix: prevent sentence transformers from blocking async event loop (#95)

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-22 20:54:59 -05:00
Timothy Jaeryang Baek bb3e222e09 refac: clean null bytes on load 2025-11-22 20:50:27 -05:00
Timothy Jaeryang Baek 4af7cc818e refac/fix: chat search null byte filter 2025-11-22 20:34:49 -05:00
joaoback 19ffa9fc19 Update translation.json (pt-BR) (#19384)
new translations of the newly added items
2025-11-22 19:37:34 -05:00
Siwadon S. (Jay) d48f34cc5b fix(i18n): comprehensive revision and improvement of all Thai translations across the app (#19377) 2025-11-22 16:56:12 -05:00
Timothy Jaeryang Baek b32f7815b8 refac: search chat postgres 2025-11-21 18:31:03 -05:00
Timothy Jaeryang Baek 8f2812d394 fix: translation 2025-11-21 18:30:43 -05:00
Timothy Jaeryang Baek 833e3c2690 refac 2025-11-21 17:45:55 -05:00
Timothy Jaeryang Baek 598650f70b refac 2025-11-21 17:01:38 -05:00
Classic298 de971d7aa2 Update translation.json (#19364) 2025-11-21 15:59:48 -05:00
Siwadon S. (Jay) eec697e00d fix(i18n): correct Thai translation in sidebar (#19363) 2025-11-21 15:59:42 -05:00
Timothy Jaeryang Baek b4c4d9baf5 refac 2025-11-21 15:59:08 -05:00
Timothy Jaeryang Baek 4d4c572bba refac 2025-11-21 15:42:13 -05:00
Timothy Jaeryang BaekandG30 b88f829dbb enh: clone system models
Co-Authored-By: G30 <50341825+silentoplayz@users.noreply.github.com>
2025-11-21 05:28:55 -05:00
Timothy Jaeryang Baek 2b58191b82 refac 2025-11-21 04:32:49 -05:00
Timothy Jaeryang Baek 0c18cd67d5 refac/fix: openai edit multiple images 2025-11-21 04:13:59 -05:00
Timothy Jaeryang Baek 0dd9ad7ffc refac 2025-11-21 03:58:30 -05:00
Timothy Jaeryang BaekandG30 a51579a84b refac/pref: chat import optimization
Co-Authored-By: G30 <50341825+silentoplayz@users.noreply.github.com>
2025-11-21 03:49:49 -05:00
Aleix Dorca 3ef7367f01 Update Catalan translation.json (#19338) 2025-11-21 03:28:38 -05:00
Timothy Jaeryang Baek 89b1ad649b refac: styling 2025-11-21 03:10:00 -05:00
Shirasawa 23cadaa41a fix: add missing i18n import to fix build (#19337) 2025-11-21 03:05:15 -05:00
Danny Liu bed201e46e fix: format date according to DEFAULT_LOCALE in chat search (#19305)
* fix: localized format

* load default_locale from backend
2025-11-21 01:49:27 -05:00
Timothy Jaeryang Baek 27b8775032 refac 2025-11-21 01:48:06 -05:00
Shirasawa 41701697ec i18n: improve Chinese translation (#19334) 2025-11-21 01:39:27 -05:00
Cyp 2d8e321add Korean update (#19336) 2025-11-21 01:39:19 -05:00
Timothy Jaeryang Baek 6442871947 refac: profile_image_url optimization 2025-11-20 20:43:59 -05:00
Timothy Jaeryang Baek cd30152c83 refac: styling 2025-11-20 19:57:11 -05:00
Timothy Jaeryang Baek 557170c0b6 refac/enh: dedicated enable image edit toggle 2025-11-20 19:52:31 -05:00
Timothy Jaeryang Baek 0c430629e5 chore: dep 2025-11-20 19:24:52 -05:00
Diwakar b8728064d8 feat: add support for Weaviate vector database (#14747) 2025-11-20 19:23:46 -05:00
Timothy Jaeryang Baek 7be750bcbb feat/enh: group share setting 2025-11-20 19:12:56 -05:00
Timothy Jaeryang Baek d1e7957e69 refac 2025-11-20 18:39:30 -05:00
Timothy Jaeryang Baek f69e37a850 feat/enh: user sharing perms 2025-11-20 18:32:34 -05:00
Timothy Jaeryang Baek 849278ca4f refac: styling 2025-11-20 18:27:08 -05:00
b65c728208 feat: Add default group assignment for new users (#94) (#19325)
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-20 17:47:15 -05:00
Timothy Jaeryang Baek 680cde8f9b feat/enh: optional password validation 2025-11-20 17:44:49 -05:00
Timothy Jaeryang Baek e6c7495c1a refac/fix: styling 2025-11-20 17:19:59 -05:00
Timothy Jaeryang Baek 6083960655 refac: feedback list optimisation 2025-11-20 17:10:12 -05:00
Classic298andClaude 485896753d feat: Add user header information for TTS/STT requests (#93) (#19323)
Resolves #19312

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-20 16:43:22 -05:00
Blake 22e85df448 Support folder drag-n-drop (#19320) 2025-11-20 16:26:36 -05:00
Timothy Jaeryang Baek ff7a54653a refac/enh: unregisterServiceWorkers on update 2025-11-20 15:34:15 -05:00
Timothy Jaeryang BaekandShirasawa 4c28f19bdd enh/pref: convert markdown base64 images to urls
Co-Authored-By: Shirasawa <kaguyashirasawa@gmail.com>
2025-11-20 04:00:02 -05:00
Shirasawa 17ac79920f i18n: improve Chinese translation (#19309) 2025-11-20 03:51:07 -05:00
gerhardj-b 66c5b7380d feat: allow flat claims instead of nested claims as alternative (#19286) 2025-11-19 19:58:53 -05:00
Timothy Jaeryang Baek c2f889cf9a refac: add reasoning_effort to azure supported params 2025-11-19 14:59:20 -05:00
Timothy Jaeryang Baek c49bcc65c4 refac 2025-11-19 14:22:42 -05:00
Timothy Jaeryang Baek aad23e2e53 refac 2025-11-19 14:14:53 -05:00
Timothy Jaeryang Baek 17389e1b66 refac 2025-11-19 06:41:15 -05:00
Timothy Jaeryang Baek c4ecad0605 enh: revoked token handling 2025-11-19 06:08:59 -05:00
Shirasawa e486490451 i18n: improve Chinese translation (#19285) 2025-11-19 05:15:49 -05:00
Timothy Jaeryang BaekandClassic298 4bb15aa425 feat: default pinned models
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2025-11-19 05:04:03 -05:00
Timothy Jaeryang Baek 93d0b8241c refac 2025-11-19 04:44:40 -05:00
Timothy Jaeryang Baek ee26c0537e refac 2025-11-19 03:55:08 -05:00
Timothy Jaeryang Baek c2afd0d5aa refac 2025-11-19 03:54:05 -05:00
Timothy Jaeryang Baek 76dbbf57d2 refac: rm ai slop 2025-11-19 03:51:10 -05:00
Timothy Jaeryang Baek 88416161cc refac: styling 2025-11-19 03:26:10 -05:00
Timothy Jaeryang Baek a1d09eae95 chore: format 2025-11-19 03:23:33 -05:00
Timothy Jaeryang BaekandG30 3d50d5ff77 refac
Co-Authored-By: G30 <50341825+silentoplayz@users.noreply.github.com>
2025-11-19 03:21:28 -05:00
Timothy Jaeryang BaekandG30 5bec4a8005 refac
Co-Authored-By: G30 <50341825+silentoplayz@users.noreply.github.com>
2025-11-19 03:19:12 -05:00
Timothy Jaeryang Baek af1db82c7d refac: models endpoint 2025-11-19 03:18:16 -05:00
Timothy Jaeryang Baek 0c47cbd16a refac/enh: mcp oauth auth method support 2025-11-19 02:26:42 -05:00
Timothy Jaeryang Baek 76acdabdc3 chore: mcp bump 2025-11-19 02:18:59 -05:00
Timothy Jaeryang Baek 90f76d24ec refac 2025-11-19 02:16:09 -05:00
Shirasawa 31dc97b68b feat: Add image handling in middleware for delta updates (#19073)
* feat: Add image handling in middleware for delta updates

* refactor: optimize the code logic
2025-11-19 02:07:56 -05:00
Timothy Jaeryang Baek 7031bb9067 feat/enh: api keys user permission
breaking change, `ENABLE_API_KEY` renamed to `ENABLE_API_KEYS` and disabled by default and must be explicitly toggled on.
2025-11-19 01:50:52 -05:00
Adam Skalicky f89c170566 Add additional config elements to control how engineio and redis log and interact. (#19091) 2025-11-19 01:26:33 -05:00
Timothy Jaeryang BaekandG30 7e03637446 enh/feat: persist folder state
Co-Authored-By: G30 <50341825+silentoplayz@users.noreply.github.com>
2025-11-19 01:20:07 -05:00
Timothy Jaeryang Baek bbd48b3638 enh: images openai api params 2025-11-19 01:04:42 -05:00
davecrab 7762fa5ddf feat: Add adjustable text size setting to interface (#19186)
* Add adjustable text size setting to interface

Introduces a user-configurable text size (scale) setting, accessible via a slider in the interface settings. Updates CSS and Sidebar chat item components to respect the new --app-text-scale variable, and persists the setting in the store. Adds related i18n strings and ensures the text scale is applied globally and clamped to allowed values.

* Refactor text scale logic into utility module

Moved all text scale related constants and functions from components and stores into a new utility module (src/lib/utils/text-scale.ts). Updated imports and usage in Interface.svelte and index.ts to use the new module, improving code organization and reusability.

* Adjust sidebar chat scaling without extra classes

keep sidebar markup using existing Tailwind utility classes so chat items render identically pre-feature
move all text-scale sizing into app.css under the #sidebar-chat-item selectors
change the root font-size multiplier to use 1rem instead of an explicit 16px so browser/user preferences propagate

* Update Switch.svelte

Adjust toggles from fixed pixel to rem to scale with the text size

* Update Interface.svelte

Updated label from 'Text Scale' to 'UI Scale'.
Added padding around slider

* Update app.css

Added comments
2025-11-19 00:55:52 -05:00
Seth Argyle 720af637e6 fix: Use get_index() instead of list_indexes() in has_collection() to… (#19238)
* fix: Use get_index() instead of list_indexes() in has_collection() to handle pagination

Fixes #19233

  Replace list_indexes() pagination scan with direct get_index() lookup
  in has_collection() method. The previous implementation only checked
  the first ~1,000 indexes due to unhandled pagination, causing RAG
  queries to fail for indexes beyond the first page.

  Benefits:
  - Handles buckets with any number of indexes (no pagination needed)
  - ~8x faster (0.19s vs 1.53s in testing)
  - Proper exception handling for ResourceNotFoundException
  - Scales to millions of indexes

* Update s3vector.py

Unneeded exception handling removed to match original OWUI code
2025-11-19 00:19:10 -05:00
Timothy Jaeryang Baek 4386e5abb8 refac/enh: create new note 2025-11-18 18:37:57 -05:00
Jacob LeksanandTim Baek 07ef295a77 feat: Adding file metadata to hybrid search (#19095)
* Added metadata to hybrid search

* And config and env plus refac

* consistency

---------

Co-authored-by: Tim Baek <tim@openwebui.com>
2025-11-18 15:29:07 -05:00
Timothy Jaeryang Baek 42071cb8e8 refac 2025-11-18 15:27:26 -05:00
Timothy Jaeryang Baek d98b945d73 refac: styling 2025-11-18 15:23:29 -05:00
Timothy Jaeryang Baek 3f97a6993f refac: mineru api key required behaviour 2025-11-18 14:37:46 -05:00
Tom Haynes ccd80b9dba obfuscate TTS elevenlabs api key (#19262) 2025-11-18 14:36:36 -05:00
Timothy Jaeryang Baek baffa89f35 refac: styling 2025-11-18 14:27:17 -05:00
Timothy Jaeryang Baek 2af4c4b3c7 refac 2025-11-18 04:42:09 -05:00
Timothy Jaeryang BaekandClassic298 02238d3113 feat/security: Add SSRF protection with configurable blocklist
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2025-11-18 04:40:55 -05:00
lazarivandMoritz 6cdb13d5cb feat: pgvector hnsw index type (#19158)
* Adding hnsw index type for pgvector, allowing vector dimensions larger than 2000

* remove some variable assignments

* Make USE_HALFVEC variable configurable

* Simplify USE_HALFVEC handling

* Raise runtime error if the index requires rebuilt

---------

Co-authored-by: Moritz <moritz.mueller2@tu-dresden.de>
2025-11-18 04:14:43 -05:00
Timothy Jaeryang Baek 63ebc295ce refac: styling 2025-11-18 04:04:32 -05:00
Tim Baek 34684e7e58 feat/refac: group members db table (#19239)
* refac: group members table db migration

* refac: group members backend

* refac: group members frontend

* refac: group members frontend integration

* refac: styling
2025-11-18 03:59:56 -05:00
Timothy Jaeryang Baek ed6aabfbfd refac: styling 2025-11-18 03:54:19 -05:00
Timothy Jaeryang Baek 73734b186b refac: group members frontend integration 2025-11-18 03:44:26 -05:00
Classic298andClaude b4bc71d1bd feat: add granular import/export permissions for workspace items (#19242)
* feat: add granular import/export permissions for workspace items (#55)

Co-authored-by: Claude <noreply@anthropic.com>

* Fix permissions toggles not saving in EditGroupModal (#58)

Co-authored-by: Claude <noreply@anthropic.com>

* Fix permissions toggles not saving in EditGroupModal (#59)

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-17 18:25:23 -05:00
logan-hcg 0ed174f6a1 Update MCP Oauth server metadata discovery order (#19244) 2025-11-17 18:24:43 -05:00
FlorentMair80 58cff5e482 feat: add a metric to monitor daily unique users (#19236)
#19234
2025-11-17 15:31:24 -05:00
Timothy Jaeryang Baek 0a72d047ef refac: group members frontend 2025-11-17 05:09:21 -05:00
Timothy Jaeryang Baek bc576782d7 refac: group members backend 2025-11-17 05:09:06 -05:00
Timothy Jaeryang Baek f05e945a45 refac: group members table db migration 2025-11-17 05:08:45 -05:00
Timothy Jaeryang Baek e76f72576e refac 2025-11-17 05:07:59 -05:00
Timothy Jaeryang Baek f138be9d8a refac/fix: inherit model stream_response setting 2025-11-17 03:30:35 -05:00
Timothy Jaeryang Baek 3c381fad13 refac 2025-11-17 03:23:55 -05:00
Timothy Jaeryang Baek 03cc6ce8eb refac/sec: sanitize note pdf download 2025-11-16 19:17:08 -05:00
Timothy Jaeryang BaekandDaniel Pots 7c2bed2c73 fix: UserValves contamination between multiple tools
Co-Authored-By: Daniel Pots <3932988+podden@users.noreply.github.com>
2025-11-16 14:16:23 -05:00
Timothy Jaeryang Baek b1565e6913 refac 2025-11-16 13:52:45 -05:00
Timothy Jaeryang Baek ee10f372a0 refac/enh: web search domain allow/block filter 2025-11-16 13:52:09 -05:00
Davixk 21c0dd93e2 fix(images): correct config key for image edit engine (#19200)
Updates conditional to reference the appropriate configuration property for image editing, ensuring proper engine selection.
2025-11-16 13:28:43 -05:00
Classic298 a79a39bb64 Update translation.json (#19213) 2025-11-16 13:27:08 -05:00
Timothy Jaeryang Baek 31fb34918f refac/fix 2025-11-16 01:13:35 -05:00
Timothy Jaeryang Baek 80388855f4 refac/fix 2025-11-16 00:30:03 -05:00
Timothy Jaeryang Baek 8f48e96f5e refac/fix 2025-11-15 15:47:02 -05:00
Timothy Jaeryang Baek 9370b263f5 refac/fix: automatic1111 params 2025-11-15 15:43:23 -05:00
Timothy Jaeryang Baek 1b0bce529f refac 2025-11-13 20:55:34 -05:00
Timothy Jaeryang Baek bc2192e8bf refac: background image styling behaviour 2025-11-13 20:54:23 -05:00
Timothy Jaeryang Baek 4d41013804 chore: dep 2025-11-13 20:25:27 -05:00
Timothy Jaeryang Baek 8f3bd2ecbe feat: voice mode prompt template 2025-11-13 20:23:13 -05:00
Timothy Jaeryang Baek 757caeab55 chore: format 2025-11-13 20:01:21 -05:00
Timothy Jaeryang Baek 9df8d5b204 enh: copy table 2025-11-13 19:59:45 -05:00
Timothy Jaeryang Baek b160eef7eb refac: decouple api key restrictions from get user 2025-11-13 19:52:04 -05:00
Timothy Jaeryang Baek e2ff2ae252 refac 2025-11-13 19:42:32 -05:00
Timothy Jaeryang Baek a4b2dc22c4 wip: requirements-min 2025-11-13 19:24:32 -05:00
Timothy Jaeryang Baek 5d4134ba77 refac 2025-11-13 19:14:00 -05:00
Sang LêandTim Baek 64747f7f79 Add Azure Search (#19104)
Co-authored-by: Tim Baek <tim@openwebui.com>
2025-11-13 19:12:34 -05:00
Timothy Jaeryang Baek 117a33b030 fix: docling params issue 2025-11-13 17:05:30 -05:00
Mikael Schirén 7e05cf4e21 make path to audit log configurable (#19173) 2025-11-13 16:48:00 -05:00
Jeppe Kuhlmann Andersen 2bec5c5a5c Updated Danish translations (#19174) 2025-11-13 16:28:38 -05:00
Timothy Jaeryang Baek c43f95f4b8 refac: pass token_endpoint_auth_method 2025-11-13 15:34:45 -05:00
Timothy Jaeryang Baek 6d9a562edd refac: oauth pass client auth params 2025-11-13 15:30:22 -05:00
Siavash Vatanijalal 6b638db114 Updated Swedish translation (#19161)
Refined existing swedish translations and added most of the missing ones.
2025-11-13 14:39:04 -05:00
Timothy Jaeryang Baek 2de854fa02 enh: text select copy behaviour 2025-11-13 00:40:29 -05:00
Timothy Jaeryang Baek 38f45a38cb refac: chat tag suggestions behaviour 2025-11-13 00:06:32 -05:00
Timothy Jaeryang Baek 84912904fd chore: bump unstructured to 0.18.18 2025-11-12 23:41:54 -05:00
Timothy Jaeryang Baek da42850eff enh: custom headers for external tool servers 2025-11-12 23:39:27 -05:00
Timothy Jaeryang Baek 0bf686396d refac/fix 2025-11-12 23:27:03 -05:00
Timothy Jaeryang Baek f1c317349e enh/refac: enable autocompletion for non rich text input 2025-11-12 23:11:15 -05:00
xqqp 3207998114 Fix: Handle empty strings in OAuth registration response (#19144)
- The mcp package requires optional unset values to be None. If an empty string is passed, it gets validated and fails.
- Replace all empty strings with None.
2025-11-12 22:57:53 -05:00
Classic298andClaude ad17d35ac4 feat: Add custom API endpoint and user info headers for Perplexity Search (#31) (#19147)
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-12 22:53:54 -05:00
Oleg Yermolenko 8dde493e8e fix verify mcp connection with oauth type (#19149) 2025-11-12 15:05:17 -05:00
Classic298andgoogle-labs-jules[bot] 6eea0d40ab Feat: optionally disable password login endpoints (#19113)
* Implement message cleaning before API call

* Filter out empty assistant messages before cleaning

* Update catalan translation.json (#29)

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>

* Update main.py

* Update auths.py

* Update Chat.svelte

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2025-11-11 17:07:57 -05:00
Mati ce19b7120b fix: Duplicate instructions in tool selection calling prompt (#19122)
* Fix duplicated query prefix in user prompt for function calling

* Fix duplicated last user message in prompt for function calling
2025-11-11 14:35:25 -05:00
Tim Baek 4eecbf8ee4 Merge pull request #19119 from fredrikblau/feat/improve-farsi-translation
feat(i18n): fill in missing Farsi translations
2025-11-11 11:26:31 -05:00
amir ahrari cd99df870d feat(i18n): fill in missing Farsi translations 2025-11-11 15:56:04 +03:30
Tim Baek ed3744b672 Merge pull request #19084 from aindriu80/update-ga-string-10-november
i18n: Update ie-GA translation
2025-11-11 00:30:56 -05:00
Timothy Jaeryang Baek 4673e120c4 refac/fix: mineru params
breaking change
2025-11-11 00:30:11 -05:00
Timothy Jaeryang Baek 8e41fea2d9 refac: rm redundant query tag 2025-11-11 00:25:56 -05:00
Tim Baek 8bff76f745 Merge pull request #19097 from adam-skalicky/api_models_perf_optimization
perf: Fetched user_group_ids prior to looping through models with has_access to reduce DB hits for group membership
2025-11-11 00:16:14 -05:00
Timothy Jaeryang Baek 5aa2d01c17 refac/fix: rag template placeholder substitution 2025-11-11 00:08:05 -05:00
Adam Skalicky dc6e1fe6bd Fetched user_group_ids prior to looping through models with has_access to reduce DB hits for group membership 2025-11-10 16:48:56 -08:00
Aindriú Mac Giolla Eoin ce23843506 i18n - Update ie-GA translation 2025-11-10 12:14:17 +00:00
Timothy Jaeryang Baek b2667470cd refac: get event emitter/caller 2025-11-09 23:13:25 -05:00
Timothy Jaeryang Baek 62073d3b7f fix: images edit openai base url/key save issue 2025-11-09 22:36:00 -05:00
Timothy Jaeryang Baek bc739de024 refac: rerank 2025-11-09 21:33:50 -05:00
Timothy Jaeryang Baek e76f77bcb7 refac: stream chunk max buffer size 2025-11-09 21:16:34 -05:00
Timothy Jaeryang Baek 6cb41a59da refac 2025-11-09 21:10:11 -05:00
Timothy Jaeryang Baek 413fa27b18 refac 2025-11-09 21:09:59 -05:00
Tim Baek 27df461abd Merge pull request #18884 from ShirasawaSama/feature/handle-large-stream-chunks
feat: handle large stream chunks responses to support Nano Banana [Test Needed]
2025-11-09 21:08:49 -05:00
Timothy Jaeryang BaekandSihyeon Jang a65cc196a5 refac: batch file processing
Co-Authored-By: Sihyeon Jang <24850223+sihyeonn@users.noreply.github.com>
2025-11-09 21:06:21 -05:00
Tim Baek 284764e178 Merge pull request #19025 from krishna-medapati/fix-hybrid-search-17046-clean
fix: Handle AttributeError in hybrid search with reranking (#17046)
2025-11-09 20:43:52 -05:00
Timothy Jaeryang Baek 908f504885 refac 2025-11-09 20:39:05 -05:00
Timothy Jaeryang Baek e69c2cf3f6 refac 2025-11-09 16:12:38 -05:00
Timothy Jaeryang BaekandClassic298 25c7f101f2 enh: optionally add user headers external websearch
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2025-11-09 16:09:29 -05:00
Timothy Jaeryang Baek 9578bac099 refac: suggestions display full name on hover 2025-11-09 15:44:27 -05:00
Tim Baek 79fd61cd5f Merge pull request #19042 from Classic298/remove-litellm-endpoint
refactor: Remove unused litellm endpoint and associated frontend code
2025-11-08 15:12:39 -05:00
Tim Baek c13a444326 Merge pull request #19038 from rgaricano/dev-es_ES
Upd: i18n_ es-ES Spanish Translation v0.6.36
2025-11-08 15:06:29 -05:00
Tim Baek 3621e2d56c Merge pull request #19034 from Kylapaallikko/dev
i18n: Update fi-FI translation
2025-11-08 15:06:17 -05:00
google-labs-jules[bot] dec59e87a3 refactor: Remove unused litellm endpoint and associated frontend code
Removes the unused `/litellm/config` endpoint, the corresponding `downloadLiteLLMConfig` frontend API function, and the unused import from the `Database.svelte` component. This code was identified as dead code as it was not being used in the UI.
2025-11-08 15:05:29 +00:00
_00_ bebba7424e Upd: i18n_ es-ES Spanish Translation v0.6.36
### UPD Spanish Translation v0.6.35

Added new strings
2025-11-08 13:52:29 +01:00
Kylapaallikko 2e5668e25c Update fi-FI translation.json
Improved and added missing translations.
2025-11-08 13:44:52 +02:00
Adam SkalickyandTim Baek e72a7e4eca perf Optimize Socket Emits Using User Rooms (#18996)
* This PR optimizes socket delta event broadcasting by leveraging rooms. Instead of iterating through a user's sessions and emitting events individually, this change sends a single event to a user-specific room. This approach is more efficient, reducing overhead and improving performance, particularly for users with multiple concurrent sessions.

In testing this dramatically reduces emits and server load.

* Update main.py

Added userroom join

---------

Co-authored-by: Tim Baek <tim@openwebui.com>
2025-11-07 21:11:58 -05:00
Tim Baek c260efa2f4 Merge pull request #19002 from aleixdorca/dev
i18n: Update Catalan Translation File
2025-11-07 16:26:08 -05:00
Tim Baek 7afa83e880 Merge pull request #19012 from joaoback/patch-14
Update translation.json (pt-BR)
2025-11-07 16:25:04 -05:00
Tim Baek 8114ddc5f9 Merge pull request #19004 from ShirasawaSama/i18n/improve-chinese-translation
i18n: improve Chinese translation
2025-11-07 16:21:54 -05:00
krishna-medapati 684324ae9e fix: Handle AttributeError in hybrid search with reranking (#17046)
- Split attribute existence checks from document content checks
- Added hasattr() check for metadatas attribute
- Prevents AttributeError when collection_result is missing attributes
- Maintains all original validation logic

Fixes #17046
2025-11-07 23:31:11 +05:30
joaoback 3d43797361 Update translation.json (pt-BR)
New translations have been made of the new items that were added in the latest version.
2025-11-07 08:27:37 -03:00
Shirasawa ce1079d358 feat: Allow configuration of not process large single-line data 2025-11-07 07:00:06 +00:00
Shirasawa 89c0e150c8 feat: handle large stream chunks responses 2025-11-07 07:00:06 +00:00
Shirasawa 1097838b35 i18n: improve Chinese translation 2025-11-07 06:56:26 +00:00
Aleix Dorca 9a8646157e Update catalan translation.json 2025-11-07 06:44:28 +01:00
Aleix Dorca f052b2801a Update catalan translation.json 2025-11-07 06:40:37 +01:00
Tim Baek e0d5de1697 Merge pull request #18978 from open-webui/dev
0.6.36
2025-11-06 16:45:23 -05:00
Timothy Jaeryang Baek 49d57ae82b chore: format 2025-11-06 16:44:33 -05:00
Tim Baek 918e720f97 Merge pull request #18990 from EventHorizon-AI/fix/shortcuts-i18n
fix: Shortcuts Modal i18n
2025-11-06 16:42:46 -05:00
Tim Baek 261a55b275 Merge pull request #18989 from Classic298/patch-2
chore: CHANGELOG 0.6.36
2025-11-06 16:42:28 -05:00
Timothy Jaeryang Baek 0d0a37c884 chore: format 2025-11-06 16:39:07 -05:00
Classic298 a32a3dfee4 chore: CHANGELOG 0.6.36 2025-11-06 22:38:29 +01:00
Timothy Jaeryang Baek 9b3ecb703a chore: bump 2025-11-06 16:37:41 -05:00
EntropyYue e239e17050 fix: Shortcuts Modal i18n 2025-11-07 05:37:30 +08:00
Timothy Jaeryang Baek c2c02846a8 fix: tool calling 2025-11-06 16:35:19 -05:00
Tim Baek 4754108253 Merge pull request #18987 from rndmcnlly/feat/oauth-groups-separator-v2
feat: add OAUTH_GROUPS_SEPARATOR for configurable group parsing
2025-11-06 16:34:27 -05:00
Adam M. Smith 96b98cd13c feat: add OAUTH_GROUPS_SEPARATOR for configurable group parsing 2025-11-06 21:01:51 +00:00
Timothy Jaeryang BaekandGero Doll 639d26252e fix: Socket.IO CORS warning
Co-Authored-By: Gero Doll <6284675+limbicnation@users.noreply.github.com>
2025-11-06 15:21:41 -05:00
Timothy Jaeryang Baek 7faf19dad9 refac 2025-11-06 15:21:06 -05:00
Timothy Jaeryang Baek c38f878e1e fix: firecrawl import 2025-11-06 15:19:08 -05:00
Timothy Jaeryang Baek 67c4ea1e57 fix: image edit workflow editor 2025-11-06 14:17:54 -05:00
Tim Baek e85c7f7931 Merge pull request #18402 from open-webui/dev
0.6.35
2025-11-06 13:40:46 -05:00
Timothy Jaeryang Baek d5fe0f6067 refac 2025-11-06 13:31:55 -05:00
Timothy Jaeryang Baek 9c0bd0c0ed refac 2025-11-06 13:24:34 -05:00
Timothy Jaeryang Baek 651f855289 refac 2025-11-06 13:20:03 -05:00
Timothy Jaeryang Baek dc3d704800 refac: shortcuts 2025-11-06 13:08:48 -05:00
Davixk 8da4e5bb19 fix(chats): fix chat search crash (#18576)
* fix(chats): handle null bytes in PostgreSQL search

Removes null bytes from message content before performing
case-insensitive search in PostgreSQL, preventing conversion
errors and ensuring reliable query results.

* fix(chats): prevent null byte errors in PostgreSQL queries

Ensures chat content and titles containing null bytes are excluded from PostgreSQL text queries to avoid conversion errors.

Improves reliability of search and filtering by handling problematic characters in JSON fields.
2025-11-06 12:41:21 -05:00
Timothy Jaeryang Baek cabbdd719b refac 2025-11-06 12:34:30 -05:00
Timothy Jaeryang Baek 4e18c8a689 refac 2025-11-06 11:41:20 -05:00
Timothy Jaeryang Baek 224e4c3a61 chore: format 2025-11-06 03:51:26 -05:00
Classic298 d3f40c5a56 chore: Update CHANGELOG for version 0.6.35 (#18481)
* chore: Update CHANGELOG for version 0.6.35

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG with recent feature additions

* 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
2025-11-06 03:49:36 -05:00
Timothy Jaeryang Baek 7f4b45e7e8 revert/fix: edit valves modal 2025-11-06 03:48:53 -05:00
Timothy Jaeryang Baek 63e8ab7a05 feat: comfyui image edit support 2025-11-06 03:43:59 -05:00
Tim Baek 74db2b9f36 Merge pull request #18956 from Classic298/patch-2
chore: SECURITY: improved clarity and fixed layout issues
2025-11-06 03:42:56 -05:00
Classic298 e4e2f8352c Revise SECURITY.md for improved clarity
Updated security reporting guidelines for clarity and structure.
2025-11-06 08:43:56 +01:00
Tim Baek 5f3f5170b7 Merge pull request #18215 from avatsaev/fix-auto-show-artifacts
fix: auto show artifacts when opening a conversation
2025-11-06 01:49:57 -05:00
Tim Baek aed2d4a8ee Merge pull request #18955 from ShirasawaSama/i18n/improve-chinese-translation
i18n: improve Chinese translation
2025-11-06 01:48:49 -05:00
Timothy Jaeryang Baek 1cc3493dc8 enh/refac: read aloud audio queue 2025-11-06 01:48:10 -05:00
Shirasawa 3b944072e3 i18n: improve Chinese translation 2025-11-06 06:42:37 +00:00
Timothy Jaeryang BaekandClassic298 e2b9942648 feat: Optionally forward user headers to external document loader
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2025-11-06 00:05:46 -05:00
Timothy Jaeryang BaekandJacob Leksan f5c7152a6b enh/fix: update extra params for native function calling
Co-Authored-By: Jacob Leksan <63938553+jmleksan@users.noreply.github.com>
2025-11-05 23:46:30 -05:00
Tim Baek 1aa285edb9 Merge pull request #18911 from silentoplayz/fix-chat-preview-bug
fix: correct chat preview loading in chats search modal
2025-11-05 23:33:26 -05:00
Tim Baek 4daf81fba2 Merge pull request #18841 from acomarcho/fix/duplicate-prompt-suggestions-freezes-webpage
fix: duplicate prompt suggestions freezes webpage
2025-11-05 23:31:27 -05:00
Timothy Jaeryang Baek bdd198e946 a11y: message role 2025-11-05 23:30:23 -05:00
Timothy Jaeryang Baek 415b93c7c3 enh: configurable mistral ocr base url 2025-11-05 23:25:51 -05:00
Timothy Jaeryang BaekandMarchotridyo 00520a9602 fix: message input dictate
Co-Authored-By: Marchotridyo <29671825+acomarcho@users.noreply.github.com>
2025-11-05 23:06:00 -05:00
Timothy Jaeryang Baek 6c583ef9d3 refac/fix: mineru params 2025-11-05 14:43:25 -05:00
Timothy Jaeryang Baek cdf90222c7 refac 2025-11-05 03:59:09 -05:00
Timothy Jaeryang Baek 3dc20a25b1 refac: styling 2025-11-05 03:44:35 -05:00
Timothy Jaeryang Baek 5c1ba23026 refac 2025-11-05 03:42:34 -05:00
Timothy Jaeryang Baek 72f8539fd2 feat: image edit support 2025-11-05 03:31:37 -05:00
Timothy Jaeryang Baek 8d34fcb586 enh: gemini flash image generation support 2025-11-05 01:59:16 -05:00
Timothy Jaeryang Baek 72900cd686 refac: images 2025-11-05 00:54:25 -05:00
Timothy Jaeryang Baek 314cac0113 refac: dedup tags 2025-11-04 23:44:33 -05:00
Tim Baek cde4b93fa6 Merge pull request #18934 from mgl/feat/voxtral-support
feat: add voxtral support
2025-11-04 18:28:45 -05:00
Timothy Jaeryang Baek 89e59d0103 revert 2025-11-04 18:14:09 -05:00
Timothy Jaeryang Baek 2a98ba0ff8 refac/fix 2025-11-04 18:12:00 -05:00
Timothy Jaeryang Baek 75efa4f931 refac 2025-11-04 17:58:51 -05:00
mglo 3561c7eedd feat: add voxtral support 2025-11-04 20:57:36 +01:00
Timothy Jaeryang Baek a4fd26b478 enh/fix: google pse referer header 2025-11-04 13:50:07 -05:00
Timothy Jaeryang Baek 03f207b803 enh: display mcp connection error 2025-11-04 13:48:23 -05:00
Timothy Jaeryang Baek 414ab51d5d refac: user menu alignment 2025-11-04 13:45:04 -05:00
Timothy Jaeryang Baek 8e5690aab4 refac: images 2025-11-04 13:30:59 -05:00
Tim Baek 939521b75d Merge pull request #18912 from silentoplayz/fix-functions-page-delete
fix: update Functions page after deleting a function
2025-11-04 13:30:23 -05:00
Timothy Jaeryang Baek a8fe979cf8 refac 2025-11-04 13:20:13 -05:00
Timothy Jaeryang Baek bafeb76c41 refac/fix: trusted env for proxy 2025-11-04 12:21:18 -05:00
Timothy Jaeryang Baek ec21577f1a Merge branch 'dev' of https://github.com/open-webui/open-webui into dev 2025-11-03 23:47:45 -05:00
silentoplayz 244809bab7 fix: update Functions page after deleting a function
The Functions page was not updating after a function was deleted. This was because the local `functions` variable was not being updated after a successful deletion.

This commit fixes the bug by filtering the deleted function from the local `functions` array, which triggers a UI refresh.
2025-11-03 21:03:27 -05:00
silentoplayz 71b86c08ee fix: correct chat preview loading in search modal
This commit fixes a bug in the search modal where the chat preview would fail to load for the bottom search results, especially when using tags to filter.

The issue was caused by an incorrect index calculation in the `loadChatPreview` function, which resulted in an out-of-bounds error when accessing the `chatList` array.

This commit resolves the issue by adding a guard clause to the `loadChatPreview` function to ensure that the `selectedChatIdx` is always a valid index. This prevents the out-of-bounds error and ensures that the chat preview is always displayed for the selected chat.
2025-11-03 19:58:26 -05:00
Timothy Jaeryang Baek 989f192c92 enh: force refresh page on update 2025-11-03 13:43:07 -05:00
Tim Baek 08bc00ea77 Merge pull request #18879 from ShirasawaSama/i18n/improve-chinese-translation
i18n: improve Chinese translation
2025-11-03 09:47:08 -05:00
Shirasawa 1447102331 i18n: improve Chinese translation 2025-11-03 02:59:13 +00:00
Timothy Jaeryang Baek 67aa1b028d refac/fix: note export 2025-11-02 19:40:44 -05:00
Timothy Jaeryang Baek 024f78d3e0 refac/fix: reasoning content tag parsing 2025-11-02 18:52:53 -05:00
Timothy Jaeryang Baek d14329b285 refac 2025-11-02 18:47:41 -05:00
Timothy Jaeryang Baek 6681ff5cbd refac/fix: endpoint conflict 2025-11-02 18:41:57 -05:00
Tim Baek 478163eb3b Merge pull request #18867 from andrewbbaek/dev
chore: Add id attributes for better element identification
2025-11-02 11:51:35 -05:00
Andrew Baek 9e8e004929 Add id attributes for better element identification
Updated ResponseMessage.svelte

Update Sidebar.svelte
2025-11-03 01:46:50 +09:00
Tim Baek a9b4774bde Merge pull request #18847 from IllimarR/main
i18n: update Estonian translation file
2025-11-01 18:49:11 -04:00
Timothy Jaeryang Baek d98c539d89 refac 2025-11-01 18:48:11 -04:00
Timothy Jaeryang Baek 20af60be42 refac 2025-11-01 16:00:11 -04:00
IllimarR 00eacfcacc Merge branch 'dev' into main 2025-11-01 16:05:35 +02:00
IllimarR cf6a476998 Update translation.json (Estonian translations) 2025-11-01 16:00:35 +02:00
Timothy Jaeryang Baek fdf7ca15ea refac 2025-11-01 06:07:00 -04:00
Timothy Jaeryang Baek ee61970fb0 refac 2025-11-01 05:58:54 -04:00
Tim Baek 43f817a75e Merge pull request #18751 from ShirasawaSama/i18n/improve-chinese-translation
i18n: improve Chinese translation
2025-11-01 02:57:11 -07:00
Timothy Jaeryang Baek fdc0f04a36 refac 2025-11-01 05:56:44 -04:00
Tim Baek 7fc4c56ea3 Merge pull request #18761 from acomarcho/fix/chat-title-not-generated-if-title-auto-generation-is-off
fix: chat title not generated if title auto generation is off in interface settings
2025-11-01 02:46:14 -07:00
acomarcho da282ce5c7 fix: duplicate prompt suggestions freezes webpage 2025-11-01 11:25:20 +07:00
Tim Baek c787070dc9 Merge pull request #18765 from mkhludnev/patch-2
fix: Don't missguide Tika with mime-type
2025-10-31 14:18:43 -07:00
Mikhail Khludnev 24aeec9120 Don't missguide Tika with mime-type
Fix #18683 
* Tika is smart enough to detect content type. 
* Windows browsers just misguides Tika providing `application/ms-word` for .rtf files.
2025-10-31 23:12:23 +03:00
acomarcho 15cc9b6cee fix: titles are not generated if title auto-generation is set to false in interface settings 2025-11-01 00:07:59 +07:00
Shirasawa 9f837267b6 i18n: improve Chinese translation 2025-10-31 12:03:20 +00:00
Tim Baek f18f1db704 Merge pull request #18707 from silentoplayz/fix-tool-tooltip-description
fix: add tooltips to tools on model edit page
2025-10-30 16:23:36 -07:00
Timothy Jaeryang Baek 1d98a45b33 refac 2025-10-30 15:57:50 -07:00
Tim Baek 16af088f4e Merge pull request #18714 from OAburub/patch
fix: make SSL Verificiation async
2025-10-30 10:47:21 -07:00
G30 8feed02d40 fix: de-duplicate model tags case-insensitively (#18716)
* fix: de-duplicate model tags case-insensitively

This change updates the `setTags` function in the `Models.svelte` component and the `onMount` function in the `Selector.svelte` component to convert all model tags to lowercase before removing duplicates. This ensures that tags with different capitalization (e.g., "Best" and "best") are treated as a single tag, preventing duplicate entries in the tag filter dropdown on both the workspace models page and the new chat page.

* Update Selector.svelte

* refac
2025-10-30 10:44:05 -07:00
Omar Aburub 3bcf9a442a fix: make SSL verification async 2025-10-29 16:14:53 +03:00
silentoplayz 4a0359789f fix: add tooltips to tools on model edit page
This change adds tooltips to the tools on the model edit page, displaying the tool's description on hover.
2025-10-28 21:54:29 -04:00
Timothy Jaeryang Baek 292be82754 refac/fix: sidebar open status 2025-10-28 17:31:38 -07:00
Timothy Jaeryang Baek a0068c4a17 refac 2025-10-28 14:05:49 -07:00
Timothy Jaeryang Baek 61a2909a88 refac 2025-10-28 14:03:05 -07:00
Tim Baek 48b538f312 Merge pull request #18432 from silentoplayz/fix-clipboard-image-paste
fix: correctly handle clipboard images with {{CLIPBOARD}} in prompts
2025-10-28 12:42:01 -07:00
Timothy Jaeryang Baek d1c9555a0b refac 2025-10-28 01:46:36 -07:00
Timothy Jaeryang Baek f524a6a8e7 refac/fix: kb image upload handling 2025-10-28 00:34:53 -07:00
Timothy Jaeryang Baek e986488ab5 enh: ELEVENLABS_API_BASE_URL env var 2025-10-28 00:06:52 -07:00
Tim Baek 81c530a9c4 Merge pull request #18667 from wangweixuan/fix-7008
fix: use trusted env proxy setting in web search loader
2025-10-27 23:52:20 -07:00
Timothy Jaeryang Baek 76bde402fe refac 2025-10-27 23:49:26 -07:00
Timothy Jaeryang Baek a776dbd01d refac 2025-10-27 23:42:00 -07:00
Timothy Jaeryang Baek b9bbf22581 refac 2025-10-27 16:46:49 -07:00
Timothy Jaeryang Baek cbcab062eb refac 2025-10-27 16:46:04 -07:00
Timothy Jaeryang Baek c8b2313362 refac 2025-10-27 15:38:59 -07:00
Timothy Jaeryang Baek 92aafd6c06 refac 2025-10-27 15:31:25 -07:00
Tim Baek bfadbc9934 Merge pull request #18415 from taylorwilsdon/oauth_error_handling_enh
enh: More detailed OAuth2.1 tool callback error handling + fix for editing existing tools
2025-10-27 15:13:33 -07:00
Tim Baek f0834e397c Merge pull request #18648 from itk-dev/feature/danish-translations-added
i18n danish translation added
2025-10-27 15:10:00 -07:00
Timothy Jaeryang Baek 182e4138bf refac: styling 2025-10-27 15:07:00 -07:00
Timothy Jaeryang Baek b72443004d refac/chore: svelte 5 migration 2025-10-27 15:06:53 -07:00
Wang Weixuan 5e17882488 fix: use trusted env in web search loader
Signed-off-by: Wang Weixuan <wangweixvan@gmail.com>
2025-10-28 04:58:00 +08:00
sinejespersen b2ed5be457 add danish translation 2025-10-27 09:11:31 +01:00
Timothy Jaeryang Baek f47214314b refac 2025-10-26 22:20:57 -07:00
Timothy Jaeryang Baek 46a3f7de5e refac: rm rich text highlight 2025-10-26 22:12:34 -07:00
Timothy Jaeryang Baek a996497bf1 refac/fix: marker api key validation 2025-10-26 22:11:47 -07:00
Timothy Jaeryang Baek 5338edd644 refac 2025-10-26 21:35:16 -07:00
Timothy Jaeryang Baek c573321305 refac 2025-10-26 21:11:05 -07:00
Timothy Jaeryang Baek 20cd9e9461 chore: svelte5 bump 2025-10-26 21:06:32 -07:00
Tim Baek 69e1ba8234 Merge pull request #18533 from silentoplayz/fix-keyboard-shortcuts-display
fix: display correct keys for international keyboards
2025-10-26 19:34:13 -07:00
Timothy Jaeryang Baek a70bc52c34 chore: format 2025-10-26 19:33:39 -07:00
Timothy Jaeryang Baek 54c05ac6e0 refac: swtich 2025-10-26 19:25:38 -07:00
Timothy Jaeryang Baek ed6449d35f refac: chat navbar menu 2025-10-26 19:23:55 -07:00
Timothy Jaeryang Baek 82c08a3b5d enh: sidebar models collapsible 2025-10-26 19:00:27 -07:00
Timothy Jaeryang Baek 8197844ff7 refac 2025-10-26 17:22:23 -07:00
Tim Baek 0c4a1ac54d Merge pull request #18635 from wei840222/dev
refactor: replace requests with Firecrawl SDK in search and requests Firecrawl SDK in scrape rather than langchain_community FireCrawlLoader
2025-10-26 14:57:10 -07:00
Tim Baek c9465da8f2 Merge pull request #18636 from rgaricano/dev-FIXSTYLES_dark_mode_select_boxes
UPD: Refactor dark select styles using Tailwind CSS classes
2025-10-26 14:56:05 -07:00
_00_ 2f6a050325 Refactor dark mode select styles-more specific
Refactor dark mode select styles to be more specific and avoid interference with already classed select elements.
2025-10-26 15:05:42 +01:00
_00_ 235ed8956c UPD: Refactor dark select styles using Tailwind CSS classes
### UPD_Styles: Add dark mode styles for select elements and options.

Actually some select "boxes" have css dark theme support, but other not.
This PR add CSS for dark theme selects.
2025-10-26 09:40:05 +01:00
wei840222 7a3f4d85f6 refactor: replace requests with Firecrawl SDK in search and requests Firecrawl SDK in scrape rather than langchain_community FireCrawlLoader 2025-10-26 15:05:35 +08:00
Timothy Jaeryang Baek d11d49a08a refac 2025-10-25 23:01:13 -07:00
Tim Baek a4d0bd1073 Merge pull request #18537 from OAburub/patch
fix: prevent cancellation scope corruption by exitting in LIFO and ha…
2025-10-25 22:47:40 -07:00
Tim Baek f83dc60666 Merge pull request #18572 from attilaolah/issue-18542
feat: OAUTH_ROLES_SEPARATOR env var
2025-10-25 22:47:07 -07:00
Tim Baek 82c45b721e Merge pull request #18564 from iPagar/empty-names
fix: validate folder and channel names before creation
2025-10-25 22:30:53 -07:00
_00_ f06e2c1a4a FIX:style_dark_mode_select_boxes
### UPD_Styles: Add dark mode styles for select elements and options.

Actually some select "boxes" have css dark theme support, but other not.
This PR add CSS for dark theme selects.
2025-10-25 13:35:58 +02:00
Tim Baek 46a8e4acad Merge pull request #18594 from iPagar/chat-system-trim
fix: prevent icon layout shift near system instructions in Chat component
2025-10-24 17:50:29 -07:00
Tim Baek 06e280d831 Merge pull request #18577 from Classic298/fix-global-activated-actions
fix: global activated actions
2025-10-24 17:49:25 -07:00
Tim Baek 1f287be27f Merge pull request #18585 from silentoplayz/fix-ui-freeze-on-mixed-sources
fix: prevent UI freeze by initializing distances array
2025-10-24 17:47:34 -07:00
Tim Baek 5444210163 Merge pull request #18591 from iPagar/modal
fix: add stable scrollbar gutter to Modal component
2025-10-24 17:47:08 -07:00
Tim Baek d28559c49c Merge pull request #18595 from iPagar/menu-gaps-patch
fix: update spacing in UserMenu dropdown items
2025-10-24 17:45:31 -07:00
Tim Baek c7df4c5082 Merge pull request #18602 from htulipe/patch-1
i18n: fix french translation key
2025-10-24 17:44:17 -07:00
htulipe c0fc37d112 Update translation.json 2025-10-24 19:28:21 +02:00
Pavel Garaev 79b0c3af47 refac: update spacing in UserMenu dropdown items 2025-10-24 20:27:34 +04:00
Pavel Garaev 6471945076 fix: conditionally render system instructions in Chat component 2025-10-24 19:56:01 +04:00
Pavel Garaev 17e80ecd81 fix: add stable scrollbar gutter to Modal component 2025-10-24 19:26:31 +04:00
silentoplayz 7222a9aef6 fix: prevent UI freeze by initializing distances array
Fixes a bug where the UI would freeze when processing citation sources with mixed distance metrics.

The `Citations.svelte` component was attempting to call `.push()` on an `undefined` `distances` array. This happened when the first document for a source had no distance value, but a subsequent document for the same source did.

This patch ensures the `distances` array is always initialized as an empty array `[]` instead of `undefined`, preventing the `TypeError` and resolving the UI freeze.
2025-10-24 08:53:51 -04:00
Classic298 006a2d6bb3 fix: Modify ActionsSelector to handle global action states
Updated checkbox behavior to account for global actions.
2025-10-24 11:46:15 +02:00
Attila Oláh c165a6b6c2 fix: exclude empty roles
This is a minor tweak that allows using whitespace as a separator,
without it having to be exactly one space. Convenient for using YAML
text fold syntax in Helm charts when providing long lists of roles.
2025-10-24 08:48:57 +02:00
Attila Oláh 35504e8486 feat: add OAUTH_ROLES_SEPARATOR env var
This allows changing the separator for the `OAUTH_ALLOWED_ROLES` and
`OAUTH_ADMIN_ROLES` env vars, from the default comma (,) to something
that is not present in the role name. The intended audience is folks
with LDAP-syntax groups/roles, e.g.
`cn=webui_admin,ou=it_department,o=my_org` instead of just
`webui_admin`.
2025-10-24 08:42:28 +02:00
Timothy Jaeryang Baek 267794638c doc: readme 2025-10-23 18:05:22 -07:00
Pavel Garaev e0e50f7380 fix: validate folder and channel names before creation 2025-10-24 02:18:26 +04:00
Timothy Jaeryang Baek e171c7915a refac: styling 2025-10-23 12:48:48 -04:00
Omar Aburub 8f060ee2fa fix: prevent cancellation scope corruption by exitting in LIFO and handling exceptions 2025-10-23 15:34:47 +03:00
silentoplayz 02a2683eb0 fix: display correct keys for international keyboards
Updates the ShortcutsModal to dynamically display the correct physical keys for users with non-US keyboard layouts.

The `ShortcutItem` component now uses `navigator.keyboard.getLayoutMap()` to resolve `KeyboardEvent.code` values (e.g., "Slash") to the character they produce on the user's active keyboard layout (e.g., "-").

This ensures the displayed shortcuts match the keys the user needs to press. A fallback is included for older browsers that do not support this API.
2025-10-23 05:40:31 -04:00
Timothy Jaeryang Baek 6eecade06e refac 2025-10-22 22:35:39 -04:00
Timothy Jaeryang Baek 546a334328 refac 2025-10-22 17:12:05 -04:00
Timothy Jaeryang Baek 8f5eb03a40 refac: styling 2025-10-22 17:12:00 -04:00
Tim Baek 710f6eec12 Merge pull request #18473 from silentoplayz/hotkey-hints-sidebar
feat: add toggleable hotkey hints to sidebar buttons and refac ShortcutsModal
2025-10-22 16:50:24 -04:00
Tim Baek 92f359fb9b Merge pull request #18506 from duncansmart/patch-1
perf: compressImage - preserve image type
2025-10-22 16:46:15 -04:00
Duncan Smart fe192eb738 compressImage: preserve image type
Previously always exported as PNG, causing JPEG images to balloon 3-10x in size with considerable implications for front-end performance (UI gets sluggish e.g. https://github.com/open-webui/open-webui/discussions/11941)
2025-10-22 01:16:26 +01:00
Timothy Jaeryang Baek 23ea754061 fix: create new note 2025-10-21 18:21:00 -04:00
silentoplayz 3c7e739b3c refac 2025-10-21 18:11:56 -04:00
Timothy Jaeryang Baek 6593b7ccc8 refac 2025-10-21 18:11:30 -04:00
Timothy Jaeryang Baek eb9c4c0e35 refac/fix: insert prompt as rich text 2025-10-21 17:59:52 -04:00
Timothy Jaeryang Baek 9942de8011 refac/fix: mermaid 2025-10-21 17:03:04 -04:00
Timothy Jaeryang Baek 0a78ceef6d fix: dictate autosend 2025-10-21 16:40:22 -04:00
Tim Baek b68a5f330d Merge pull request #18496 from ShirasawaSama/patch-44
fix: Clear file list when selected models do not support file uploads
2025-10-21 16:36:05 -04:00
Tim Baek b9f0d239b0 Merge pull request #18484 from ShirasawaSama/patch-41
fix: do not display the move menu when folders are empty
2025-10-21 16:26:43 -04:00
Tim Baek a708cdf55e Merge pull request #18483 from ShirasawaSama/patch-40
fix: fix color of Attach Webpage button when model not support file uploads
2025-10-21 16:26:27 -04:00
Tim Baek bc9067d5aa Merge pull request #18489 from rgaricano/dev-es_ES
UPD_i18n:  es-ES Translation v0.6.35
2025-10-21 16:24:57 -04:00
Tim Baek 9c9085adfa Merge pull request #18487 from ShirasawaSama/patch-42
fix: Do not display divider when no integrations enabled
2025-10-21 16:24:52 -04:00
Tim Baek 8cbc472f7f Merge pull request #18491 from ShirasawaSama/i18n/improve-chinese-translation
i18n: improve Chinese translation
2025-10-21 16:24:26 -04:00
Shirasawa 1be9187236 fix: Clear file list when selected models do not support file uploads 2025-10-21 20:47:27 +08:00
Shirasawa 098e64b35d i18n: improve Chinese translation 2025-10-21 10:16:47 +00:00
_00_ c469369c35 UPD_i18n: es-ES Translation v0.6.35
### UPDATE of es-ES Translation
v0.6.35

Added new strings
2025-10-21 11:43:12 +02:00
Shirasawa da89e36abe fix: Do not display divider when no integrations enabled 2025-10-21 16:46:18 +08:00
Shirasawa b754aad987 fix: do not display the move menu when folders are empty 2025-10-21 16:14:38 +08:00
Shirasawa e225435c8e fix: fix color of Attach Webpage button when model not support file uploads 2025-10-21 16:05:56 +08:00
silentoplayz e361606c61 feat: add toggleable hotkey hints to sidebar buttons and refac ShortcutsModal
- Add src/lib/shortcuts.ts as the single source of truth for every shortcut
- Create HotkeyHint.svelte to show OS-aware key combos (⌘ on Mac, Ctrl elsewhere)
- Make sidebar “New Chat” and “Search” buttons display their shortcuts on hover
- Add user setting to toggle these sidebar hints
- Refactor ShortcutsModal into categorized (Global, Chat, Message, Input), data-driven sections
- Introduce ShortcutItem.svelte to render each row, dividers, and multi-line text
- Fix “Focus text area” action and include “Close modal” shortcut
- Wire everything through +layout.svelte and the shortcuts registry
2025-10-20 16:21:59 -04:00
Tim Baek e24fec0de4 Merge pull request #18452 from kaiwinut/fix/chat-getattr-error
fix: Handle invalid order_by attribute in Chat
2025-10-20 14:00:21 -04:00
Tim Baek 1f89eacb69 Merge pull request #18455 from Classic298/patch-1
chore: Revise feature request template for clarity
2025-10-20 11:24:07 -05:00
Classic298 892ddf9eac Revise feature request template for clarity
Updated the feature request template to clarify scope and submission guidelines.
2025-10-20 08:38:43 +02:00
kaiwdev 2b0b87c0f9 fix: handle invalid order_by attribute in Chat 2025-10-20 14:19:02 +09:00
Timothy Jaeryang Baek f5899e875c refac/fix: 72 bytes+ password 2025-10-20 01:18:03 -04:00
Timothy Jaeryang Baek 43eac35b5b refac: code block stdout/err styling 2025-10-20 01:11:19 -04:00
Tim Baek 9aea08ccb2 Merge pull request #18389 from Classic298/docs-chore
chore: Updated various templates to fix typos and improve them
2025-10-19 23:50:34 -05:00
Timothy Jaeryang Baek 3e003a5f17 refac 2025-10-20 00:48:01 -04:00
Timothy Jaeryang Baek acac6d5973 fix/refac: docling 2025-10-20 00:27:50 -04:00
Tim Baek 2a8b0b2581 Merge pull request #18388 from Ithanil/de_i18n
i18n: German translation of new strings
2025-10-19 23:22:55 -05:00
Tim Baek a2b3abfa14 Merge pull request #18385 from ShirasawaSama/patch-39
i18n: improve zh-TW translation
2025-10-19 23:22:45 -05:00
Tim Baek 32996a16cc Merge pull request #18449 from open-webui/main
dev
2025-10-19 23:18:24 -05:00
Tim Baek 3af6192495 Merge pull request #18384 from ShirasawaSama/patch-38
i18n: improve zh-CN translation
2025-10-19 22:45:34 -05:00
Tim Baek 7a83e7dfa3 Merge pull request #18395 from EkaterinePapava/main
Update Georgian translation
2025-10-19 22:45:09 -05:00
Tim Baek 3984184a82 Merge pull request #18411 from ricdikulous/feat/websocket-cors-security
feat: add CORS validation to WebSocket connections for defense-in-depth
2025-10-19 22:44:55 -05:00
Timothy Jaeryang Baek ca5bafcd2f refac 2025-10-19 23:41:25 -04:00
Timothy Jaeryang Baek ca332db7eb refac 2025-10-19 23:35:59 -04:00
Tim Baek 680361a88c Merge pull request #18419 from ivanostanin/fix-youtube-proxy-passing
fix: pass youtube_proxy as a `GenericProxyConfig` type object
2025-10-19 22:32:37 -05:00
Tim Baek 375d29bab4 Merge pull request #18438 from acwoo97/fix/load-defaults
fix: load default settings when sessionStorage is empty
2025-10-19 22:26:21 -05:00
Chanwoo An 9784eb4c0b fix: load default settings when sessionStorage is empty 2025-10-20 11:52:29 +09:00
silentoplayz 0031fb8274 fix: correctly handle clipboard images in prompts
The textVariableHandler was using URL.createObjectURL() for clipboard images, which created a blob URL instead of the required base64-encoded data URL. This caused an "illegal base64 data" error when sending messages with images pasted via a {{CLIPBOARD}} prompt.

This commit updates the handler to use FileReader.readAsDataURL() to properly encode the image, aligning it with the existing on:paste logic. Additionally, it adds error handling for navigator.clipboard.read() to address potential permission issues in Firefox.
2025-10-19 20:15:39 -04:00
Timothy Jaeryang Baek 8af6a4cf21 refac: direct connections 2025-10-19 18:30:36 -04:00
Taylor Wilsdon 4b74034967 black fmt 2025-10-19 16:58:09 -04:00
Timothy Jaeryang Baek 9762ef3ef6 refac 2025-10-19 12:53:34 -04:00
Tim Baek 691012782a Merge pull request #18413 from zhsh-12/fix-manifest-duplicate-crossorigin
Fix manifest duplicate crossorigin
2025-10-19 11:42:49 -05:00
Tim Baek f503ba499c Merge pull request #18424 from athoik/bump_python_jose
build(deps): bump python-jose from 1.4.0 to 1.5.0
2025-10-19 11:42:23 -05:00
Tim Baek d6915c0f40 Merge pull request #18430 from athoik/bump_unstructured
build(deps): bump unstructured from 0.16.17 to 0.18.15
2025-10-19 11:42:10 -05:00
Athanasios Oikonomou f39f29c38f build(deps): bump unstructured from 0.16.17 to 0.18.15
This PR updates the unstructured package from version 0.16.17 to 0.18.15 to enable installation and compatibility with Python 3.13.

More info #18349
2025-10-19 17:35:12 +03:00
Athanasios Oikonomou 6a0300fdb7 build(deps): bump python-jose from 1.4.0 to 1.5.0
Upgraded `python-jose` from **1.4.0** to **1.5.0** to ensure compatibility with newer Python versions and apply security improvements.
- Adds explicit support for Python 3.12 and 3.13.
- Improves cryptographic backend handling.
- Includes bug fixes and security enhancements (e.g., removes sensitive data from exceptions).

- [python-jose 1.5.0 Release Notes](https://github.com/mpdavis/python-jose/releases)
2025-10-19 12:45:12 +03:00
Ivan Ostanin 7d29991fa5 fix: pass youtube_proxy as a GenericProxyConfig type object 2025-10-19 02:01:27 +02:00
Taylor Wilsdon ecbf74dbea Added a preflight authorize check that automatically re-registers MCP OAuth clients when the stored client ID no longer exists on the server, so the browser flow never hits the stale-ID failure 2025-10-18 16:53:44 -04:00
Taylor Wilsdon d49fb9c010 complete cleanup of oauth clients 2025-10-18 14:16:10 -04:00
Taylor Wilsdon c107a3799f Added a targeted utility to wipe all OAuth sessions for a provider so the cleanup can remove stale access tokens across every user when a connection is updated 2025-10-18 14:00:46 -04:00
Taylor Wilsdon 40c450e6e5 Add more granular information to oauth failure messages 2025-10-18 13:43:51 -04:00
zhsh-12 6cb58af3db feat: add new feature or fix bug 2025-10-18 22:06:10 +08:00
Richard Watts-Seale 25087e09e6 feat: Add CORS validation to WebSocket connections. #18410 2025-10-18 20:25:45 +11:00
Classic298 d347497609 Merge branch 'dev' into docs-chore 2025-10-18 11:12:36 +02:00
Classic298 ab07bab140 Clarify PR guidelines for translation contributions
Emphasize the importance of standalone PRs for translation updates.
2025-10-18 11:11:51 +02:00
Tim Baek f97e73962a Merge pull request #18394 from joaoback/patch-13
Update translation.json (pt-BR)
2025-10-17 12:42:17 -05:00
joaoback c0a91b566b Update translation.json (pt-BR)
translations of the new items added in the last version have been carried out.
2025-10-17 08:29:01 -03:00
Ekaterine Papava 850ca01ca2 Update Georgian translation 2025-10-17 12:56:31 +02:00
Classic298 a483d41de2 Patch 1 (#22) 2025-10-17 11:32:17 +02:00
Jan Kessler 185e01eecc German translation of new strings in i18n 2025-10-17 11:14:55 +02:00
Shirasawa 7c393bc166 i18n: improve zh-TW translation 2025-10-17 08:42:17 +00:00
Shirasawa f170f37ba4 i18n: improve zh-CN translation 2025-10-17 16:29:53 +08:00
Tim Jaeryang Baek 27e85e448b Merge pull request #18372 from Tsafaras/i18n/more-greek-translations
i18n: Add more Greek translations
2025-10-16 16:46:39 -05:00
Konstantinos eff40229fe i18n: add more greek translations 2025-10-16 21:41:02 +03:00
Tim Jaeryang Baek 9ae06a3cac Merge pull request #18138 from open-webui/dev 2025-10-16 11:55:47 -05:00
Classic298 2783e0eb89 chore: Update CHANGELOG.md (#18199)
* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG for version 0.6.34

Updated changelog for version 0.6.34 with new features, fixes, and improvements.

* Woops
2025-10-16 11:44:37 -05:00
Timothy Jaeryang Baek efed0e3f63 fix: system prompt perm 2025-10-16 11:40:06 -05:00
Timothy Jaeryang Baek fd0e9652a8 chore: format 2025-10-16 11:36:26 -05:00
Timothy Jaeryang Baek d0da1d722c chore: i18n 2025-10-16 11:34:59 -05:00
Timothy Jaeryang Baek b48c790a76 chore: bump 2025-10-16 11:34:15 -05:00
Tim Jaeryang Baek 417c9d923a Merge pull request #18263 from silentoplayz/chat-deletion-bug
fix: reword misleading knowledge base warning in documents settings
2025-10-16 11:33:45 -05:00
Tim Jaeryang Baek e8c1dbb2da Merge pull request #18306 from palazski/main
feat: add mineru as document parser backend with support of both local and managed api
2025-10-16 10:49:29 -05:00
Tim Jaeryang Baek 2bd972305a Merge pull request #18361 from teephopdisawas/dev
Improved the Thai translations to match more with the contexts they're being used for.
2025-10-16 10:42:27 -05:00
Tim Jaeryang Baek 9b79486316 Merge pull request #18369 from taylorwilsdon/issues/11782
enh: restore visible scrollbar
2025-10-16 10:40:16 -05:00
Taylor Wilsdon fbeff475cf restore visible scrollbar 2025-10-16 09:05:17 -04:00
teephopdisawas 0dc04fd8b9 Fix Thai translations for consistency
Improve the translations to match more with the contexts.
2025-10-16 11:05:20 +07:00
palazski 288b323df8 feat: use MINERU_PARAMS json field for mineru settings 2025-10-15 22:59:59 +03:00
Tim Jaeryang Baek 29d4148fe9 Merge pull request #18344 from Classic298/patch-1
chore: feat template
2025-10-15 14:30:41 -05:00
Tim Jaeryang Baek 62e57fe32c Merge pull request #18346 from cubukcum/main
i18n: update Turkish translations for various UI elements
2025-10-15 11:46:52 -05:00
Mehmet Cubukcu 9dc7889493 update Turkish translations 2025-10-15 14:36:21 +03:00
Classic298 aa5de4c05b chore 2025-10-15 08:09:15 +02:00
Timothy Jaeryang Baek c46ea40f70 refac 2025-10-14 19:06:55 -05:00
silentoplayz ffe127e0d6 remove "Note:" 2025-10-14 19:36:43 -04:00
Timothy Jaeryang Baek bcb7b65553 chore: authlib bump 2025-10-14 18:36:28 -05:00
Tim Jaeryang Baek dd51808a6f Merge pull request #18258 from petrkrapek/community
i18n: Update Czech translation
2025-10-14 18:35:13 -05:00
Tim Jaeryang Baek 94806555bf Merge pull request #18261 from Classic298/patch-1
enh: lower JWT expiration default value and add warn message
2025-10-14 18:35:03 -05:00
Tim Jaeryang Baek 7a68f065fa Merge pull request #18260 from silentoplayz/fix-toast-click-issue
fix: allow toast notifications to be closed when a modal is open
2025-10-14 18:33:30 -05:00
Timothy Jaeryang Baek ad61460faf fix: tool server connection ui 2025-10-14 18:32:50 -05:00
Timothy Jaeryang BaekandClassic298 91a43848a1 enh: jwt_expires_in security warning
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2025-10-14 18:32:40 -05:00
Tim Jaeryang Baek 2a34a0ae10 Merge pull request #18273 from Cyp9715/dev
i18n: Korean translation updated
2025-10-14 18:24:34 -05:00
Timothy Jaeryang Baek fcc2bb5a05 refac: oracle23ai 2025-10-14 18:22:48 -05:00
Tim Jaeryang Baek 7031708c87 Merge pull request #18284 from Stoyan-Zlatev/feature/knowledge-response-file-hash
feat: Extend FileMetadataResponse to have hash field
2025-10-14 18:15:26 -05:00
Timothy Jaeryang Baek 8039cc40f7 refac/fix: support delete method for openapi tool servers 2025-10-14 18:12:19 -05:00
Tim Jaeryang Baek 515e136502 Merge pull request #18324 from ShirasawaSama/patch-37
fix: Fix missing model auto-pull when user settings are unmodified
2025-10-14 18:07:32 -05:00
Timothy Jaeryang Baek 5fe56a862b fix: pinned chats in ref chat 2025-10-14 18:06:29 -05:00
Tim Jaeryang Baek 9971919ca1 Merge pull request #18320 from BoFFire/patch-2
i18n : Kabyle translation
2025-10-14 15:01:33 -05:00
Shirasawa f0689b260a fix: Fix missing model auto-pull when user settings are unmodified 2025-10-14 11:43:51 +00:00
ButterflyOfFire 7ba752e2aa Update translation.json 2025-10-14 09:10:24 +01:00
Tim Jaeryang Baek 2b3b2e4aa6 Merge pull request #18310 from silentoplayz/fix-integrations-menu-closing-outside-click
fix: prevent integrations menu from closing when valves modal is open
2025-10-13 20:58:58 -05:00
silentoplayz 4b160d88a2 fix: prevent integrations menu from closing when valves modal is open
When the "Valves" modal is opened from the "Integrations" menu, a click outside the modal would incorrectly close the integrations menu first. This was because the dropdown's outside click handler was still active.

This commit fixes the issue by introducing a `closeOnOutsideClick` prop to the `Dropdown` component. This prop is controlled by the `MessageInput` component, which now disables the outside click handler on the integrations menu when the valves modal is open, and re-enables it when the modal is closed.
2025-10-13 18:13:08 -04:00
Timothy Jaeryang Baek a730a277b9 refac 2025-10-13 16:22:47 -05:00
palazski 40e9d9c330 feat: add mineru as document parser support with both local and managed api 2025-10-13 21:09:52 +03:00
Tim Jaeryang Baek f373784aaf Merge pull request #18266 from Classic298/patch-4
chore: expand SECURITY.MD once again (hopefully the last time)
2025-10-13 11:13:10 -05:00
Tim Jaeryang Baek 43d64b3bf8 Merge pull request #18267 from aleixdorca/dev
i18n: Update catalan translation.json
2025-10-13 11:11:17 -05:00
Stoyan Zlatev f0dfdb30c3 Extend FileMetadataResponse to have hash field 2025-10-13 12:38:10 +03:00
Cyp cf76922a8f Korean translation updated 2025-10-13 11:29:03 +09:00
Classic298 e41836f8bd Update SECURITY.md 2025-10-12 17:24:38 +02:00
Classic298 0417a456c3 Update SECURITY.md 2025-10-12 17:23:23 +02:00
Aleix Dorca 2f8685d0b7 Update catalan translation.json 2025-10-12 17:08:47 +02:00
Classic298 3fc29b292c chore: expand SECURITY.MD once again 2025-10-12 17:08:13 +02:00
silentoplayz e7476350e3 fix: reword misleading knowledge base warning 2025-10-12 07:49:38 -04:00
Classic298 31f6520ba9 enh: lower JWT expiration default value and add warn message 2025-10-12 12:33:35 +02:00
silentoplayz 1e6426ed4f fix: allow toast notifications to be closed when a modal is open
The focus trap in the modal component was preventing clicks on elements outside of the modal, including the notification toasts. This change configures the focus trap to allow clicks on toast notifications, so they can be dismissed even when a modal is open.
2025-10-12 05:52:24 -04:00
petrkrapek 6e78f855a3 Update Czech translations for various terms 2025-10-12 09:57:41 +02:00
Timothy Jaeryang Baek ffad1f1dd1 refac 2025-10-11 15:50:32 -05:00
Timothy Jaeryang Baek 5064506de4 refac/fix: inherit request form data 2025-10-11 14:54:07 -05:00
Tim Jaeryang Baek e14430daa8 Merge pull request #18248 from petrkrapek/community
i18n: Update Czech translation
2025-10-11 13:44:20 -05:00
Tim Jaeryang Baek 8c84501235 Merge pull request #18232 from silentoplayz/fix-chats-button-navigation
fix: prevent click on "Chats" in sidebar from going to new chat
2025-10-11 13:41:54 -05:00
Timothy Jaeryang Baek 74576b88f6 refac/fix 2025-10-11 13:40:49 -05:00
petrkrapek 7b3bd58735 Update Czech translations in translation.json 2025-10-11 12:04:31 +02:00
silentoplayz 95e3717f96 fix(sidebar): prevent click on Chats from going to new chat 2025-10-10 21:12:27 -04:00
Aslan Vatsaev a9c4e4b422 fix: auto show artifacts when opening a conversation 2025-10-10 13:47:33 +02:00
Aslan Vatsaev d13a35ab96 fix: auto show artifacts when opening a conversation 2025-10-10 13:26:12 +02:00
Tim Jaeryang Baek 43a2881074 Merge pull request #18179 from ShirasawaSama/patch-35
feat: do not initiate requests when the RecursiveFolder is not open to speed up page loading
2025-10-09 17:13:45 -05:00
Tim Jaeryang Baek a65b78fc61 Merge pull request #18198 from Classic298/patch-1
i18n: Update translation.json de-DE
2025-10-09 16:41:51 -05:00
Timothy Jaeryang Baek 4e763e8aa8 refac 2025-10-09 16:16:24 -05:00
Classic298 f9ff780422 Update translation.json 2025-10-09 22:02:47 +02:00
Tim Jaeryang Baek b93e9b1698 Merge pull request #18195 from EventHorizon-AI/fix/artifacts-duplicate-match
fix: Artifacts duplicate matching
2025-10-09 14:22:00 -05:00
Tim Jaeryang Baek de722390fc Merge pull request #18176 from Classic298/patch-1
chore: security.md
2025-10-09 14:20:58 -05:00
Tim Jaeryang Baek b37eb3dd5f Merge pull request #18178 from ShirasawaSama/patch-34
fix: fix incorrect display of undefined tool ID
2025-10-09 14:19:52 -05:00
Tim Jaeryang Baek f1113d2758 Merge pull request #18181 from ShirasawaSama/i18n/improve-chinese-translation
i18n: improve Chinese translation
2025-10-09 14:19:32 -05:00
EntropyYue 8ac30955a3 fix: Artifacts duplicate matching 2025-10-10 01:13:59 +08:00
Timothy Jaeryang Baek c4832fdb70 fix: full context issue 2025-10-09 11:55:12 -05:00
Shirasawa cce20f8bfc i18n: improve Chinese translation 2025-10-09 16:43:24 +08:00
Shirasawa ccfb0f54e1 feat: do not initiate requests when the RecursiveFolder is not open to speed up page loading 2025-10-09 15:57:50 +08:00
Shirasawa f1b50fb83a fix: fix incorrect display of undefined tool ID 2025-10-09 15:42:59 +08:00
Classic298 8ca4596918 Update SECURITY.md 2025-10-09 09:03:36 +02:00
Timothy Jaeryang Baek 5043e7fc8c refac: introduce model id length limit 2025-10-08 16:54:06 -05:00
+7 fbfbc29789 fix: search action i18n (#18162)
* 0.6.33 (#18118)

* feat: improve ollama model management experience

This commit introduces several improvements to the Ollama model management modal:

- Adds a cancel button to the model pulling operation, using the existing 'x' button pattern.
- Adds a cancel button to the "Update All" models operation, allowing the user to cancel the update for the currently processing model.
- Cleans up toast notifications when updating all models. A single toast is now shown at the beginning and a summary toast at the end, preventing notification spam.
- Refactors the `ManageOllama.svelte` component to support these new cancellation features.
- Adds tooltips to all buttons in the modal to improve clarity.
- Disables buttons when their corresponding input fields are empty to prevent accidental clicks.

* fix

* i18n: improve Chinese translation

* fix: handle non‑UTF8 chars in third‑party responses without error

* German translation of new strings in i18n

* log web search queries only with level 'debug' instead of 'info'

* Tool calls now only include text and dont inlcude other content like image b64

* fix onedrive

* fix: discovery url

* fix: default permissions not being loaded

* fix: ai hallucination

* fix: non rich text input copy

* refac: rm print statements

* refac: disable direct models from model editors

* refac/fix: do not process xlsx files with azure doc intelligence

* Update pull_request_template.md

* Update generated image translation in DE-de

* added missing danish translations

* feat(onedrive): Enable search and "My Organization" pivot

* style(onedrive): Formatting fix

* feat: Implement toggling for vertical and horizontal flow layouts

This commit introduces the necessary logic and UI controls to allow users to switch the Flow component layout between vertical and horizontal orientations.

*   **`Flow.svelte` Refactoring:**
    *   Updates logic for calculating level offsets and node positions to consistently respect the current flow orientation.
    *   Adds a control panel using `<Controls>` and `<SwitchButton>` components.
    *   Provides user interface elements to easily switch the flow layout between horizontal and vertical orientations.

* build(deps): bump pydantic from 2.11.7 to 2.11.9 in /backend

Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.11.7 to 2.11.9.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/v2.11.9/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.11.7...v2.11.9)

---
updated-dependencies:
- dependency-name: pydantic
  dependency-version: 2.11.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* build(deps): bump black from 25.1.0 to 25.9.0 in /backend

Bumps [black](https://github.com/psf/black) from 25.1.0 to 25.9.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/25.1.0...25.9.0)

---
updated-dependencies:
- dependency-name: black
  dependency-version: 25.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* build(deps): bump markdown from 3.8.2 to 3.9 in /backend

Bumps [markdown](https://github.com/Python-Markdown/markdown) from 3.8.2 to 3.9.
- [Release notes](https://github.com/Python-Markdown/markdown/releases)
- [Changelog](https://github.com/Python-Markdown/markdown/blob/master/docs/changelog.md)
- [Commits](https://github.com/Python-Markdown/markdown/compare/3.8.2...3.9.0)

---
updated-dependencies:
- dependency-name: markdown
  dependency-version: '3.9'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* build(deps): bump chromadb from 1.0.20 to 1.1.0 in /backend

Bumps [chromadb](https://github.com/chroma-core/chroma) from 1.0.20 to 1.1.0.
- [Release notes](https://github.com/chroma-core/chroma/releases)
- [Changelog](https://github.com/chroma-core/chroma/blob/main/RELEASE_PROCESS.md)
- [Commits](https://github.com/chroma-core/chroma/compare/1.0.20...1.1.0)

---
updated-dependencies:
- dependency-name: chromadb
  dependency-version: 1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* build(deps): bump opentelemetry-api from 1.36.0 to 1.37.0

Bumps [opentelemetry-api](https://github.com/open-telemetry/opentelemetry-python) from 1.36.0 to 1.37.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-python/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-python/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-python/compare/v1.36.0...v1.37.0)

---
updated-dependencies:
- dependency-name: opentelemetry-api
  dependency-version: 1.37.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* refac: ollama embed form data

* fix: non rich text handling

* fix: oauth client registration

* refac

* chore: dep bump

* chore: fastapi bump

* chore/refac: bump bcrypt and remove passlib

* Improving Korean Translation

* refac

* Improving Korean Translation

* feat: PWA share_target implementation

Co-Authored-By: gjveld <19951982+gjveld@users.noreply.github.com>

* refac: message input mobile detection behaviour

* feat: model_ids per folder

* Update translation.json (pt-BR)

inclusion of new translations of items that have been added

* refac

* refac

* refac

* refac

* refac/fix: temp chat

* refac

* refac: stop task

* refac/fix: azure audio escape

* refac: external tool validation

* refac/enh: start.sh additional args support

* refac

* refac: styling

* refac/fix: direct connection floating action buttons

* refac/fix: system prompt duplication

* refac/enh: openai tts additional params support

* refac

* feat: load data in parallel to accelerate page loading speed

* i18n: improve Chinese translation

* refac

* refac: model selector

* UPD: i18n es-ES Translation v0.6.33

UPD: i18n es-ES Translation v0.6.33

Updated new strings.

* refac

* improved query pref by querying only relevant columns

* refac/enh: docling params

* refac

* refac: openai additional headers support

* refac

* FEAT: Add Vega Char Visualizer Renderer

### FEAT: Add Vega Char Visualizer Renderer

Feature required in https://github.com/open-webui/open-webui/discussions/18022

Added npm vega lib to package.json
Added function for visualization renderer to src/libs/utils/index.ts
Added logic to src/lib/components/chat/Messages/CodeBlock.svelte

The treatment is similar as for mermaid diagrams.

Reference: https://vega.github.io/vega/

* refac

* chore

* refac

* FEAT: Add Vega-Lite Char Visualizer Renderer

### FEAT: Add Vega Char Visualizer Renderer

Add suport for Vega-Lite Specifications.
Vega-Lite is a "compiled" version of Vega Char Visualizer.
For be rendered with Vega it have to be compiled.
This PR add the check and compile if necessary, is a complement of recent Vega Renderer Feature added.

* refac

* refac/fix: switch

* enh/refac: url input handling

* refac

* refac: styling

* UPD: Add Validators & Error Toast for Mermaid & Vega diagrams

### UPD: Feat:  Add Validators & Error Toast for Mermaid & Vega diagrams

Description:
As many time the diagrams generated or entered have syntax errors the diagrams are not rendered due to that errors, but as there isn't any notification is difficult to know what happend.

This PR add validator and toast notification when error on Mermaid and Vega/Vega-Lite diagrams, helping the user to fix its.

* removed redundant knowledge API call

* Fix Code Format

* refac: model workspace view

* refac

* refac: knowledge

* refac: prompts

* refac: tools

* refac

* feat: attach folder

* refac: make tencentcloud-sdk-python optional

* refac/fix: oauth

* enh: ENABLE_OAUTH_EMAIL_FALLBACK

* refac/fix: folders

* Update requirements.txt

* Update pyproject.toml

* UPD: Add Validators & Error Toast for Mermaid & Vega diagrams

### UPD: Feat:  Add Validators & Error Toast for Mermaid & Vega diagrams

Description:
As many time the diagrams generated or entered have syntax errors the diagrams are not rendered due to that errors, but as there isn't any notification is difficult to know what happend.

This PR add validator and toast notification when error on Mermaid and Vega/Vega-Lite diagrams, helping the user to fix its.

Note:
Another possibility of integrating this Graph Visualizer is through its svelte component: https://github.com/vega/svelte-vega/tree/main/packages/svelte-vega

* Removed unused toast import & Code Format

* refac

* refac: external tool server view

* refac

* refac: overview

* refac: styling

* refac

* Update bug_report.yaml

* refac

* refac

* refac

* refac

* refac: oauth client fallback

* Fixed: Cannot handle batch sizes > 1 if no padding token is defined

Fixes Cannot handle batch sizes > 1 if no padding token is defined

For reranker models that do not have this defined in their config by using the eos_token_id if present as pad_token_id.

* refac: fallback to reasoning content

* fix(i18n): corrected typo in Spanish translation for "Reasoning Tags"

Typo fixed in Spanish translation file at line 1240 of `open-webui/src/lib/i18n/locales/es-ES/translation.json`:

- Incorrect: "Eriquetas de Razonamiento"
- Correct:   "Etiquetas de Razonamiento"

This improves clarity and consistency in the UI.

* refac/fix: ENABLE_STAR_SESSIONS_MIDDLEWARE

* refac/fix: redirect

* refac

* refac

* refac

* refac: web search error handling

* refac: source parsing

* refac: functions

* refac

* refac/enh: note pdf export

* refac/fix: mcp oauth2.1

* chore: format

* chore: Changelog (#17995)

* 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

* refac

* chore: dep bump

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: silentoplayz <jacwoo21@outlook.com>
Co-authored-by: Shirasawa <764798966@qq.com>
Co-authored-by: Jan Kessler <jakessle@uni-mainz.de>
Co-authored-by: Jacob Leksan <jacob.leksan@expedient.com>
Co-authored-by: Classic298 <27028174+Classic298@users.noreply.github.com>
Co-authored-by: sinejespersen <sinejespersen@protonmail.com>
Co-authored-by: Selene Blok <selene.blok@rws.nl>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Cyp <cypher9715@naver.com>
Co-authored-by: gjveld <19951982+gjveld@users.noreply.github.com>
Co-authored-by: joaoback <156559121+joaoback@users.noreply.github.com>
Co-authored-by: _00_ <131402327+rgaricano@users.noreply.github.com>
Co-authored-by: expruc <eygabi01@gmail.com>
Co-authored-by: YetheSamartaka <55753928+YetheSamartaka@users.noreply.github.com>
Co-authored-by: Akutangulo <akutangulo@gmail.com>

* fix: search action i18n

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Tim Jaeryang Baek <tim@openwebui.com>
Co-authored-by: silentoplayz <jacwoo21@outlook.com>
Co-authored-by: Shirasawa <764798966@qq.com>
Co-authored-by: Jan Kessler <jakessle@uni-mainz.de>
Co-authored-by: Jacob Leksan <jacob.leksan@expedient.com>
Co-authored-by: Classic298 <27028174+Classic298@users.noreply.github.com>
Co-authored-by: sinejespersen <sinejespersen@protonmail.com>
Co-authored-by: Selene Blok <selene.blok@rws.nl>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Cyp <cypher9715@naver.com>
Co-authored-by: gjveld <19951982+gjveld@users.noreply.github.com>
Co-authored-by: joaoback <156559121+joaoback@users.noreply.github.com>
Co-authored-by: _00_ <131402327+rgaricano@users.noreply.github.com>
Co-authored-by: expruc <eygabi01@gmail.com>
Co-authored-by: YetheSamartaka <55753928+YetheSamartaka@users.noreply.github.com>
Co-authored-by: Akutangulo <akutangulo@gmail.com>
2025-10-08 15:31:15 -05:00
Tim Jaeryang Baek 86cb1058c3 Merge pull request #18151 from Kylapaallikko/dev
i18n: Update fi-FI translation
2025-10-08 15:30:42 -05:00
Tim Jaeryang Baek 40f7e17b3e Merge pull request #18153 from rgaricano/dev-es_ES
UPD: i18n: es-ES Translation v0.6.34
2025-10-08 15:29:29 -05:00
Tim Jaeryang Baek bc29e75116 Merge pull request #18152 from joaoback/patch-12
Update translation.json (pt-BR)
2025-10-08 15:28:42 -05:00
Tim Jaeryang Baek 17ad6f8aad Merge pull request #18141 from ShirasawaSama/i18n/improve-chinese-translation
i18n: improve Chinese translation
2025-10-08 15:28:24 -05:00
_00_ 3f030ef537 UPD: i18n: es-ES Translation v0.6.34
### UPD: i18n: es-ES Translation v0.6.34

Added new strings
2025-10-08 16:18:50 +02:00
joaoback 62456a606e Update translation.json (pt-BR)
Translations of new items added in the latest version.
2025-10-08 11:11:38 -03:00
joaoback 3512904e00 Update translation.json (pt-BR)
Translations of new items added in the latest version.
2025-10-08 11:04:27 -03:00
Kylapaallikko 38a39b5892 Update fi-FI translation.json
Added missing translations
2025-10-08 16:31:18 +03:00
Shirasawa 0127ac39eb i18n: improve Chinese translation 2025-10-08 14:20:47 +08:00
+6 bc0da24e9c 0.6.33 (#18118) (#18137)
* feat: improve ollama model management experience

This commit introduces several improvements to the Ollama model management modal:

- Adds a cancel button to the model pulling operation, using the existing 'x' button pattern.
- Adds a cancel button to the "Update All" models operation, allowing the user to cancel the update for the currently processing model.
- Cleans up toast notifications when updating all models. A single toast is now shown at the beginning and a summary toast at the end, preventing notification spam.
- Refactors the `ManageOllama.svelte` component to support these new cancellation features.
- Adds tooltips to all buttons in the modal to improve clarity.
- Disables buttons when their corresponding input fields are empty to prevent accidental clicks.

* fix

* i18n: improve Chinese translation

* fix: handle non‑UTF8 chars in third‑party responses without error

* German translation of new strings in i18n

* log web search queries only with level 'debug' instead of 'info'

* Tool calls now only include text and dont inlcude other content like image b64

* fix onedrive

* fix: discovery url

* fix: default permissions not being loaded

* fix: ai hallucination

* fix: non rich text input copy

* refac: rm print statements

* refac: disable direct models from model editors

* refac/fix: do not process xlsx files with azure doc intelligence

* Update pull_request_template.md

* Update generated image translation in DE-de

* added missing danish translations

* feat(onedrive): Enable search and "My Organization" pivot

* style(onedrive): Formatting fix

* feat: Implement toggling for vertical and horizontal flow layouts

This commit introduces the necessary logic and UI controls to allow users to switch the Flow component layout between vertical and horizontal orientations.

*   **`Flow.svelte` Refactoring:**
    *   Updates logic for calculating level offsets and node positions to consistently respect the current flow orientation.
    *   Adds a control panel using `<Controls>` and `<SwitchButton>` components.
    *   Provides user interface elements to easily switch the flow layout between horizontal and vertical orientations.

* build(deps): bump pydantic from 2.11.7 to 2.11.9 in /backend

Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.11.7 to 2.11.9.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/v2.11.9/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.11.7...v2.11.9)

---
updated-dependencies:
- dependency-name: pydantic
  dependency-version: 2.11.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...



* build(deps): bump black from 25.1.0 to 25.9.0 in /backend

Bumps [black](https://github.com/psf/black) from 25.1.0 to 25.9.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/25.1.0...25.9.0)

---
updated-dependencies:
- dependency-name: black
  dependency-version: 25.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...



* build(deps): bump markdown from 3.8.2 to 3.9 in /backend

Bumps [markdown](https://github.com/Python-Markdown/markdown) from 3.8.2 to 3.9.
- [Release notes](https://github.com/Python-Markdown/markdown/releases)
- [Changelog](https://github.com/Python-Markdown/markdown/blob/master/docs/changelog.md)
- [Commits](https://github.com/Python-Markdown/markdown/compare/3.8.2...3.9.0)

---
updated-dependencies:
- dependency-name: markdown
  dependency-version: '3.9'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...



* build(deps): bump chromadb from 1.0.20 to 1.1.0 in /backend

Bumps [chromadb](https://github.com/chroma-core/chroma) from 1.0.20 to 1.1.0.
- [Release notes](https://github.com/chroma-core/chroma/releases)
- [Changelog](https://github.com/chroma-core/chroma/blob/main/RELEASE_PROCESS.md)
- [Commits](https://github.com/chroma-core/chroma/compare/1.0.20...1.1.0)

---
updated-dependencies:
- dependency-name: chromadb
  dependency-version: 1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...



* build(deps): bump opentelemetry-api from 1.36.0 to 1.37.0

Bumps [opentelemetry-api](https://github.com/open-telemetry/opentelemetry-python) from 1.36.0 to 1.37.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-python/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-python/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-python/compare/v1.36.0...v1.37.0)

---
updated-dependencies:
- dependency-name: opentelemetry-api
  dependency-version: 1.37.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...



* refac: ollama embed form data

* fix: non rich text handling

* fix: oauth client registration

* refac

* chore: dep bump

* chore: fastapi bump

* chore/refac: bump bcrypt and remove passlib

* Improving Korean Translation

* refac

* Improving Korean Translation

* feat: PWA share_target implementation



* refac: message input mobile detection behaviour

* feat: model_ids per folder

* Update translation.json (pt-BR)

inclusion of new translations of items that have been added

* refac

* refac

* refac

* refac

* refac/fix: temp chat

* refac

* refac: stop task

* refac/fix: azure audio escape

* refac: external tool validation

* refac/enh: start.sh additional args support

* refac

* refac: styling

* refac/fix: direct connection floating action buttons

* refac/fix: system prompt duplication

* refac/enh: openai tts additional params support

* refac

* feat: load data in parallel to accelerate page loading speed

* i18n: improve Chinese translation

* refac

* refac: model selector

* UPD: i18n es-ES Translation v0.6.33

UPD: i18n es-ES Translation v0.6.33

Updated new strings.

* refac

* improved query pref by querying only relevant columns

* refac/enh: docling params

* refac

* refac: openai additional headers support

* refac

* FEAT: Add Vega Char Visualizer Renderer

### FEAT: Add Vega Char Visualizer Renderer

Feature required in https://github.com/open-webui/open-webui/discussions/18022

Added npm vega lib to package.json
Added function for visualization renderer to src/libs/utils/index.ts
Added logic to src/lib/components/chat/Messages/CodeBlock.svelte

The treatment is similar as for mermaid diagrams.

Reference: https://vega.github.io/vega/

* refac

* chore

* refac

* FEAT: Add Vega-Lite Char Visualizer Renderer

### FEAT: Add Vega Char Visualizer Renderer

Add suport for Vega-Lite Specifications.
Vega-Lite is a "compiled" version of Vega Char Visualizer.
For be rendered with Vega it have to be compiled.
This PR add the check and compile if necessary, is a complement of recent Vega Renderer Feature added.

* refac

* refac/fix: switch

* enh/refac: url input handling

* refac

* refac: styling

* UPD: Add Validators & Error Toast for Mermaid & Vega diagrams

### UPD: Feat:  Add Validators & Error Toast for Mermaid & Vega diagrams

Description:
As many time the diagrams generated or entered have syntax errors the diagrams are not rendered due to that errors, but as there isn't any notification is difficult to know what happend.

This PR add validator and toast notification when error on Mermaid and Vega/Vega-Lite diagrams, helping the user to fix its.

* removed redundant knowledge API call

* Fix Code Format

* refac: model workspace view

* refac

* refac: knowledge

* refac: prompts

* refac: tools

* refac

* feat: attach folder

* refac: make tencentcloud-sdk-python optional

* refac/fix: oauth

* enh: ENABLE_OAUTH_EMAIL_FALLBACK

* refac/fix: folders

* Update requirements.txt

* Update pyproject.toml

* UPD: Add Validators & Error Toast for Mermaid & Vega diagrams

### UPD: Feat:  Add Validators & Error Toast for Mermaid & Vega diagrams

Description:
As many time the diagrams generated or entered have syntax errors the diagrams are not rendered due to that errors, but as there isn't any notification is difficult to know what happend.

This PR add validator and toast notification when error on Mermaid and Vega/Vega-Lite diagrams, helping the user to fix its.

Note:
Another possibility of integrating this Graph Visualizer is through its svelte component: https://github.com/vega/svelte-vega/tree/main/packages/svelte-vega

* Removed unused toast import & Code Format

* refac

* refac: external tool server view

* refac

* refac: overview

* refac: styling

* refac

* Update bug_report.yaml

* refac

* refac

* refac

* refac

* refac: oauth client fallback

* Fixed: Cannot handle batch sizes > 1 if no padding token is defined

Fixes Cannot handle batch sizes > 1 if no padding token is defined

For reranker models that do not have this defined in their config by using the eos_token_id if present as pad_token_id.

* refac: fallback to reasoning content

* fix(i18n): corrected typo in Spanish translation for "Reasoning Tags"

Typo fixed in Spanish translation file at line 1240 of `open-webui/src/lib/i18n/locales/es-ES/translation.json`:

- Incorrect: "Eriquetas de Razonamiento"
- Correct:   "Etiquetas de Razonamiento"

This improves clarity and consistency in the UI.

* refac/fix: ENABLE_STAR_SESSIONS_MIDDLEWARE

* refac/fix: redirect

* refac

* refac

* refac

* refac: web search error handling

* refac: source parsing

* refac: functions

* refac

* refac/enh: note pdf export

* refac/fix: mcp oauth2.1

* chore: format

* chore: Changelog (#17995)

* 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

* refac

* chore: dep bump

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: silentoplayz <jacwoo21@outlook.com>
Co-authored-by: Shirasawa <764798966@qq.com>
Co-authored-by: Jan Kessler <jakessle@uni-mainz.de>
Co-authored-by: Jacob Leksan <jacob.leksan@expedient.com>
Co-authored-by: Classic298 <27028174+Classic298@users.noreply.github.com>
Co-authored-by: sinejespersen <sinejespersen@protonmail.com>
Co-authored-by: Selene Blok <selene.blok@rws.nl>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Cyp <cypher9715@naver.com>
Co-authored-by: gjveld <19951982+gjveld@users.noreply.github.com>
Co-authored-by: joaoback <156559121+joaoback@users.noreply.github.com>
Co-authored-by: _00_ <131402327+rgaricano@users.noreply.github.com>
Co-authored-by: expruc <eygabi01@gmail.com>
Co-authored-by: YetheSamartaka <55753928+YetheSamartaka@users.noreply.github.com>
Co-authored-by: Akutangulo <akutangulo@gmail.com>
2025-10-07 23:06:04 -05:00
Tim Jaeryang Baek 46ae3f4f5d Merge branch 'dev' into main 2025-10-07 23:05:53 -05:00
Timothy Jaeryang Baek 19ba6c06dd refac/fix: verify connection 2025-10-07 22:56:52 -05:00
+6 8d7d79d54b 0.6.33 (#18118)
* feat: improve ollama model management experience

This commit introduces several improvements to the Ollama model management modal:

- Adds a cancel button to the model pulling operation, using the existing 'x' button pattern.
- Adds a cancel button to the "Update All" models operation, allowing the user to cancel the update for the currently processing model.
- Cleans up toast notifications when updating all models. A single toast is now shown at the beginning and a summary toast at the end, preventing notification spam.
- Refactors the `ManageOllama.svelte` component to support these new cancellation features.
- Adds tooltips to all buttons in the modal to improve clarity.
- Disables buttons when their corresponding input fields are empty to prevent accidental clicks.

* fix

* i18n: improve Chinese translation

* fix: handle non‑UTF8 chars in third‑party responses without error

* German translation of new strings in i18n

* log web search queries only with level 'debug' instead of 'info'

* Tool calls now only include text and dont inlcude other content like image b64

* fix onedrive

* fix: discovery url

* fix: default permissions not being loaded

* fix: ai hallucination

* fix: non rich text input copy

* refac: rm print statements

* refac: disable direct models from model editors

* refac/fix: do not process xlsx files with azure doc intelligence

* Update pull_request_template.md

* Update generated image translation in DE-de

* added missing danish translations

* feat(onedrive): Enable search and "My Organization" pivot

* style(onedrive): Formatting fix

* feat: Implement toggling for vertical and horizontal flow layouts

This commit introduces the necessary logic and UI controls to allow users to switch the Flow component layout between vertical and horizontal orientations.

*   **`Flow.svelte` Refactoring:**
    *   Updates logic for calculating level offsets and node positions to consistently respect the current flow orientation.
    *   Adds a control panel using `<Controls>` and `<SwitchButton>` components.
    *   Provides user interface elements to easily switch the flow layout between horizontal and vertical orientations.

* build(deps): bump pydantic from 2.11.7 to 2.11.9 in /backend

Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.11.7 to 2.11.9.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/v2.11.9/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.11.7...v2.11.9)

---
updated-dependencies:
- dependency-name: pydantic
  dependency-version: 2.11.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* build(deps): bump black from 25.1.0 to 25.9.0 in /backend

Bumps [black](https://github.com/psf/black) from 25.1.0 to 25.9.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/25.1.0...25.9.0)

---
updated-dependencies:
- dependency-name: black
  dependency-version: 25.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* build(deps): bump markdown from 3.8.2 to 3.9 in /backend

Bumps [markdown](https://github.com/Python-Markdown/markdown) from 3.8.2 to 3.9.
- [Release notes](https://github.com/Python-Markdown/markdown/releases)
- [Changelog](https://github.com/Python-Markdown/markdown/blob/master/docs/changelog.md)
- [Commits](https://github.com/Python-Markdown/markdown/compare/3.8.2...3.9.0)

---
updated-dependencies:
- dependency-name: markdown
  dependency-version: '3.9'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* build(deps): bump chromadb from 1.0.20 to 1.1.0 in /backend

Bumps [chromadb](https://github.com/chroma-core/chroma) from 1.0.20 to 1.1.0.
- [Release notes](https://github.com/chroma-core/chroma/releases)
- [Changelog](https://github.com/chroma-core/chroma/blob/main/RELEASE_PROCESS.md)
- [Commits](https://github.com/chroma-core/chroma/compare/1.0.20...1.1.0)

---
updated-dependencies:
- dependency-name: chromadb
  dependency-version: 1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* build(deps): bump opentelemetry-api from 1.36.0 to 1.37.0

Bumps [opentelemetry-api](https://github.com/open-telemetry/opentelemetry-python) from 1.36.0 to 1.37.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-python/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-python/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-python/compare/v1.36.0...v1.37.0)

---
updated-dependencies:
- dependency-name: opentelemetry-api
  dependency-version: 1.37.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* refac: ollama embed form data

* fix: non rich text handling

* fix: oauth client registration

* refac

* chore: dep bump

* chore: fastapi bump

* chore/refac: bump bcrypt and remove passlib

* Improving Korean Translation

* refac

* Improving Korean Translation

* feat: PWA share_target implementation

Co-Authored-By: gjveld <19951982+gjveld@users.noreply.github.com>

* refac: message input mobile detection behaviour

* feat: model_ids per folder

* Update translation.json (pt-BR)

inclusion of new translations of items that have been added

* refac

* refac

* refac

* refac

* refac/fix: temp chat

* refac

* refac: stop task

* refac/fix: azure audio escape

* refac: external tool validation

* refac/enh: start.sh additional args support

* refac

* refac: styling

* refac/fix: direct connection floating action buttons

* refac/fix: system prompt duplication

* refac/enh: openai tts additional params support

* refac

* feat: load data in parallel to accelerate page loading speed

* i18n: improve Chinese translation

* refac

* refac: model selector

* UPD: i18n es-ES Translation v0.6.33

UPD: i18n es-ES Translation v0.6.33

Updated new strings.

* refac

* improved query pref by querying only relevant columns

* refac/enh: docling params

* refac

* refac: openai additional headers support

* refac

* FEAT: Add Vega Char Visualizer Renderer

### FEAT: Add Vega Char Visualizer Renderer

Feature required in https://github.com/open-webui/open-webui/discussions/18022

Added npm vega lib to package.json
Added function for visualization renderer to src/libs/utils/index.ts
Added logic to src/lib/components/chat/Messages/CodeBlock.svelte

The treatment is similar as for mermaid diagrams.

Reference: https://vega.github.io/vega/

* refac

* chore

* refac

* FEAT: Add Vega-Lite Char Visualizer Renderer

### FEAT: Add Vega Char Visualizer Renderer

Add suport for Vega-Lite Specifications.
Vega-Lite is a "compiled" version of Vega Char Visualizer.
For be rendered with Vega it have to be compiled.
This PR add the check and compile if necessary, is a complement of recent Vega Renderer Feature added.

* refac

* refac/fix: switch

* enh/refac: url input handling

* refac

* refac: styling

* UPD: Add Validators & Error Toast for Mermaid & Vega diagrams

### UPD: Feat:  Add Validators & Error Toast for Mermaid & Vega diagrams

Description:
As many time the diagrams generated or entered have syntax errors the diagrams are not rendered due to that errors, but as there isn't any notification is difficult to know what happend.

This PR add validator and toast notification when error on Mermaid and Vega/Vega-Lite diagrams, helping the user to fix its.

* removed redundant knowledge API call

* Fix Code Format

* refac: model workspace view

* refac

* refac: knowledge

* refac: prompts

* refac: tools

* refac

* feat: attach folder

* refac: make tencentcloud-sdk-python optional

* refac/fix: oauth

* enh: ENABLE_OAUTH_EMAIL_FALLBACK

* refac/fix: folders

* Update requirements.txt

* Update pyproject.toml

* UPD: Add Validators & Error Toast for Mermaid & Vega diagrams

### UPD: Feat:  Add Validators & Error Toast for Mermaid & Vega diagrams

Description:
As many time the diagrams generated or entered have syntax errors the diagrams are not rendered due to that errors, but as there isn't any notification is difficult to know what happend.

This PR add validator and toast notification when error on Mermaid and Vega/Vega-Lite diagrams, helping the user to fix its.

Note:
Another possibility of integrating this Graph Visualizer is through its svelte component: https://github.com/vega/svelte-vega/tree/main/packages/svelte-vega

* Removed unused toast import & Code Format

* refac

* refac: external tool server view

* refac

* refac: overview

* refac: styling

* refac

* Update bug_report.yaml

* refac

* refac

* refac

* refac

* refac: oauth client fallback

* Fixed: Cannot handle batch sizes > 1 if no padding token is defined

Fixes Cannot handle batch sizes > 1 if no padding token is defined

For reranker models that do not have this defined in their config by using the eos_token_id if present as pad_token_id.

* refac: fallback to reasoning content

* fix(i18n): corrected typo in Spanish translation for "Reasoning Tags"

Typo fixed in Spanish translation file at line 1240 of `open-webui/src/lib/i18n/locales/es-ES/translation.json`:

- Incorrect: "Eriquetas de Razonamiento"
- Correct:   "Etiquetas de Razonamiento"

This improves clarity and consistency in the UI.

* refac/fix: ENABLE_STAR_SESSIONS_MIDDLEWARE

* refac/fix: redirect

* refac

* refac

* refac

* refac: web search error handling

* refac: source parsing

* refac: functions

* refac

* refac/enh: note pdf export

* refac/fix: mcp oauth2.1

* chore: format

* chore: Changelog (#17995)

* 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

* refac

* chore: dep bump

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: silentoplayz <jacwoo21@outlook.com>
Co-authored-by: Shirasawa <764798966@qq.com>
Co-authored-by: Jan Kessler <jakessle@uni-mainz.de>
Co-authored-by: Jacob Leksan <jacob.leksan@expedient.com>
Co-authored-by: Classic298 <27028174+Classic298@users.noreply.github.com>
Co-authored-by: sinejespersen <sinejespersen@protonmail.com>
Co-authored-by: Selene Blok <selene.blok@rws.nl>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Cyp <cypher9715@naver.com>
Co-authored-by: gjveld <19951982+gjveld@users.noreply.github.com>
Co-authored-by: joaoback <156559121+joaoback@users.noreply.github.com>
Co-authored-by: _00_ <131402327+rgaricano@users.noreply.github.com>
Co-authored-by: expruc <eygabi01@gmail.com>
Co-authored-by: YetheSamartaka <55753928+YetheSamartaka@users.noreply.github.com>
Co-authored-by: Akutangulo <akutangulo@gmail.com>
2025-10-07 16:20:27 -05:00
Timothy Jaeryang Baek c1e86ad0a6 chore: dep bump 2025-10-07 16:19:47 -05:00
Timothy Jaeryang Baek dbbdad3ebd refac 2025-10-07 16:13:20 -05:00
Classic298 73f38dce88 chore: Changelog (#17995)
* 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
2025-10-07 16:10:52 -05:00
Timothy Jaeryang Baek d11fba2dfd chore: format 2025-10-07 15:23:46 -05:00
Timothy Jaeryang Baek 911a114ad4 refac/fix: mcp oauth2.1 2025-10-07 14:56:10 -05:00
Timothy Jaeryang Baek 216fb5c3db refac/enh: note pdf export 2025-10-07 14:34:47 -05:00
Timothy Jaeryang Baek 18d74f2b10 refac 2025-10-07 14:23:44 -05:00
Timothy Jaeryang Baek f5e1a42f51 refac: functions 2025-10-07 14:20:07 -05:00
Tim Jaeryang Baek 299e0e2674 Merge pull request #18114 from Akutangulo/main
i18n: Fix typo in Spanish translation ("Eriquetas" → "Etiquetas")
2025-10-07 13:53:05 -05:00
Timothy Jaeryang Baek 3c47e49cf0 refac: source parsing 2025-10-07 12:46:18 -05:00
Timothy Jaeryang Baek 53e98620bf refac: web search error handling 2025-10-07 12:15:24 -05:00
Timothy Jaeryang Baek 7d205b1711 refac 2025-10-07 12:08:21 -05:00
Timothy Jaeryang Baek e000494e48 refac 2025-10-07 11:53:30 -05:00
Timothy Jaeryang Baek 2d71193bc0 refac 2025-10-07 10:59:53 -05:00
Tim Jaeryang Baek 696876d393 Merge pull request #18108 from YetheSamartaka/main
fix: "Cannot handle batch sizes > 1 if no padding token is defined" for some reranking models
2025-10-07 10:57:34 -05:00
Timothy Jaeryang Baek db1780678e refac/fix: redirect 2025-10-07 10:53:58 -05:00
Timothy Jaeryang Baek 861953fd2d refac/fix: ENABLE_STAR_SESSIONS_MIDDLEWARE 2025-10-07 10:52:34 -05:00
Akutangulo 7448cf6c65 fix(i18n): corrected typo in Spanish translation for "Reasoning Tags"
Typo fixed in Spanish translation file at line 1240 of `open-webui/src/lib/i18n/locales/es-ES/translation.json`:

- Incorrect: "Eriquetas de Razonamiento"
- Correct:   "Etiquetas de Razonamiento"

This improves clarity and consistency in the UI.
2025-10-07 15:06:42 +02:00
Timothy Jaeryang Baek 4bb5b39410 refac: fallback to reasoning content 2025-10-07 07:59:21 -05:00
YetheSamartaka f69426fd77 Fixed: Cannot handle batch sizes > 1 if no padding token is defined
Fixes Cannot handle batch sizes > 1 if no padding token is defined

For reranker models that do not have this defined in their config by using the eos_token_id if present as pad_token_id.
2025-10-07 14:47:35 +02:00
Timothy Jaeryang Baek 82a16f1305 refac: oauth client fallback 2025-10-07 07:46:13 -05:00
Timothy Jaeryang Baek b98d8aa8ec refac 2025-10-07 07:31:06 -05:00
Timothy Jaeryang Baek b6108527c2 refac 2025-10-06 18:42:48 -05:00
Timothy Jaeryang Baek c58a4a0166 refac 2025-10-06 18:42:24 -05:00
Tim Jaeryang Baek ca3563b3b4 Merge pull request #18079 from Classic298/patch-2
Update bug_report.yaml
2025-10-06 15:29:13 -05:00
Timothy Jaeryang Baek dec3748509 refac 2025-10-06 11:58:15 -05:00
Classic298 d62f21549f Update bug_report.yaml 2025-10-06 12:36:56 +02:00
Timothy Jaeryang Baek 3f71fa641f refac 2025-10-06 02:45:02 -05:00
Timothy Jaeryang Baek 2f398895c6 refac: styling 2025-10-06 00:02:22 -05:00
Timothy Jaeryang Baek b4536a691a refac: overview 2025-10-05 23:56:23 -05:00
Timothy Jaeryang Baek 270ca2ddbe refac 2025-10-05 23:36:26 -05:00
Timothy Jaeryang Baek 9b4f032660 refac: external tool server view 2025-10-05 23:35:04 -05:00
Timothy Jaeryang Baek 53de48d2b3 refac 2025-10-05 23:29:48 -05:00
Tim Jaeryang Baek 08f8713ee1 Merge pull request #18068 from rgaricano/dev-FEAT_Vega_Visualizer
UPD: Add Validators & Error Toast for Mermaid & Vega diagrams
2025-10-05 21:39:25 -05:00
Tim Jaeryang Baek a97f1457f6 Merge pull request #18066 from Classic298/pymilvus
chore: Update pymilvus dependency
2025-10-05 21:38:28 -05:00
_00_ fa2534a529 Removed unused toast import & Code Format 2025-10-06 00:16:01 +02:00
_00_ 8538d1b2cb UPD: Add Validators & Error Toast for Mermaid & Vega diagrams
### UPD: Feat:  Add Validators & Error Toast for Mermaid & Vega diagrams

Description:
As many time the diagrams generated or entered have syntax errors the diagrams are not rendered due to that errors, but as there isn't any notification is difficult to know what happend.

This PR add validator and toast notification when error on Mermaid and Vega/Vega-Lite diagrams, helping the user to fix its.

Note:
Another possibility of integrating this Graph Visualizer is through its svelte component: https://github.com/vega/svelte-vega/tree/main/packages/svelte-vega
2025-10-06 00:09:17 +02:00
Classic298 079b9ec86e Update pyproject.toml 2025-10-05 22:47:33 +02:00
Classic298 52998e6aaa Update requirements.txt 2025-10-05 22:47:07 +02:00
Timothy Jaeryang Baek 5448618dd5 refac/fix: folders 2025-10-05 15:20:47 -05:00
Timothy Jaeryang Baek eaf786c1ef enh: ENABLE_OAUTH_EMAIL_FALLBACK 2025-10-05 15:11:56 -05:00
Tim Jaeryang Baek c453d919b3 Merge pull request #18057 from expruc/perf/knowledge_page
Perf: knowledge page loading speed
2025-10-05 15:05:45 -05:00
Timothy Jaeryang Baek 062264c7f6 refac/fix: oauth 2025-10-05 14:22:00 -05:00
Timothy Jaeryang Baek 713f0ef9ce refac: make tencentcloud-sdk-python optional 2025-10-05 04:49:06 -05:00
Timothy Jaeryang Baek d2cb78179d feat: attach folder 2025-10-05 02:48:08 -05:00
Timothy Jaeryang Baek fff0e55f41 refac 2025-10-05 02:05:52 -05:00
Timothy Jaeryang Baek a743b16728 refac: tools 2025-10-05 02:03:57 -05:00
Timothy Jaeryang Baek 23c8f6d507 refac: prompts 2025-10-05 01:41:42 -05:00
Timothy Jaeryang Baek 2250d102b2 refac: knowledge 2025-10-05 01:06:40 -05:00
Timothy Jaeryang Baek 96ecb47bc7 refac 2025-10-05 00:25:40 -05:00
Timothy Jaeryang Baek 6050c86ab6 refac: model workspace view 2025-10-04 21:44:51 -05:00
_00_ e6cc7db3c1 Fix Code Format 2025-10-04 14:45:49 +02:00
expruc 0ba1cfc612 removed redundant knowledge API call 2025-10-04 15:35:47 +03:00
_00_ 2e08bda19d UPD: Add Validators & Error Toast for Mermaid & Vega diagrams
### UPD: Feat:  Add Validators & Error Toast for Mermaid & Vega diagrams

Description:
As many time the diagrams generated or entered have syntax errors the diagrams are not rendered due to that errors, but as there isn't any notification is difficult to know what happend.

This PR add validator and toast notification when error on Mermaid and Vega/Vega-Lite diagrams, helping the user to fix its.
2025-10-04 14:17:04 +02:00
Timothy Jaeryang Baek 2c59a28860 refac: styling 2025-10-04 02:35:15 -05:00
Timothy Jaeryang Baek f20723ca54 refac 2025-10-04 02:07:02 -05:00
Timothy Jaeryang Baek a2a2bafdf6 enh/refac: url input handling 2025-10-04 02:02:26 -05:00
Timothy Jaeryang Baek ce83276fa4 refac/fix: switch 2025-10-04 02:00:10 -05:00
Timothy Jaeryang Baek d40c710354 refac 2025-10-04 01:19:33 -05:00
Tim Jaeryang Baek f65231becb Merge pull request #18040 from rgaricano/dev-FEAT_Vega_Visualizer
FEAT: Add Vega-Lite Char Visualizer Renderer
2025-10-03 23:52:30 -05:00
_00_ 039358e049 FEAT: Add Vega-Lite Char Visualizer Renderer
### FEAT: Add Vega Char Visualizer Renderer

Add suport for Vega-Lite Specifications.
Vega-Lite is a "compiled" version of Vega Char Visualizer.
For be rendered with Vega it have to be compiled.
This PR add the check and compile if necessary, is a complement of recent Vega Renderer Feature added.
2025-10-04 06:41:07 +02:00
Timothy Jaeryang Baek 59929a8d06 refac 2025-10-03 18:11:46 -05:00
Timothy Jaeryang Baek 9ff6baf7d9 chore 2025-10-03 18:06:05 -05:00
Tim Jaeryang Baek 0a4922b40f Merge pull request #18033 from rgaricano/dev-FEAT_Vega_Visualizer
FEAT: Add Vega Char Visualizer Renderer
2025-10-03 12:51:03 -05:00
Timothy Jaeryang Baek c5c6c32f24 refac 2025-10-03 12:50:07 -05:00
_00_ 351ba167f5 FEAT: Add Vega Char Visualizer Renderer
### FEAT: Add Vega Char Visualizer Renderer

Feature required in https://github.com/open-webui/open-webui/discussions/18022

Added npm vega lib to package.json
Added function for visualization renderer to src/libs/utils/index.ts
Added logic to src/lib/components/chat/Messages/CodeBlock.svelte

The treatment is similar as for mermaid diagrams.

Reference: https://vega.github.io/vega/
2025-10-03 19:42:04 +02:00
Timothy Jaeryang Baek 8334149cb2 refac 2025-10-03 02:20:03 -05:00
Tim Jaeryang Baek 5b05f13ce5 Merge pull request #18013 from expruc/perf/query_file_metadatas
perf: improve file query time by querying only relevant columns
2025-10-03 00:29:39 -05:00
Timothy Jaeryang Baek f25b7b73b4 refac: openai additional headers support 2025-10-03 00:23:26 -05:00
Timothy Jaeryang Baek 7e70f8d2c1 refac 2025-10-02 17:45:05 -05:00
Tim Jaeryang Baek 227139aa33 Merge pull request #16863 from silentoplayz/ollama-cancel-and-toast-cleanup
feat: improve Ollama model management modal
2025-10-02 17:41:21 -05:00
Timothy Jaeryang Baek 339e95e9d7 refac/enh: docling params 2025-10-02 16:28:06 -05:00
Tim Jaeryang Baek 2494de8f12 Merge pull request #18009 from rgaricano/dev-es_ES
UPD: i18n es-ES Translation v0.6.33
2025-10-02 15:35:28 -05:00
expruc d12afc6039 improved query pref by querying only relevant columns 2025-10-02 22:26:24 +03:00
Timothy Jaeryang Baek abe70d1793 refac 2025-10-02 13:26:41 -05:00
_00_ 893d4fba5f UPD: i18n es-ES Translation v0.6.33
UPD: i18n es-ES Translation v0.6.33

Updated new strings.
2025-10-02 20:09:06 +02:00
Tim Jaeryang Baek d9fdbb627f Merge pull request #17559 from ShirasawaSama/patch-14
feat: load data in parallel to accelerate page loading speed
2025-10-02 13:06:54 -05:00
Timothy Jaeryang Baek 79afb7afdb refac: model selector 2025-10-02 13:03:33 -05:00
Tim Jaeryang Baek 5fe588b480 Merge pull request #18004 from ShirasawaSama/i18n/improve-chinese-translation
i18n: improve Chinese translation
2025-10-02 11:51:34 -05:00
Timothy Jaeryang Baek 8f41835352 refac 2025-10-02 11:48:56 -05:00
Shirasawa b8649fc364 i18n: improve Chinese translation 2025-10-02 23:29:31 +08:00
Shirasawa 981306fa2b feat: load data in parallel to accelerate page loading speed 2025-10-02 23:23:43 +08:00
Timothy Jaeryang Baek c8c6a48b94 refac 2025-10-02 04:09:17 -05:00
Timothy Jaeryang Baek 5d5b42d3f5 refac/enh: openai tts additional params support 2025-10-02 03:52:29 -05:00
Timothy Jaeryang Baek a1fc99c66f refac/fix: system prompt duplication 2025-10-02 02:57:54 -05:00
Timothy Jaeryang Baek 6c4deed37a refac/fix: direct connection floating action buttons 2025-10-02 02:21:21 -05:00
Timothy Jaeryang Baek b6538b2cdd refac: styling 2025-10-02 02:19:01 -05:00
Timothy Jaeryang Baek a57bb6a7d4 refac 2025-10-02 01:59:10 -05:00
Timothy Jaeryang Baek 6ff392edc0 refac/enh: start.sh additional args support 2025-10-02 01:58:06 -05:00
Tim Jaeryang Baek e9bced867d Merge pull request #17986 from joaoback/patch-11
Update translation.json (pt-BR)
2025-10-02 01:30:04 -05:00
Timothy Jaeryang Baek 39675434f6 refac: external tool validation 2025-10-02 01:29:44 -05:00
Timothy Jaeryang Baek 2e75c6dbdf refac/fix: azure audio escape 2025-10-01 23:01:08 -05:00
Timothy Jaeryang Baek c0d3e70296 refac: stop task 2025-10-01 22:58:04 -05:00
Timothy Jaeryang Baek 028f29556f refac 2025-10-01 22:51:57 -05:00
Timothy Jaeryang Baek 3a601e0fc3 refac/fix: temp chat 2025-10-01 22:49:25 -05:00
Timothy Jaeryang Baek af34e414e1 refac 2025-10-01 22:05:42 -05:00
Timothy Jaeryang Baek 9677871ce7 refac 2025-10-01 21:57:34 -05:00
Timothy Jaeryang Baek d87a2315ce refac 2025-10-01 21:56:37 -05:00
Timothy Jaeryang Baek 97faeccebf refac 2025-10-01 21:56:32 -05:00
joaoback 6afbcac9d0 Update translation.json (pt-BR)
inclusion of new translations of items that have been added
2025-10-01 23:05:56 -03:00
Timothy Jaeryang Baek e2ca7b8632 feat: model_ids per folder 2025-10-01 20:50:04 -05:00
Timothy Jaeryang Baek 557367cf48 refac: message input mobile detection behaviour 2025-10-01 20:02:27 -05:00
Timothy Jaeryang Baekandgjveld 6c8c3257fd feat: PWA share_target implementation
Co-Authored-By: gjveld <19951982+gjveld@users.noreply.github.com>
2025-10-01 19:57:19 -05:00
Tim Jaeryang Baek 6f88db1be2 Merge pull request #17983 from Cyp9715/dev
i18n: Improving Korean Translation
2025-10-01 19:48:06 -05:00
Cyp dc3911be14 Improving Korean Translation 2025-10-02 09:36:37 +09:00
Timothy Jaeryang Baek 03498bd2fd refac 2025-10-01 19:31:47 -05:00
Cyp 6ddb44985e Improving Korean Translation 2025-10-02 09:28:29 +09:00
Tim Jaeryang Baek 2f923e47ec Merge pull request #17941 from silentoplayz/vertical-and-horizontal-overview
feat: Implement toggling for vertical and horizontal flow layouts
2025-10-01 19:27:13 -05:00
Timothy Jaeryang Baek ebce0578e6 chore/refac: bump bcrypt and remove passlib 2025-10-01 19:19:56 -05:00
Timothy Jaeryang Baek 7563a62dfe chore: fastapi bump 2025-10-01 18:14:55 -05:00
Timothy Jaeryang Baek f5a4d27e57 chore: dep bump 2025-10-01 18:13:27 -05:00
Tim Jaeryang Baek 6eb479d5af Merge pull request #17952 from open-webui/dependabot/uv/dev/opentelemetry-api-1.37.0
build(deps): bump opentelemetry-api from 1.36.0 to 1.37.0
2025-10-01 18:07:20 -05:00
Tim Jaeryang Baek 8c9c2bec6c Merge pull request #17948 from open-webui/dependabot/pip/backend/dev/chromadb-1.1.0
build(deps): bump chromadb from 1.0.20 to 1.1.0 in /backend
2025-10-01 18:07:09 -05:00
Tim Jaeryang Baek 86727ae271 Merge pull request #17947 from open-webui/dependabot/pip/backend/dev/markdown-3.9
build(deps): bump markdown from 3.8.2 to 3.9 in /backend
2025-10-01 18:07:05 -05:00
Tim Jaeryang Baek c6e498e837 Merge pull request #17945 from open-webui/dependabot/pip/backend/dev/black-25.9.0
build(deps): bump black from 25.1.0 to 25.9.0 in /backend
2025-10-01 18:06:54 -05:00
Tim Jaeryang Baek be01b87cd4 Merge pull request #17944 from open-webui/dependabot/pip/backend/dev/pydantic-2.11.9
build(deps): bump pydantic from 2.11.7 to 2.11.9 in /backend
2025-10-01 18:06:49 -05:00
Timothy Jaeryang Baek 0330dc3159 refac 2025-10-01 15:35:37 -05:00
Timothy Jaeryang Baek e493562735 fix: oauth client registration 2025-10-01 15:15:24 -05:00
Timothy Jaeryang Baek b1c196ed83 fix: non rich text handling 2025-10-01 12:39:49 -05:00
Timothy Jaeryang Baek a8a0a2655d refac: ollama embed form data 2025-09-30 22:04:22 -05:00
dependabot[bot] b0e3afc0be build(deps): bump opentelemetry-api from 1.36.0 to 1.37.0
Bumps [opentelemetry-api](https://github.com/open-telemetry/opentelemetry-python) from 1.36.0 to 1.37.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-python/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-python/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-python/compare/v1.36.0...v1.37.0)

---
updated-dependencies:
- dependency-name: opentelemetry-api
  dependency-version: 1.37.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 02:25:40 +00:00
dependabot[bot] 242bcc8643 build(deps): bump chromadb from 1.0.20 to 1.1.0 in /backend
Bumps [chromadb](https://github.com/chroma-core/chroma) from 1.0.20 to 1.1.0.
- [Release notes](https://github.com/chroma-core/chroma/releases)
- [Changelog](https://github.com/chroma-core/chroma/blob/main/RELEASE_PROCESS.md)
- [Commits](https://github.com/chroma-core/chroma/compare/1.0.20...1.1.0)

---
updated-dependencies:
- dependency-name: chromadb
  dependency-version: 1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 02:09:24 +00:00
dependabot[bot] 17af98126a build(deps): bump markdown from 3.8.2 to 3.9 in /backend
Bumps [markdown](https://github.com/Python-Markdown/markdown) from 3.8.2 to 3.9.
- [Release notes](https://github.com/Python-Markdown/markdown/releases)
- [Changelog](https://github.com/Python-Markdown/markdown/blob/master/docs/changelog.md)
- [Commits](https://github.com/Python-Markdown/markdown/compare/3.8.2...3.9.0)

---
updated-dependencies:
- dependency-name: markdown
  dependency-version: '3.9'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 02:09:20 +00:00
dependabot[bot] d9a723b0ab build(deps): bump black from 25.1.0 to 25.9.0 in /backend
Bumps [black](https://github.com/psf/black) from 25.1.0 to 25.9.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/25.1.0...25.9.0)

---
updated-dependencies:
- dependency-name: black
  dependency-version: 25.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 02:09:14 +00:00
dependabot[bot] 6cc7a8b052 build(deps): bump pydantic from 2.11.7 to 2.11.9 in /backend
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.11.7 to 2.11.9.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/v2.11.9/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.11.7...v2.11.9)

---
updated-dependencies:
- dependency-name: pydantic
  dependency-version: 2.11.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 02:09:11 +00:00
Tim Jaeryang Baek 2341c2e729 Merge pull request #17930 from selenecodes/feat/onedrive-file-picker-add-search-and-my-organisation-pivot
feat: Enable search and "My Organization" pivot in OneDrive integration
2025-09-30 19:26:56 -05:00
silentoplayz 80cbdbb535 feat: Implement toggling for vertical and horizontal flow layouts
This commit introduces the necessary logic and UI controls to allow users to switch the Flow component layout between vertical and horizontal orientations.

*   **`Flow.svelte` Refactoring:**
    *   Updates logic for calculating level offsets and node positions to consistently respect the current flow orientation.
    *   Adds a control panel using `<Controls>` and `<SwitchButton>` components.
    *   Provides user interface elements to easily switch the flow layout between horizontal and vertical orientations.
2025-09-30 16:09:55 -04:00
Tim Jaeryang Baek 39b6f4bbf5 Merge pull request #17912 from Classic298/patch-1
chore: proposed changes to pull request template
2025-09-30 10:35:51 -05:00
Tim Jaeryang Baek c850439f92 Merge pull request #17916 from Classic298/patch-2
i18n: Update generated image translation in DE-de
2025-09-30 10:35:28 -05:00
Tim Jaeryang Baek 44b71de41e Merge pull request #17923 from itk-dev/feature/danish-translations-added
added missing danish translations
2025-09-30 10:35:10 -05:00
Selene Blok 5c059e604b style(onedrive): Formatting fix 2025-09-30 16:49:53 +02:00
Selene Blok 07cc807bc9 feat(onedrive): Enable search and "My Organization" pivot 2025-09-30 16:43:38 +02:00
sinejespersen e96fb67367 added missing danish translations 2025-09-30 12:49:12 +02:00
Classic298 7259905114 Update generated image translation in DE-de 2025-09-30 10:51:39 +02:00
Classic298 0d0d28641d Update pull_request_template.md 2025-09-30 08:36:31 +02:00
Timothy Jaeryang Baek 01a5b97415 refac/fix: do not process xlsx files with azure doc intelligence 2025-09-29 23:05:24 -05:00
Timothy Jaeryang Baek 8c662c65a9 refac: disable direct models from model editors 2025-09-29 22:59:28 -05:00
Timothy Jaeryang Baek 88a6fe3c7a refac: rm print statements 2025-09-29 22:59:10 -05:00
Timothy Jaeryang Baek fcc3d9ed2b fix: non rich text input copy 2025-09-29 22:45:38 -05:00
Timothy Jaeryang Baek 58efa18f96 fix: ai hallucination 2025-09-29 21:37:38 -05:00
Timothy Jaeryang Baek 887772db22 fix: default permissions not being loaded 2025-09-29 21:36:07 -05:00
Timothy Jaeryang Baek 73f32a88e3 fix: discovery url 2025-09-29 21:30:19 -05:00
Tim Jaeryang Baek ddb109a456 Merge pull request #17888 from Ithanil/nolog_websearch
fix: log web search queries only with level 'debug' instead of 'info'
2025-09-29 21:27:27 -05:00
Tim Jaeryang Baek ccf53afad4 Merge pull request #17886 from Ithanil/de_i18n
i18n: German translation of new strings
2025-09-29 21:26:24 -05:00
Tim Jaeryang Baek 170ae9f3be Merge pull request #17882 from ShirasawaSama/patch-3
fix: handle non‑UTF8 chars in third‑party responses without error
2025-09-29 21:25:55 -05:00
Tim Jaeryang Baek 7d513ff1fe Merge pull request #17881 from ShirasawaSama/patch-20
i18n: improve Chinese (zhCN & zh-TW) translation
2025-09-29 21:25:26 -05:00
Tim Jaeryang Baek b387880194 Merge pull request #17897 from jmleksan/fix/tool-task-only-include-text
Fix: Tool task only include text
2025-09-29 21:23:24 -05:00
Tim Jaeryang Baek b15db795e1 Merge pull request #17902 from Classic298/patch-1
fix: onedrive
2025-09-29 21:22:56 -05:00
Classic298 2a3f57bc61 fix onedrive 2025-09-29 22:12:49 +02:00
Jacob Leksan 0a928d6e9d Tool calls now only include text and dont inlcude other content like image b64 2025-09-29 12:50:01 -04:00
Tim Jaeryang Baek 4d7fddaf7e Merge pull request #17892 from open-webui/dev
Dev
2025-09-29 10:07:13 -05:00
Timothy Jaeryang Baek 5438eb0057 doc: changelog 2025-09-29 10:04:21 -05:00
Tim Jaeryang Baek 88f7852928 Merge pull request #17890 from Classic298/patch-1
DEV branch changelog 0.6.32
2025-09-29 10:03:53 -05:00
Timothy Jaeryang Baek a5a098943c refac 2025-09-29 10:03:03 -05:00
Classic298 f267e479ba DEV branch changelog 0.6.32 2025-09-29 15:55:58 +02:00
Jan Kessler c9c0dd367f log web search queries only with level 'debug' instead of 'info' 2025-09-29 11:59:04 +02:00
Jan Kessler a395af31b1 German translation of new strings in i18n 2025-09-29 11:43:17 +02:00
Shirasawa 3389f0eece fix: handle non‑UTF8 chars in third‑party responses without error 2025-09-29 08:16:50 +00:00
Shirasawa 3b1197a1bb i18n: improve Chinese translation 2025-09-29 07:47:11 +00:00
Tim Jaeryang Baek 37d1c85c99 Merge pull request #17827 from open-webui/dev
0.6.32
2025-09-29 01:13:00 -05:00
Timothy Jaeryang Baek b6a485371d doc: changelog 2025-09-29 01:04:59 -05:00
Timothy Jaeryang Baek da236d5b38 chore: bump 2025-09-29 00:58:45 -05:00
Timothy Jaeryang Baek e7fa86aa26 chore: format 2025-09-29 00:58:21 -05:00
Timothy Jaeryang Baek 51a138aec3 refac: edit user modal 2025-09-29 00:51:06 -05:00
Timothy Jaeryang Baek aee7aaae68 refac 2025-09-29 00:35:21 -05:00
Tim Jaeryang Baek 2d94b8e905 Merge pull request #17837 from Classic298/milvus-multitenancy
feat: Impelement Milvus multitenancy // breaking: set milvus multitenancy as standard option (just like Qdrant already is)
2025-09-29 00:29:35 -05:00
Timothy Jaeryang Baek 8656bfc5f4 refac 2025-09-29 00:03:03 -05:00
Tim Jaeryang Baek d37f108ccd Merge pull request #17848 from Classic298/feat/group-permission-warning
Feat/chore: Add warning for conflicting group permissions / Refactored Permissions.svelte
2025-09-28 23:51:52 -05:00
Timothy Jaeryang Baek 88aca2e4d8 chore: format 2025-09-28 23:42:38 -05:00
Timothy Jaeryang Baek 234aa69753 refac 2025-09-28 22:16:11 -05:00
Timothy Jaeryang Baek f58fc753e3 feat/enh: embed citation 2025-09-28 22:15:47 -05:00
Timothy Jaeryang Baek 118549caf3 enh/fix: filter content metadata 2025-09-28 20:17:27 -05:00
Tim Jaeryang Baek 1c418a7f83 Merge pull request #17871 from silentoplayz/backend-json-model-import
feat: move JSON model import to backend for massive speedup
2025-09-28 19:25:40 -05:00
silentoplayz fe28097817 feat: refactor model import to a single backend endpoint
This refactors the model import functionality to improve performance and user experience by centralizing the logic on the backend.

Previously, the frontend would parse an imported JSON file and send an individual API request for each model, which was slow and inefficient.

This change introduces a new backend endpoint, `/api/v1/models/import`, that accepts a list of model objects. The frontend now reads the selected JSON file, parses it, and sends the entire payload to the backend in a single request. The backend then processes this list, creating or updating models as necessary.

This commit also includes the following fixes:
- Handles cases where the imported JSON contains models without `meta` or `params` fields by providing default empty values.
2025-09-28 18:49:42 -04:00
Timothy Jaeryang Baek 7bae9053ac refac: copy behaviour 2025-09-28 17:10:11 -05:00
silentoplayz 231d182c35 feat: move JSON model import to backend
This moves the JSON model import functionality to the backend. Instead of the frontend parsing the JSON file and sending multiple requests, it now uploads the file to a new endpoint (/api/v1/models/import), which processes the file and imports the models. This improves efficiency and provides better user feedback.
2025-09-28 18:09:58 -04:00
Timothy Jaeryang Baek fe54fb61aa fix: session middleware should be required by default 2025-09-28 16:35:13 -05:00
Timothy Jaeryang Baek 7e8ee46d2c refac 2025-09-28 15:18:12 -05:00
Timothy Jaeryang Baek da6748a68f refac 2025-09-28 14:59:30 -05:00
Timothy Jaeryang Baek 91b6483aa9 refac 2025-09-28 14:46:01 -05:00
Classic298 cf20f04bdc Update Permissions.svelte 2025-09-28 21:33:40 +02:00
Classic298 67b3929954 Delete src/lib/components/common/Warning.svelte 2025-09-28 21:27:31 +02:00
Classic298 ad9cae32dc Delete src/lib/components/icons/ExclamationTriangle.svelte 2025-09-28 21:27:20 +02:00
Classic298 a4e0c10f34 Delete src/lib/components/admin/Users/Groups/PermissionSwitch.svelte 2025-09-28 21:27:06 +02:00
Classic298 b1e63639cd ADD FAT WARNING - QDRANT 2025-09-28 21:17:07 +02:00
Classic298 0e99c43495 ADD FAT WARNING 2025-09-28 21:16:02 +02:00
Timothy Jaeryang Baek 4f06f29348 refac 2025-09-28 13:22:39 -05:00
Timothy Jaeryang Baek 4c2c58214c refac 2025-09-28 13:09:09 -05:00
Tim Jaeryang Baek 9a8c9ecb4d Merge pull request #17823 from ITOTI-Y/main
fix: correct messageId parameter in createMessagesList call
2025-09-28 12:57:56 -05:00
Timothy Jaeryang Baek 95462a394a refac 2025-09-28 12:54:31 -05:00
Tim Jaeryang Baek 11fd0735d1 Merge pull request #17851 from Classic298/feat-toggle-chat-title-in-browser-tab
feat: add toggle to show/hide chat title in browser tab
2025-09-28 12:53:19 -05:00
Tim Jaeryang Baek 2cb35e7142 Merge pull request #17843 from Classic298/feat/clone-default-group-permissions
feat/chore: Clone default group permissions for new groups and remove redundant modal
2025-09-28 12:50:59 -05:00
Tim Jaeryang Baek e71ed76165 Merge pull request #17857 from silentoplayz/feat-bulk-unarchive
feat: add backend handling for unarchiving all chats
2025-09-28 12:45:49 -05:00
Timothy Jaeryang Baek 742e2ff193 refac 2025-09-28 12:42:02 -05:00
Timothy Jaeryang Baek 3aad157006 refac 2025-09-28 12:26:13 -05:00
silentoplayz a572cf4842 feat: add backend handling for unarchiving all chats
The previous implementation for unarchiving all chats in `ArchivedChatsModal.svelte` was inefficient, as it sent a separate request for each chat, which could potentially overload the server.

This commit introduces a new backend endpoint, `/chats/unarchive/all`, to handle the bulk unarchiving of all chats for a user with a single API call.

The frontend has been updated to use this new endpoint, resolving the performance issue by minimizing the number of requests to the server.
2025-09-28 13:25:34 -04:00
Timothy Jaeryang Baek 97dae432ab refac 2025-09-28 12:23:20 -05:00
Timothy Jaeryang Baek db9d3d386d refac: mcp spec/response handling 2025-09-28 12:22:11 -05:00
Timothy Jaeryang Baek 4aa41aa139 refac 2025-09-28 12:00:19 -05:00
Tim Jaeryang Baek 5568a312cd Merge pull request #17835 from BakirBukvic/main
feat/ i18n: Add Bosnian (Latin) language
2025-09-28 11:48:35 -05:00
Tim Jaeryang Baek 2915bcf28c Merge pull request #17833 from ShirasawaSama/patch-20
i18n: improved Chinese (zh-CN & zh-TW) translation
2025-09-28 11:47:06 -05:00
Tim Jaeryang Baek 54ed62f7b0 Merge pull request #17832 from silentoplayz/pinned-chats-not-updating
fix: pinned chats not updating after archiving all chats
2025-09-28 11:46:53 -05:00
Tim Jaeryang Baek baf913c832 Merge pull request #17850 from silentoplayz/remove-unused-params
chore: Remove unused parameter definitions
2025-09-28 11:37:50 -05:00
google-labs-jules[bot] 57c02b2523 feat: add toggle to show/hide chat title in browser tab
This commit introduces a new setting in the Interface settings that allows users to control whether the chat title is used as the browser's tab title.

The following changes were made:
- Added `useChatTitleAsTabTitle` to the `Settings` type in `src/lib/stores/index.ts`.
- Added a toggle switch in `src/lib/components/chat/Settings/Interface.svelte` to manage this new setting.
- Updated `src/lib/components/chat/Chat.svelte` to conditionally set the document title based on the `useChatTitleAsTabTitle` setting.
2025-09-28 16:10:00 +00:00
silentoplayz 670dfca6a8 remove: used advanced params 2025-09-28 11:41:12 -04:00
Classic298 fb3eeaa126 Update Groups.svelte 2025-09-28 16:56:04 +02:00
google-labs-jules[bot] 30550d9190 Feat: Add warning for conflicting group permissions
This change introduces a visual warning in the group settings page. The warning appears when an admin attempts to disable a permission for a group that is already enabled in the default 'user' group. This is necessary because permissions are additive, and disabling a permission in a specific group will not revoke it if it's enabled in the default group.

To achieve this, the following changes were made:
- A new `PermissionSwitch.svelte` component was created to encapsulate the permission switch and its warning logic, avoiding redundant code.
- The `Groups.svelte` component was updated to correctly fetch the default user group's permissions.
- The `Permissions.svelte` component was refactored to use the new `PermissionSwitch.svelte` component, making the code cleaner and more maintainable.
2025-09-28 13:42:10 +00:00
google-labs-jules[bot] cafe748b1b feat(groups): Clone default group permissions for new groups
When creating a new user group, the permissions for the new group will now be pre-populated with the same permissions as the "default user group".

This is achieved by removing the separate `AddGroupModal` and instead using the `EditGroupModal` for both creating and editing groups. When creating a new group, the `EditGroupModal` is now pre-populated with the default permissions, saving administrators from having to manually configure them each time.

This change simplifies the codebase by removing a redundant component and directly addresses the user's request to streamline the group creation process.
2025-09-28 12:56:39 +00:00
Classic298 1773a4d4b8 typo 2025-09-28 11:14:27 +02:00
Classic298 27af4506df Update config.py 2025-09-28 11:10:37 +02:00
Classic298 01d4a8ab7a Update factory.py 2025-09-28 11:06:29 +02:00
Classic298 8dc43f9e3a Create milvus_multitenancy.py 2025-09-28 11:05:15 +02:00
Classic298 50a2204046 add env vars 2025-09-28 11:04:24 +02:00
bakir baed3035ac add bosnian to languages.json 2025-09-28 09:23:33 +02:00
bakir 88bfe777cd add Bosnian language pack 2025-09-28 09:19:51 +02:00
Shirasawa f912b06b42 i18n: improve Chinese translation 2025-09-28 03:39:00 +00:00
silentoplayz 2b5dca2929 fix: pinned chats not updating after archiving all chats
This commit fixes a UI bug where pinned chats would remain visible in the sidebar after all chats were archived from the Data Controls menu.

The `archiveAllChatsHandler` in `DataControls.svelte` has been updated to clear the `pinnedChats` store, ensuring the sidebar UI is correctly updated.
2025-09-27 22:44:04 -04:00
Tim Jaeryang Baek 2c80f60f3e Merge pull request #17812 from silentoplayz/long-text-truncation
fix: truncate long names in delete confirmation modals
2025-09-27 16:30:33 -05:00
Tim Jaeryang Baek 1283a8ce79 Merge pull request #17819 from silentoplayz/fix-layout-and-i18n
fix: i81n.t and correct button layout issue
2025-09-27 16:30:07 -05:00
ITOTI 68a4fdf0e9 fix: correct messageId parameter in createMessagesList call
- Fix FloatingButtons component to use messageId instead of id
2025-09-27 20:41:05 +00:00
silentoplayz 27cd87e9ad fix: i81n.t and correct button layout issue
1.  **i18n Regression:** A latent bug in `src/routes/(app)/workspace/models/create/+page.svelte` was causing an `i18n.t is not a function` error. This was due to an incorrect call to the `i18n` Svelte store. The fix corrects the call to use the proper auto-subscription syntax (`$i18n.t()`).

2.  **Vertical Button Text:** In `src/lib/components/playground/Chat.svelte`, the "Assistant"/"User" role button's text was displaying vertically. This was caused by a `flex-1` class on its container, which has been removed.
2025-09-27 15:45:25 -04:00
Timothy Jaeryang Baek 272c6f5ec5 refac 2025-09-27 04:38:54 -05:00
Timothy Jaeryang Baek 86ef57f6c3 refac 2025-09-27 04:33:00 -05:00
Timothy Jaeryang Baek b4eea78aff refac 2025-09-27 04:06:42 -05:00
Timothy Jaeryang Baek 1a18928c94 enh: reply to message 2025-09-27 04:05:12 -05:00
Timothy Jaeryang Baek d7c54d92b5 refac 2025-09-26 22:57:38 -05:00
silentoplayz e62f2b3c75 fix: truncate more long names in delete confirmation modals 2025-09-26 23:56:18 -04:00
Timothy Jaeryang Baek 45e1c72613 refac 2025-09-26 22:53:57 -05:00
Timothy Jaeryang Baek 23f62a7312 enh: tool server import/export 2025-09-26 22:53:02 -05:00
Timothy Jaeryang Baek d1a6c6b209 refac 2025-09-26 22:09:27 -05:00
Timothy Jaeryang Baek bad7d69a58 feat/enh: external tool server manual JSON spec 2025-09-26 22:02:48 -05:00
Timothy Jaeryang Baek a05dab6298 refac 2025-09-26 21:16:34 -05:00
Timothy Jaeryang Baek c80bb31968 refac/enh: folder optimization 2025-09-26 20:48:17 -05:00
Timothy Jaeryang Baek 54beeeaf72 refac: tools 2025-09-26 19:01:22 -05:00
Tim Jaeryang Baek 1de5827eb3 Merge pull request #17805 from silentoplayz/fix-username-truncation
fix: truncate long usernames in UI
2025-09-26 17:53:37 -05:00
Timothy Jaeryang Baek b77848244b refac: user valves 2025-09-26 17:49:42 -05:00
silentoplayz 16cf973ce5 fix: truncate long usernames in UI
Long usernames were causing layout issues in several parts of the application. This change truncates long usernames with an ellipsis to prevent them from overflowing.

The following areas have been fixed:
- Edit User modal
- User Chats modal
- Edit User Group modal
- Users table in the admin overview

fix: truncate long usernames in UI

Long usernames were causing layout issues in several parts of the application. This change truncates long usernames with an ellipsis to prevent them from overflowing.

The following areas have been fixed:
- Edit User modal
- User Chats modal
- Edit User Group modal
- Users table in the admin overview

Revert "fix: truncate long usernames in UI"

This reverts commit b623fdc95d0c494228b49f9369db3bbb3042cef0.
2025-09-26 18:30:48 -04:00
Tim Jaeryang Baek 807a8be299 Merge pull request #17767 from ShirasawaSama/patch-31
fix: fixed the hover effect for the MessageInput Integrations button
2025-09-26 16:58:15 -05:00
Tim Jaeryang Baek 5765a87720 Merge pull request #17779 from silentoplayz/fix-set-as-default
fix: 'Set as default' click position
2025-09-26 16:57:31 -05:00
Timothy Jaeryang Baek a1829f6a3e refac: styling 2025-09-26 16:55:38 -05:00
Timothy Jaeryang Baek b8c3e5ed3e refac 2025-09-26 16:43:12 -05:00
Tim Jaeryang Baek d10b518f42 Merge pull request #17801 from Classic298/feature/notes-public-sharing-permission
feat: add permission toggle for public sharing of notes
2025-09-26 16:11:17 -05:00
Timothy Jaeryang Baek 4997ef2662 refac 2025-09-26 15:57:03 -05:00
Timothy Jaeryang Baek cda4c95c86 fix: default tool calling 2025-09-26 15:50:16 -05:00
google-labs-jules[bot] 41e4e7395c feat: add permission toggle for public sharing of notes
This commit introduces a new permission toggle that allows administrators to control whether users can publicly share their notes.

- Adds a new environment variable `USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING` to control the default setting.
- Adds a `public_notes` permission to the `sharing` section of the user permissions.
- Adds a toggle switch to the admin panel for managing this permission.
- Implements backend logic to enforce the permission when a user attempts to share a note publicly.
2025-09-26 20:48:01 +00:00
Timothy Jaeryang Baek 680783266c refac/fix: tool response header type check 2025-09-26 15:42:17 -05:00
Timothy Jaeryang Baek 99d7773230 refac: styling 2025-09-26 15:00:06 -05:00
Timothy Jaeryang Baek ac08529380 refac 2025-09-26 14:40:30 -05:00
Tim Jaeryang Baek a80d5240f6 Merge pull request #17796 from silentoplayz/truncate-selector-tags
fix: truncate long filter tags in model selector and prevent wrapping
2025-09-26 14:35:58 -05:00
Tim Jaeryang Baek 9b86932714 Merge pull request #17797 from SZegotaM/dev
i18n(de-DE): add translations for newly added UI items + other translation fixes
2025-09-26 14:35:12 -05:00
Timothy Jaeryang Baek 0431ad9cc4 refac: get_discovery_urls 2025-09-26 14:34:26 -05:00
Tim Jaeryang Baek f8a3ed2d18 Merge pull request #17770 from Classic298/feat-milvus-diskann-support
feat: Add DISKANN index type support for Milvus
2025-09-26 14:23:53 -05:00
SZegotaM dcb0933149 Update German translations in translation.json
Added missing German translation
2025-09-26 21:16:35 +02:00
silentoplayz b516431569 fix: truncate long filter tags in model selector and prevent wrapping
This commit addresses an issue where long filter tags at the top of the model selector dropdown were not truncated correctly and would wrap to a new line, causing layout issues.

- A hard character limit of 16 characters is applied to the filter tags within the `Selector.svelte` component. Tags longer than 16 characters are truncated with an ellipsis (...) directly in the code. The full tag name remains available in the tooltip.
- The `whitespace-nowrap` class has been added to the tag container to ensure that the tags remain on a single, horizontally scrollable line.
2025-09-26 15:03:44 -04:00
Tim Jaeryang Baek af6a25eed7 Merge pull request #17769 from Classic298/fix-milvus-limit-error
Fix: milvus error because the limit set to None by default
2025-09-26 12:54:00 -05:00
Tim Jaeryang Baek acdafcd18d Merge pull request #17777 from ShirasawaSama/patch-33
fix: show error message when the uploading file is modified
2025-09-26 12:52:09 -05:00
Tim Jaeryang Baek a97cdc0da0 Merge pull request #17791 from silentoplayz/truncate-models-tag
Fix: truncate long model tags with a 32 characters limit
2025-09-26 12:46:45 -05:00
silentoplayz ac6292b812 Fix: truncate long model tags with a character limit
Long model tags on the Models page in the workspace section were not truncated consistently, which could cause layout issues.

This change implements a hard character limit of 32 characters on the model tags. Tags longer than 32 characters are truncated with an ellipsis (...) directly in the code. The full tag name remains available in the tooltip.
2025-09-26 13:28:35 -04:00
Tim Jaeryang Baek d126c23ef8 Merge pull request #17783 from joaoback/patch-10
Update translation.json (pt-BR)
2025-09-26 12:26:25 -05:00
joaoback 36da2b06a5 Update translation.json (pt-BR)
translation of the new items that were included in the latest version.
2025-09-26 09:18:27 -03:00
silentoplayz 90a5b3befb fix: 'Set as default' 2025-09-26 06:42:23 -04:00
Shirasawa 42faa63227 fix: show error message when the uploading file is modified 2025-09-26 17:31:16 +08:00
Classic298 9e3d5407ae Merge branch 'open-webui:main' into feat-milvus-diskann-support 2025-09-26 10:43:01 +02:00
Classic298 b550d78905 Merge branch 'open-webui:main' into fix-milvus-limit-error 2025-09-26 10:42:53 +02:00
google-labs-jules[bot] 123dbf152e feat: Add DISKANN index type support for Milvus
This commit introduces support for the DISKANN index type in the Milvus vector database integration.

Changes include:
- Added `MILVUS_DISKANN_MAX_DEGREE` and `MILVUS_DISKANN_SEARCH_LIST_SIZE` configuration variables.
- Updated the Milvus client to recognize and configure the DISKANN index type during collection creation.
2025-09-26 06:54:06 +00:00
google-labs-jules[bot] e7ccaf6e78 Fix: milvus error because the limit set to None by default
The pymilvus library expects -1 for unlimited queries, but the code was passing None, which caused a TypeError. This commit changes the default value of the limit parameter in the query method from None to -1. It also updates the call site in the get method to pass -1 instead of None and updates the type hint and a comment to reflect this change.
2025-09-26 06:39:54 +00:00
Shirasawa 15bd5040f1 fix: fixed the hover effect for the MessageInput Integrations button 2025-09-26 11:56:34 +08:00
Tim Jaeryang Baek 598282cf75 Merge pull request #17747 from open-webui/dev
0.6.31
2025-09-25 15:28:06 -05:00
Timothy Jaeryang Baek f7ea60b500 Update CHANGELOG.md 2025-09-25 15:26:26 -05:00
Timothy Jaeryang Baek 2fa222d00a refac 2025-09-25 15:20:13 -05:00
Timothy Jaeryang Baek 6920fed97e refac 2025-09-25 14:53:21 -05:00
Timothy Jaeryang Baek 528e58155b doc: changelog 2025-09-25 14:49:23 -05:00
Tim Jaeryang Baek 503fc354cc Merge pull request #17752 from Classic298/changelog-done
chore: Changelog 0.6.31
2025-09-25 14:45:34 -05:00
Classic298 4eec423950 Update CHANGELOG.md 2025-09-25 21:42:07 +02:00
Timothy Jaeryang Baek 52bc5306b0 refac 2025-09-25 14:37:30 -05:00
Timothy Jaeryang Baek 90e4b49b88 refac/fix: onedrive input menu styling issue 2025-09-25 14:36:25 -05:00
Timothy Jaeryang Baek 750a659a9f security: svg xss fix 2025-09-25 14:29:50 -05:00
Classic298 6fde0f2960 Update CHANGELOG.md 2025-09-25 21:20:39 +02:00
Timothy Jaeryang Baek a0e323b89b refac 2025-09-25 14:20:02 -05:00
Classic298 a68ac4efe6 Update CHANGELOG.md 2025-09-25 21:14:46 +02:00
Timothy Jaeryang Baek 393bdd7ead chore: bump 2025-09-25 14:13:12 -05:00
Timothy Jaeryang Baek 7f411dd5cc feat/enh: perplexity search support 2025-09-25 14:02:46 -05:00
Classic298 c790ec7625 Update CHANGELOG.md 2025-09-25 20:58:17 +02:00
Classic298 c46f3d05a8 Update CHANGELOG.md 2025-09-25 20:56:59 +02:00
Timothy Jaeryang Baek b38d59dee6 refac 2025-09-25 13:53:56 -05:00
Timothy Jaeryang Baek da661756fa refac/fix: proper notes db operations 2025-09-25 13:47:43 -05:00
Classic298 46967ffd04 Update CHANGELOG.md 2025-09-25 20:47:12 +02:00
Timothy Jaeryang Baek 5b1f9e3e21 refac: search modal input filters behaviour 2025-09-25 13:25:58 -05:00
Timothy Jaeryang Baek 776d4773a9 refac: note & chat sticky attachment 2025-09-25 13:21:47 -05:00
Timothy Jaeryang Baek eeed0df743 refac 2025-09-25 13:17:58 -05:00
Timothy Jaeryang Baek b1006a2b57 refac 2025-09-25 13:16:52 -05:00
Timothy Jaeryang BaekandJacob Leksan 53cd660de7 refac/fix: valves array type handling
Co-Authored-By: Jacob Leksan <63938553+jmleksan@users.noreply.github.com>
2025-09-25 13:13:03 -05:00
Tim Jaeryang Baek a7061383e8 Merge pull request #17753 from rgaricano/dev-es_ES
UPD: i18n _ es-ES Translation v0.6.31
2025-09-25 13:01:10 -05:00
Tim Jaeryang Baek 97529e8ae4 Merge pull request #17749 from ShirasawaSama/patch-2
i18n: improve zh-TW translation
2025-09-25 13:00:58 -05:00
Tim Jaeryang Baek c579e1fe44 Merge pull request #17748 from ShirasawaSama/i18n/improve-chinese-translation
i18n: improve zh-CN translation
2025-09-25 13:00:49 -05:00
Classic298 17080a1c00 REORDER BY PRIORITY 2025-09-25 19:41:54 +02:00
_00_ e6b3c91343 UPD: i18n - es-ES Translation v0.6.31
UPD: i18n - es-ES Translation v0.6.31

Update of new strings.
2025-09-25 19:39:12 +02:00
Classic298 deb64fa6a6 Update CHANGELOG.md 2025-09-25 19:37:54 +02:00
Classic298 9bc8784e3e Changelog dev (#20)
* 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
2025-09-25 19:37:18 +02:00
Shirasawa d6f93753c1 i18n: improve zh-TW translation 2025-09-26 01:23:33 +08:00
Shirasawa 0f12d512c4 i18n: improve zh-CN translation 2025-09-26 01:14:09 +08:00
Tim Jaeryang Baek b76d234f97 Merge pull request #17607 from sihyeonn/perf/sh-notes
perf: optimize notes query and separate access control logic
2025-09-25 12:13:42 -05:00
Tim Jaeryang Baek 4aa5574ba7 Merge pull request #17746 from andrewbbaek/dev
chore: add IDs to elements for better maintainability
2025-09-25 12:07:35 -05:00
Andrew Baek ead1c1881c Merge branch 'dev' of https://github.com/andrewbbaek/open-webui into dev 2025-09-26 01:57:38 +09:00
Andrew Baek 3df5826d93 moved sidebar-folder-button from class to id 2025-09-26 01:56:45 +09:00
Tim Jaeryang Baek cd417ca0ba Merge pull request #17744 from Classic298/fix-rag-full-context
Fix: Prevent RAG queries when all files are in full context
2025-09-25 11:55:41 -05:00
Timothy Jaeryang Baek 4f8f8e0e0b chore: dep bump 2025-09-25 11:51:04 -05:00
Timothy Jaeryang Baek 05d7d1e562 chore: format 2025-09-25 11:43:10 -05:00
Andrew Baek 08f4a2dff8 Merge branch 'open-webui:dev' into dev 2025-09-26 01:31:20 +09:00
Timothy Jaeryang Baek ff44fa242d refac 2025-09-25 11:28:33 -05:00
Andrew Baek e4f27ab75f Merge branch 'open-webui:dev' into dev 2025-09-26 01:25:50 +09:00
Andrew Baek 86f024b9d7 added id and class 2025-09-26 01:23:58 +09:00
Timothy Jaeryang Baek 3c7d01163d refac 2025-09-25 11:02:49 -05:00
google-labs-jules[bot] 4a7e1b93e5 Fix: Prevent RAG queries when all files are in full context
This commit fixes an issue where Retrieval-Augmented Generation (RAG)
queries were still being generated even when all attached files were set
to 'full context' mode. This was inefficient as the full content of the
files was already available to the model.

The `chat_completion_files_handler` in `backend/open_webui/utils/middleware.py`
has been updated to:
- Check if all attached files have the `context: 'full'` property.
- Skip the `generate_queries` step if all files are in full context mode.
- Pass a `full_context=True` flag to the `get_sources_from_items`
  function to ensure it fetches the entire document content instead of
  performing a vector search.

This change ensures that RAG queries are only generated when necessary,
improving the efficiency of the system.
2025-09-25 15:54:58 +00:00
Timothy Jaeryang Baek 98883a68f2 refac 2025-09-25 02:18:10 -05:00
Tim Jaeryang Baek b8a51de977 Merge pull request #17721 from Classic298/fix-image-download-filename
fix: Use generic filename for downloaded images
2025-09-25 02:02:19 -05:00
Timothy Jaeryang Baek cd7bd0aa20 refac 2025-09-25 02:00:02 -05:00
Timothy Jaeryang Baek 4d16cf6bf3 refac 2025-09-25 01:59:18 -05:00
Timothy Jaeryang Baek 422d38fd11 refac 2025-09-25 01:56:09 -05:00
Timothy Jaeryang Baek 879abd7fee refac 2025-09-25 01:53:10 -05:00
Timothy Jaeryang Baek 77e971dd9f feat: oauth2.1 mcp integration 2025-09-25 01:49:16 -05:00
Classic298 4f22a1c5e4 Update translation.json 2025-09-25 07:55:17 +02:00
Classic298 e201dc995c Update ImagePreview.svelte 2025-09-25 07:54:58 +02:00
Timothy Jaeryang Baek 972be4eda5 enh: oauth2.1 dynamic client registration 2025-09-25 00:28:13 -05:00
Timothy Jaeryang Baek 34d16791a8 refac 2025-09-24 23:27:45 -05:00
Tim Jaeryang Baek d3b09c6a02 Merge pull request #17223 from itk-dev/feature/session-in-redis
feat: Added support for redis as session storage
2025-09-24 23:23:56 -05:00
Timothy Jaeryang Baek 27d61307cd refac: tools valves 2025-09-24 21:12:25 -05:00
Timothy Jaeryang Baek 685cca5bd5 chore: dep bump 2025-09-24 18:58:17 -05:00
Timothy Jaeryang Baek bd02a456ab chore: langchain dep bump 2025-09-24 18:55:33 -05:00
Timothy Jaeryang Baek e5daec32ba refac 2025-09-24 17:16:07 -05:00
Timothy Jaeryang Baek b03529b006 refac 2025-09-24 17:05:37 -05:00
Timothy Jaeryang Baek 612a52d7bb refac 2025-09-24 16:34:41 -05:00
Timothy Jaeryang Baek aa6f63a335 enh: search modal actions 2025-09-24 16:29:02 -05:00
Timothy Jaeryang Baek 9f0010e234 enh: channel model @ image support 2025-09-24 16:08:36 -05:00
Timothy Jaeryang Baek e7061b70e9 refac 2025-09-24 16:01:31 -05:00
Timothy Jaeryang Baek fe65fe0b97 refac: ollama cloud web search count support 2025-09-24 15:58:56 -05:00
Tim Jaeryang Baek 3d2f7e3143 Merge pull request #17715 from ShirasawaSama/revert-17498-patch-19
Revert "feat: Dynamically load CodeEditor.svelte to improve first-screen loading speed (-1MB)"
2025-09-24 15:40:08 -05:00
Timothy Jaeryang Baek 1df5f1f9f6 revert: code editor dynamic import 2025-09-24 15:39:55 -05:00
Timothy Jaeryang Baek 05c46008da refac 2025-09-24 15:38:09 -05:00
Tim Jaeryang Baek 1493a5fd32 Merge pull request #17722 from Classic298/bugfix/hidden-models-in-notes
Fix: Hide hidden models in notes section
2025-09-24 15:37:52 -05:00
Timothy Jaeryang Baek 4b6d34438b refac 2025-09-24 15:20:31 -05:00
Timothy Jaeryang Baek e06489d92b enh: search_ollama_cloud 2025-09-24 15:19:05 -05:00
google-labs-jules[bot] c32b407f8a Fix(notes): Hide hidden models in notes section
This commit fixes a bug where hidden models were still visible and selectable in the notes section. The following changes were made:

- The model dropdown in the chat section of the note editor is now filtered to exclude hidden models.
- The default model selection logic in the note editor is now filtered to exclude hidden models.
2025-09-24 19:43:07 +00:00
google-labs-jules[bot] ab30b105e2 fix(image): Use generic filename for downloaded images
Previously, when downloading a generated image, the filename was set to the AI's response text. This was not ideal as the response text could be long and contain characters that are not suitable for filenames.

This commit changes the behavior to use a generic, translatable filename for downloaded images. The new filename is 'ai_generated_image' (or its translation), which is more user-friendly and consistent.
2025-09-24 19:39:08 +00:00
Timothy Jaeryang Baek f27ab14bd8 refac 2025-09-24 12:37:19 -05:00
Timothy Jaeryang Baek 23a51f2d01 refac: robust file upload failed handling 2025-09-24 12:17:01 -05:00
Timothy Jaeryang Baek 05732de898 refac 2025-09-24 11:36:17 -05:00
Tim Jaeryang Baek 316f1d6ff0 Merge pull request #17717 from Classic298/patch-1
add youtube
2025-09-24 12:34:37 -04:00
Classic298 b4d8287946 add youtube 2025-09-24 18:31:48 +02:00
Timothy Jaeryang Baek 0dee15ba97 refac/enh: include foldered chats in ref chat input menu 2025-09-24 11:27:19 -05:00
Timothy Jaeryang Baek 0e3b6b3b8f refac 2025-09-24 11:22:48 -05:00
Timothy Jaeryang Baek 32086b6ecd refac 2025-09-24 11:20:39 -05:00
Timothy Jaeryang Baek 2a95cbcef7 enh: attach webpage input menu 2025-09-24 11:11:26 -05:00
Shirasawa 6614c73b6c Revert "feat: Dynamically load CodeEditor.svelte to improve first-screen loading speed (-1MB)" 2025-09-25 00:10:01 +08:00
Timothy Jaeryang Baek 2904a78222 refac 2025-09-24 10:14:38 -05:00
Timothy Jaeryang Baek ac879513e5 enh: channel read/write perm 2025-09-24 10:09:59 -05:00
Timothy Jaeryang Baek 6d69ea3ac7 refac 2025-09-24 09:46:19 -05:00
Timothy Jaeryang Baek f096e99059 refac: rag context handling 2025-09-24 09:38:14 -05:00
Timothy Jaeryang Baek 72d19e44dc refac 2025-09-24 09:37:37 -05:00
Timothy Jaeryang Baek b25c7da154 refac: valves 2025-09-24 09:21:53 -05:00
Timothy Jaeryang Baek b8086c5edf refac: folder delete logic 2025-09-24 09:04:54 -05:00
Timothy Jaeryang Baek 91a9f32904 fix: folder expanded update 2025-09-24 08:00:58 -05:00
Timothy Jaeryang Baek 199f371f58 refac 2025-09-24 07:17:41 -05:00
Tim Jaeryang Baek 6cdadc73f2 Merge pull request #17705 from aleixdorca/dev
i18n: Update the Catalan translation file
2025-09-24 07:57:16 -04:00
Timothy Jaeryang Baek c5a967e05f refac 2025-09-24 06:56:50 -05:00
Timothy Jaeryang Baek 651f385ba5 fix: oauth refresh server metadata 2025-09-24 06:56:24 -05:00
Timothy Jaeryang Baek f25a144e09 refac 2025-09-24 06:52:44 -05:00
Timothy Jaeryang Baek 5eaee44daa refac 2025-09-24 06:49:39 -05:00
Aleix Dorca 9c76fb267a Update catalan translation.json 2025-09-24 11:21:27 +02:00
Aleix Dorca cf49b823b0 Merge branch 'open-webui:dev' into dev 2025-09-24 10:59:32 +02:00
Andrew Baek 5be58f2601 Merge branch 'open-webui:dev' into dev 2025-09-24 17:10:48 +09:00
Timothy Jaeryang Baek 1c64269387 refac 2025-09-23 16:51:31 -05:00
Timothy Jaeryang Baek f33d6eabdf chore: bump azure identity 2025-09-23 16:48:31 -05:00
Tim Jaeryang Baek 7ae42f5ae3 Merge pull request #17660 from HikkaTown/i18n/russian-translation-updated
i18n: Complete Russian translation for Open WebUI
2025-09-23 17:30:08 -04:00
Tim Jaeryang Baek 9710060c27 Merge pull request #17645 from ShirasawaSama/patch-28
feat: only retain one language supports for highlight.js to reduce bundle size (-1.58MB)
2025-09-23 16:28:05 -04:00
Timothy Jaeryang Baek 06e5c00f24 refac 2025-09-23 03:52:43 -04:00
Timothy Jaeryang Baek f7b0fdde2a refac 2025-09-23 03:42:25 -04:00
Timothy Jaeryang Baek 7aa3903b6b refac 2025-09-23 03:36:11 -04:00
Timothy Jaeryang Baek 6bf4ba523d refac 2025-09-23 03:35:00 -04:00
Timothy Jaeryang Baek c42c0436f2 refac 2025-09-23 03:33:24 -04:00
Timothy Jaeryang Baek 61f20acf61 refac 2025-09-23 03:32:25 -04:00
Timothy Jaeryang Baek c55afc4255 refac 2025-09-23 03:19:36 -04:00
Timothy Jaeryang Baek f1bbf3a91e refac 2025-09-23 03:05:38 -04:00
Timothy Jaeryang Baek de7f7b3d85 refac 2025-09-23 02:40:59 -04:00
Timothy Jaeryang Baek 777e81f7a8 feat: experimental mcp support 2025-09-23 02:03:26 -04:00
Timothy Jaeryang Baek aeb5288a3c refac 2025-09-22 23:28:19 -04:00
Timothy Jaeryang Baek 2771c26729 refac 2025-09-22 20:03:44 -04:00
Timothy Jaeryang Baek e4e97e727e enh: note drag handle 2025-09-22 20:02:37 -04:00
Timothy Jaeryang Baek 1afa366dcb refac 2025-09-22 14:46:47 -04:00
Zaytsev Gennadiy 7c1b6f9157 i18n: Complete Russian translation for Open WebUI 2025-09-22 21:35:57 +03:00
Tim Jaeryang Baek 651dea3864 Merge pull request #17640 from ShirasawaSama/patch-27
feat: do not initiate requests when the SearchModal is not open to speed up page loading
2025-09-22 13:27:56 -04:00
Tim Jaeryang Baek 96e71cf181 Merge pull request #17647 from BoFFire/patch-2
i18n: updating kabyle translation
2025-09-22 13:22:20 -04:00
Tim Jaeryang Baek ac5cc4a050 Merge pull request #17657 from helax28/dev
Updated the french translations
2025-09-22 13:22:00 -04:00
ButterflyOfFire 48dc333a3d Update translation.json 2025-09-22 10:28:27 +01:00
ButterflyOfFire 628b753a35 Update translation.json 2025-09-22 10:07:54 +01:00
ButterflyOfFire c28fbcb232 i18n: updating kabyle translation
Translation progress : 90%
2025-09-22 09:51:55 +01:00
Shirasawa 2edfde1990 feat: only retain one language supports for highlight.js to reduce bundle size 2025-09-22 16:00:46 +08:00
Shirasawa 01fcd057b7 feat: do not initiate requests when the SearchModal is not open to speed up page loading 2025-09-22 12:01:29 +08:00
Tim Jaeryang Baek fb3b736a35 Merge pull request #17635 from open-webui/main
dev
2025-09-21 19:15:06 -04:00
athiththan 064d9d6141 Updated the french translations 2025-09-21 21:45:31 +02:00
Timothy Jaeryang Baek fd7385c392 refac 2025-09-21 03:12:24 -04:00
Timothy Jaeryang Baek 834824ce7b refac 2025-09-21 03:06:54 -04:00
Timothy Jaeryang Baek 466d5bb696 refac: add separate Client IDs for OneDrive 2025-09-21 01:40:14 -04:00
Timothy Jaeryang Baek 6e4a2f18e1 refac 2025-09-21 00:14:43 -04:00
Tim Jaeryang Baek 5e6e25f8c8 Merge pull request #17615 from Cyp9715/dev
i18n: Updated korean translation.
2025-09-20 23:07:01 -05:00
Tim Jaeryang Baek 428f9a22c4 Merge pull request #17616 from Kylapaallikko/dev
i18n: Update fi-FI translation.json
2025-09-20 16:10:06 -05:00
Tim Jaeryang Baek 92d79150bd Merge pull request #17618 from rgaricano/dev-es_ES
UPD: i18n es-ES Translation v.0.6.30
2025-09-20 16:04:13 -05:00
Timothy Jaeryang Baek 6bc5d331a2 doc: readme 2025-09-20 12:45:10 -05:00
Andrew Baek a7ae280a4f Update MessageInput.svelte 2025-09-21 01:01:29 +09:00
Andrew Baek 2a1c1e34dc Merge branch 'dev' of https://github.com/andrewbbaek/open-webui into dev 2025-09-21 00:52:28 +09:00
_00_ 4b4fda46cf Update translation.json 2025-09-20 14:46:18 +02:00
_00_ 4b029048db UPD: i18n es-ES Translation v.0.6.30
### Update i18n es-ES Translation v.0.6.30

Added new strings
2025-09-20 14:39:23 +02:00
Kylapaallikko 34feaeeb96 Update fi-FI translation.json
Added missing translations and fixed typos
2025-09-20 14:53:04 +03:00
ByoungGyu Lee a6c53303dd i18n(ko-KR): Updated korean translation. 2025-09-20 20:44:51 +09:00
ByoungGyu Lee 204484d8e7 i18n(ko-KR): Updated korean translations. 2025-09-20 20:40:50 +09:00
ByoungGyu Lee 41e4f33c1d Merge remote-tracking branch 'remote/dev' into dev 2025-09-20 19:43:01 +09:00
Timothy Jaeryang Baek 94770f6059 refac 2025-09-20 01:33:36 -05:00
Timothy Jaeryang Baek c4fed37af9 refac 2025-09-20 01:25:44 -05:00
Timothy Jaeryang Baek b0279c5090 refac 2025-09-20 01:25:00 -05:00
Timothy Jaeryang Baek 73f8237d43 refac 2025-09-20 01:19:23 -05:00
Timothy Jaeryang Baek 4ad7430986 refac 2025-09-20 01:15:27 -05:00
Timothy Jaeryang Baek 27a91cc80a refac: styling 2025-09-20 00:56:25 -05:00
Tim Jaeryang Baek 820321ad55 Merge pull request #17608 from sihyeonn/fix/sh-prevent-memory-leaks-on-ollama
fix: prevent memory leaks in file handling and HTTP connections
2025-09-19 22:22:08 -05:00
Sihyeon Jang 7042318c34 fix: prevent memory leaks in file handling and HTTP connections
- Fix file handle memory leak in download_file_stream by properly closing and reopening files
- Add requests.Session context manager for proper HTTP connection cleanup
- Remove unnecessary file.seek(0) after file reopening
- Add timeout to prevent hanging connections

This prevents memory accumulation during large file downloads and ensures
proper resource cleanup in all scenarios.

Signed-off-by: Sihyeon Jang <sihyeon.jang@navercorp.com>
2025-09-20 06:37:10 +09:00
Sihyeon Jang 6ae6cc9741 perf: optimize get_notes_by_user_id to reduce database queries
- Replace inefficient memory-based filtering with database-level filtering
- Add proper access control conditions to SQL query
- Reduce memory usage by filtering at database level instead of loading all notes
- Maintain access control validation with post-filtering for complex cases

This change significantly improves performance for users with many notes
by reducing the number of database queries and memory usage.

Signed-off-by: Sihyeon Jang <sihyeon.jang@navercorp.com>
2025-09-20 06:33:24 +09:00
Tim Jaeryang Baek dbd7b90123 Merge pull request #17593 from ShirasawaSama/patch-25
refactor: refactor editor's collaboration to reduce package size (-390KB) and minimize compile errors
2025-09-19 13:16:28 -05:00
Tim Jaeryang Baek afad34fd09 Merge pull request #17600 from aindriu80/update-ga-strings-19-sept
i18n: Updated Irish (ga) translation
2025-09-19 13:13:28 -05:00
Aindriú Mac Giolla Eoin 167f4ba8ad i18n(ga): Updated Irish translations 2025-09-19 16:02:56 +01:00
Shirasawa 7f6b260c35 feat: refactor editor's collaboration to reduce package size and minimize errors 2025-09-19 17:28:16 +08:00
Timothy Jaeryang Baek 62517f01e5 refac: built-in chart js 2025-09-19 02:58:29 -05:00
Timothy Jaeryang Baek 293531549c refac: alpine injection 2025-09-19 02:19:48 -05:00
Timothy Jaeryang Baek 60db9ec8ef refac: built-in tools ui component support 2025-09-19 01:38:44 -05:00
Timothy Jaeryang Baek 7528f24b61 refac: styling 2025-09-19 00:15:03 -05:00
Timothy Jaeryang Baek e4c4ba0979 fix: oauth token 2025-09-19 00:10:48 -05:00
Timothy Jaeryang Baek a89ffccd7e refac 2025-09-19 00:10:39 -05:00
Timothy Jaeryang Baek 7be5b7f50f refac 2025-09-18 22:03:14 -05:00
Timothy Jaeryang Baek a5d8882bba refac 2025-09-18 21:25:26 -05:00
Timothy Jaeryang Baek 07c5b25bc8 feat: tool ui element support 2025-09-18 20:55:23 -05:00
Cyp 10c9a7d75e Merge branch 'dev' of https://github.com/open-webui/open-webui into dev 2025-09-19 08:59:21 +09:00
Cyp 78e7f14360 Merge branch 'main' of https://github.com/open-webui/open-webui into dev 2025-09-19 08:59:11 +09:00
Timothy Jaeryang Baek 700894a13d refac: channel modal 2025-09-18 18:02:35 -05:00
Timothy Jaeryang Baek 1c78594379 refac 2025-09-18 17:53:11 -05:00
Timothy Jaeryang Baek 48cd72fab0 refac 2025-09-18 17:51:00 -05:00
Timothy Jaeryang Baek 04251b7bff refac 2025-09-18 17:50:39 -05:00
Tim Jaeryang Baek e150d8763b Merge pull request #17551 from ShirasawaSama/patch-20
i18n: improve Chinese translation
2025-09-18 17:49:09 -05:00
Tim Jaeryang Baek 0159a33306 Merge pull request #17542 from ShirasawaSama/patch-23
feat: do not initiate requests when the ChangelogModal is not open to speed up page loading
2025-09-18 17:48:51 -05:00
Timothy Jaeryang Baek c07086401e refac 2025-09-18 17:47:32 -05:00
Tim Jaeryang Baek 2de11f8daf Merge pull request #17577 from joaoback/patch-9
Update translation.json (pt-BR)
2025-09-18 17:09:03 -05:00
Tim Jaeryang Baek 0e75e8529e Merge pull request #17562 from rgaricano/dev-HybridSearch-bm25_slider
FIX: Inconsistency in the description of BM25 - Update Documents.svelte
2025-09-18 17:07:43 -05:00
Tim Jaeryang Baek 40da51d37f Merge pull request #17546 from Cyp9715/dev
i18n: improve Korean translation
2025-09-18 17:06:15 -05:00
joaoback bfc8f74477 Update translation.json (pt-BR)
translation of newly added items.
2025-09-18 13:53:41 -03:00
Timothy Jaeryang Baek d4f21c7e84 refac 2025-09-18 10:43:59 -05:00
Timothy Jaeryang Baek b9e200f104 fix: isdangerous dep 2025-09-18 09:39:40 -05:00
_00_ da7610f6cb FIX: Inconsistency in the description of BM25 - Update Documents.svelte
FIX: Inconsistency in the description of BM25

Fix issue: https://github.com/open-webui/open-webui/discussions/17553
2025-09-18 13:18:07 +02:00
Shirasawa f188d570f7 feat: improve Chinese translation 2025-09-18 07:48:19 +00:00
bglee 52e8c6c998 Update on the Korean translation 2025-09-18 15:09:43 +09:00
Shirasawa 7735a04783 feat: do not initiate requests when the ChangelogModal is not open 2025-09-18 11:50:00 +08:00
Tim Jaeryang Baek 8920bf2377 Merge pull request #17528 from open-webui/dev
0.6.30
2025-09-17 12:25:26 -05:00
Timothy Jaeryang Baek b55a38ee97 doc: changelog 2025-09-17 12:22:19 -05:00
Timothy Jaeryang Baek 6c39dc764d chore: bump 2025-09-17 12:19:18 -05:00
Timothy Jaeryang Baek cde5879987 fix: onedrive 2025-09-17 12:18:23 -05:00
Tim Jaeryang Baek dddd1e44f3 Merge pull request #17420 from open-webui/dev
0.6.29
2025-09-17 11:32:59 -05:00
Timothy Jaeryang Baek 60f62c2f59 refac 2025-09-17 11:28:04 -05:00
Timothy Jaeryang Baek a7267b5914 doc: changelog wording 2025-09-17 11:22:27 -05:00
Timothy Jaeryang Baek c01255570b chore: format 2025-09-17 11:11:46 -05:00
Timothy Jaeryang BaekandSelene Blok caf0a1fbb6 feat: Allow Azure OpenAI to authenticate using DefaultAzureCredential
Co-Authored-By: Selene Blok <20491756+selenecodes@users.noreply.github.com>
2025-09-17 11:04:47 -05:00
Timothy Jaeryang Baek 72cd3a54f7 refac 2025-09-17 10:48:56 -05:00
Timothy Jaeryang Baek c96252f7fe refac/fix: WHISPER_LANGUAGE 2025-09-17 10:46:11 -05:00
Tim Jaeryang Baek 1d83bc15de Merge pull request #17520 from Classic298/changelog-done
chore: Update CHANGELOG.md
2025-09-17 10:33:08 -05:00
Classic298 b8159fc43e Update CHANGELOG.md 2025-09-17 17:24:50 +02:00
Tim Jaeryang Baek 09502c6664 Merge pull request #17490 from Classic298/changelog-done
chore: Changelog 0.6.29
2025-09-17 10:23:05 -05:00
Timothy Jaeryang Baek db1730f47b refac 2025-09-17 10:18:26 -05:00
Timothy Jaeryang Baek 70ab38194f refac: map marker 2025-09-17 09:26:31 -05:00
Tim Jaeryang Baek 867fcc4d95 Merge pull request #17503 from ShirasawaSama/patch-13
feat: Dynamically load leaflet to improve first-screen loading speed (-454KB)
2025-09-17 09:21:09 -05:00
Tim Jaeryang Baek d88c6f81ee Merge pull request #17502 from ShirasawaSama/patch-4
feat: Dynamically load jspdf and html2canvas-pro to improve first-screen loading speed (-980KB)
2025-09-17 09:20:48 -05:00
Timothy Jaeryang Baek 0cfc8f5256 refac 2025-09-17 09:19:56 -05:00
Classic298 59f9a4cdf5 Update CHANGELOG.md 2025-09-17 14:22:13 +02:00
Shirasawa 382299d549 feat: Dynamically load leaflet to improve first-screen loading speed 2025-09-17 08:20:56 +00:00
Shirasawa df433f852f feat: Dynamically load jspdf and html2canvas-pro to improve first-screen loading speed 2025-09-17 07:54:51 +00:00
Tim Jaeryang Baek 980019bb50 Merge pull request #17499 from ShirasawaSama/patch-5
feat: Dynamically load @huggingface/transformers to improve first-screen loading speed (-1.9MB)
2025-09-17 02:23:58 -05:00
Timothy Jaeryang Baek 6549fc839f refac/enh: channel input paste behaviour 2025-09-17 02:21:45 -05:00
Tim Jaeryang Baek 365756c9a2 Merge pull request #17498 from ShirasawaSama/patch-19
feat: Dynamically load CodeEditor.svelte to improve first-screen loading speed (-1MB)
2025-09-17 02:20:04 -05:00
Shirasawa 03d5d00d28 feat: Dynamically load @huggingface/transformers to improve first-screen loading speed 2025-09-17 07:19:57 +00:00
Tim Jaeryang Baek 051765e77a Merge pull request #17497 from ShirasawaSama/patch-20
i18n: improve Chinese translation
2025-09-17 02:19:38 -05:00
Shirasawa 001775d6c3 feat: Dynamically load CodeEditor to improve first-screen loading speed 2025-09-17 07:08:39 +00:00
Shirasawa 2ceb769057 i18n: improve Chinese translation 2025-09-17 06:56:34 +00:00
Tim Jaeryang Baek 9f9f1a1517 Merge pull request #17496 from ShirasawaSama/patch-16
feat: Dynamically load katex to improve first-screen loading speed (-630KB)
2025-09-17 01:36:55 -05:00
Timothy Jaeryang Baek 71c672c90f chore: format 2025-09-17 01:35:07 -05:00
Timothy Jaeryang Baek 1d0881e283 refac 2025-09-17 01:28:04 -05:00
Classic298 fd2b9cfd61 Update CHANGELOG.md 2025-09-17 08:26:40 +02:00
Timothy Jaeryang Baek c1f37d9aed refac 2025-09-17 01:22:15 -05:00
Timothy Jaeryang Baek 4fe97d8794 feat: channel/thread @ model 2025-09-17 00:49:44 -05:00
Timothy Jaeryang Baek 9738ddfd99 refac 2025-09-16 23:19:21 -05:00
Shirasawa 9b3d71f0d2 feat: Dynamically load katex to improve first-screen loading speed 2025-09-17 04:18:16 +00:00
Timothy Jaeryang Baek 779db74d7e refac 2025-09-16 23:00:00 -05:00
Timothy Jaeryang Baek 1077b2ac8b fix: send notification 2025-09-16 22:53:54 -05:00
Timothy Jaeryang Baek c3ea4feca5 refac 2025-09-16 22:30:32 -05:00
Timothy Jaeryang Baek 9321a6ff60 refac 2025-09-16 22:23:45 -05:00
Timothy Jaeryang Baek 26764a0b92 refac: styling 2025-09-16 22:20:50 -05:00
Timothy Jaeryang Baek aef1e06f0b refac: user status 2025-09-16 22:17:35 -05:00
Timothy Jaeryang Baek b27243df81 refac 2025-09-16 21:47:08 -05:00
Timothy Jaeryang Baek bbd1d2b58c enh: channel suggestions 2025-09-16 21:41:47 -05:00
Timothy Jaeryang Baek 99bba12de2 refac: valves filter out external tools 2025-09-16 18:22:00 -05:00
Timothy Jaeryang Baek f0856bcb46 refac 2025-09-16 16:15:52 -05:00
Timothy Jaeryang Baek b34b200682 refac 2025-09-16 16:04:44 -05:00
Timothy Jaeryang Baek a7fd76df41 refac 2025-09-16 15:59:02 -05:00
Timothy Jaeryang Baek c870b2ea29 refac: styling 2025-09-16 15:57:20 -05:00
Timothy Jaeryang Baek 71f99228ef refac 2025-09-16 15:49:43 -05:00
Timothy Jaeryang Baek f0bd74b75b refac 2025-09-16 15:47:43 -05:00
Classic298 69646eb1de chore: Changelog 0.6.29 (#19)
* 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
2025-09-16 22:34:48 +02:00
Timothy Jaeryang Baek 8edfd29102 refac 2025-09-16 15:26:08 -05:00
Timothy Jaeryang Baek d8ad384b06 refac 2025-09-16 15:16:48 -05:00
Timothy Jaeryang Baek 10e39956ef refac 2025-09-16 15:12:51 -05:00
Timothy Jaeryang Baek 4a971eeaee refac 2025-09-16 15:12:01 -05:00
Timothy Jaeryang Baek 767acd1789 refac 2025-09-16 15:09:57 -05:00
Timothy Jaeryang Baek a3dd678bbd refac: styling 2025-09-16 15:03:26 -05:00
Timothy Jaeryang Baek 8bf0b40fdd refac: styling 2025-09-16 14:57:56 -05:00
Timothy Jaeryang Baek 3e58413268 refac 2025-09-16 13:48:01 -05:00
Timothy Jaeryang Baek b1e5bc8e49 refac 2025-09-16 13:42:37 -05:00
Timothy Jaeryang Baek 2b1ee8b0dc refac 2025-09-16 13:40:11 -05:00
Timothy Jaeryang Baek 2a234829f5 enh: folder background image 2025-09-16 13:32:42 -05:00
Timothy Jaeryang Baek d46b7b8f1b refac 2025-09-16 13:11:53 -05:00
Timothy Jaeryang Baek 32015c392d refac: access control 2025-09-16 13:03:33 -05:00
Timothy Jaeryang Baek c8780a7f93 refac 2025-09-16 12:52:13 -05:00
Timothy Jaeryang Baek 0f04227c34 refac: direct external tools now require explicit toggle from input 2025-09-16 12:43:53 -05:00
Timothy Jaeryang Baek b14617a653 refac: otel metrics handle 500 2025-09-16 12:11:32 -05:00
Timothy Jaeryang Baek e66e0526ed refac/enh: function valves validation 2025-09-16 12:00:59 -05:00
Timothy Jaeryang Baek fed5615c19 refac: image max compression size 2025-09-16 11:32:40 -05:00
Timothy Jaeryang Baek f68d11ec52 refac 2025-09-16 11:17:32 -05:00
Timothy Jaeryang Baek 034163e9f9 chore: format 2025-09-16 11:16:08 -05:00
Timothy Jaeryang Baek 77358031f5 refac: placeholder 2025-09-16 11:13:15 -05:00
Timothy Jaeryang Baek e1e3009a30 refac: granular onedrive integration types 2025-09-16 10:54:13 -05:00
Timothy Jaeryang Baek 76d358bd6c refac 2025-09-16 10:40:15 -05:00
Timothy Jaeryang Baek c2f98a4cd2 refac: usage info 2025-09-16 10:28:25 -05:00
Tim Jaeryang Baek 83699cf3ef Merge pull request #17479 from ShirasawaSama/patch-8
feat: Dynamically load @azure/msal-browser to improve first-screen loading speed
2025-09-16 10:25:10 -05:00
Tim Jaeryang Baek 5ba0b52cf3 Merge pull request #17477 from ShirasawaSama/patch-13
feat: Dynamically load mermaid.js to improve first-screen loading speed
2025-09-16 10:24:35 -05:00
Tim Jaeryang Baek 40b6e6802f Merge pull request #17475 from ShirasawaSama/patch-20
i18n: improve Chinese (zh-CN, zh-TW) translation
2025-09-16 10:23:40 -05:00
Shirasawa a341b9a4ea feat: dynamically load @azure/msal-browser 2025-09-16 10:19:00 +00:00
Shirasawa db05e34736 feat: dynamically load mermaid.js 2025-09-16 09:43:03 +00:00
Shirasawa 072de1fb7e i18n: improve Chinese translation 2025-09-16 08:42:49 +00:00
Timothy Jaeryang Baek d041d58bb6 refac/enh: include pdf export in chat item 2025-09-15 18:39:07 -05:00
Timothy Jaeryang Baek 3ec1efb6e0 refac: folders 2025-09-15 18:33:57 -05:00
Timothy Jaeryang Baek 3f27d9ada1 refac: styling 2025-09-15 17:38:34 -05:00
Timothy Jaeryang Baek 19e3214997 refac: folder click behaviour 2025-09-15 17:21:10 -05:00
Timothy Jaeryang Baek 49c9b3199f refac 2025-09-15 17:21:02 -05:00
Timothy Jaeryang Baek c17f3bc82b refac: styling 2025-09-15 16:39:09 -05:00
Timothy Jaeryang Baek 8b8908fa93 refac 2025-09-15 16:35:53 -05:00
Timothy Jaeryang Baek 45da209f85 refac 2025-09-15 16:29:53 -05:00
Timothy Jaeryang Baek ad58e6b297 refac: styling 2025-09-15 16:28:37 -05:00
Timothy Jaeryang Baek 4f72d5453f refac: styling 2025-09-15 16:25:13 -05:00
Timothy Jaeryang Baek 5eb26fe7ab refac 2025-09-15 16:25:04 -05:00
Timothy Jaeryang Baek e1386fe80b refac: message input optimization 2025-09-15 16:03:34 -05:00
Timothy Jaeryang Baek ac375fe3d2 refac: styling 2025-09-15 15:54:35 -05:00
Timothy Jaeryang Baek 218920f0c3 refac: styling 2025-09-15 15:53:11 -05:00
Timothy Jaeryang Baek 3d5699ef22 refac: styling 2025-09-15 15:50:28 -05:00
Timothy Jaeryang Baek 621e7679c4 refac/enh: notification toast click behaviour 2025-09-15 15:32:43 -05:00
Timothy Jaeryang Baek c04e1787c5 refac: styling 2025-09-15 15:32:32 -05:00
Timothy Jaeryang Baek e5bb366719 refac: styling 2025-09-15 15:12:01 -05:00
Timothy Jaeryang Baek c0f3352b71 refac 2025-09-15 15:05:56 -05:00
Timothy Jaeryang Baek 9701d127bf refac 2025-09-15 15:02:47 -05:00
Timothy Jaeryang Baek 0e5320c39e refac/fix: codeblock 2025-09-15 15:02:19 -05:00
Timothy Jaeryang Baek c744ae0d63 refac: styling 2025-09-15 14:55:05 -05:00
Timothy Jaeryang Baek 596be451ec refac: styling 2025-09-15 14:28:16 -05:00
Timothy Jaeryang Baek 5434172e88 refac 2025-09-15 14:13:14 -05:00
Timothy Jaeryang Baek cf77a4276e refac: styling 2025-09-15 14:11:44 -05:00
Timothy Jaeryang Baek adaa467f75 refac: styling 2025-09-15 14:08:07 -05:00
Timothy Jaeryang Baek 879778361d refac 2025-09-15 13:43:05 -05:00
Timothy Jaeryang Baek fb02ec52a5 refac: styling 2025-09-15 13:36:51 -05:00
Timothy Jaeryang Baek 2ca34217e6 refac: styling 2025-09-15 13:34:09 -05:00
Timothy Jaeryang Baek 9e50026107 refac 2025-09-15 13:28:29 -05:00
Timothy Jaeryang Baek 67549dcadd refac 2025-09-15 13:25:36 -05:00
Timothy Jaeryang Baek 308c277fd3 refac: styling 2025-09-15 13:22:41 -05:00
Timothy Jaeryang Baek 24971ffce0 refac 2025-09-15 12:10:45 -05:00
Timothy Jaeryang Baek b6a2ca877a refac 2025-09-15 12:07:53 -05:00
Timothy Jaeryang Baek e4c864de7e fix: connection url edit 2025-09-15 12:01:46 -05:00
Tim Jaeryang Baek a35469c669 Merge pull request #17418 from rgaricano/date_format_i18n
UPD: i18n es-ES Translation - dev_v0.6.29
2025-09-15 12:47:43 -04:00
Timothy Jaeryang Baek a51f0c30ec refac/fix: knowledge permission 2025-09-15 11:40:31 -05:00
Timothy Jaeryang Baek d5824b1b49 refac: prompt template variable made not required by default 2025-09-15 11:18:31 -05:00
Andrew Baek b0720ec2aa Merge branch 'dev' of https://github.com/andrewbbaek/open-webui into dev 2025-09-16 01:07:15 +09:00
Tim Jaeryang Baek 5afa42b0d9 Merge pull request #17441 from Kylapaallikko/dev
i18n: Update fi-FI translation
2025-09-15 12:06:17 -04:00
Timothy Jaeryang Baek 48ed800d91 refac 2025-09-15 11:05:26 -05:00
Tim Jaeryang Baek dd6f83e9c5 Merge pull request #17423 from aleprj/fix-filehandler
Fix file_handler filters
2025-09-15 11:35:39 -04:00
Tim Jaeryang Baek 29f9d1d0c2 Merge pull request #17450 from ShirasawaSama/patch-20
i18n: update translations in zh-CN locale
2025-09-15 11:08:35 -04:00
Shirasawa 530460d711 i18n: update translations in zh-CN locale 2025-09-15 06:37:12 +00:00
Timothy Jaeryang Baek 3e65109900 refac 2025-09-14 19:00:49 -04:00
Timothy Jaeryang Baek db0379030e refac 2025-09-14 18:59:09 -04:00
Timothy Jaeryang Baek 098f34f400 refac/enh: mention token rendering 2025-09-14 18:49:01 -04:00
Timothy Jaeryang Baek 22e11760a1 refac 2025-09-14 18:08:31 -04:00
Timothy Jaeryang Baek cae7ad8c70 refac: styling 2025-09-14 17:45:58 -04:00
Kylapaallikko 4e21e4ba0e Update fi-FI translation.json
Added missing translations
2025-09-14 17:40:55 +03:00
Timothy Jaeryang Baek e701c65db4 refac: styling 2025-09-14 10:50:50 +02:00
Timothy Jaeryang Baek e61e7434a0 refac 2025-09-14 10:46:49 +02:00
Timothy Jaeryang Baek 1ef8204359 refac 2025-09-14 10:45:52 +02:00
Timothy Jaeryang Baek 025eef754f refac 2025-09-14 10:41:52 +02:00
Timothy Jaeryang Baek b775826a27 refac: styling 2025-09-14 10:39:39 +02:00
Timothy Jaeryang Baek 58d7ca35e3 refac 2025-09-14 10:27:07 +02:00
Timothy Jaeryang Baek aa8ab349ed feat: ref chat 2025-09-14 10:26:46 +02:00
Timothy Jaeryang Baek c923461882 refac 2025-09-14 10:11:17 +02:00
Timothy Jaeryang Baek 8753c2ff51 refac 2025-09-14 10:09:52 +02:00
Timothy Jaeryang Baek cc788eb4f9 refac 2025-09-14 10:07:21 +02:00
Timothy Jaeryang Baek b53ddfbd19 refac 2025-09-14 10:06:02 +02:00
Timothy Jaeryang Baek c03ca7270e refac/feat: note/knowledge/chat select input menu 2025-09-14 09:54:06 +02:00
Timothy Jaeryang Baek eadec9e86e refac 2025-09-14 09:20:12 +02:00
Timothy Jaeryang Baek 3ed0a6d11f refac 2025-09-14 09:08:23 +02:00
Tim Jaeryang Baek caaaa07232 Merge pull request #17425 from joaoback/patch-8
Update translation.json (pt-BR)
2025-09-14 10:55:02 +04:00
Timothy Jaeryang Baek 8802c6e8db refac 2025-09-14 08:42:27 +02:00
joaoback a4ca9a981e Update translation.json (pt-BR)
added translations for newly added UI items
2025-09-13 20:57:01 -03:00
Alexandre Oliveira f9fd22d98e fix: setting file_handler in a filter would generate errors in messages with no files, because a "files: Null" in metadata would trigger an attempt to delete a non existent files object 2025-09-13 12:57:54 -03:00
Timothy Jaeryang Baek 031cf38655 refac 2025-09-13 19:04:07 +04:00
Timothy Jaeryang Baek f68d1ba394 refac 2025-09-13 18:41:21 +04:00
Timothy Jaeryang Baek 02f7c3258b refac 2025-09-13 18:39:32 +04:00
Timothy Jaeryang Baek 5c2db102d0 refac: styling 2025-09-13 03:09:27 +04:00
Timothy Jaeryang Baek 58a8cae3d7 refac 2025-09-13 03:00:40 +04:00
Timothy Jaeryang Baek ef56b14636 refac: styling 2025-09-13 02:58:10 +04:00
Timothy Jaeryang Baek cd740f436d refac 2025-09-13 02:45:15 +04:00
Timothy Jaeryang Baek d4c628de09 refac: styling 2025-09-13 02:42:10 +04:00
Timothy Jaeryang Baek 9208a84185 refac 2025-09-13 02:33:32 +04:00
Timothy Jaeryang Baek 49bb1255c5 refac: banner styling 2025-09-13 02:29:04 +04:00
Timothy Jaeryang Baek 7344bd3e66 refac: styling 2025-09-13 02:27:03 +04:00
_00_ 7f41bb6118 Update translation.json 2025-09-13 00:23:53 +02:00
Timothy Jaeryang Baek 74263c872c refac 2025-09-13 02:15:12 +04:00
Timothy Jaeryang Baek f3cd2ffb18 refac 2025-09-13 02:13:26 +04:00
Timothy Jaeryang Baek b623a9ea69 refac 2025-09-13 02:10:41 +04:00
Timothy Jaeryang Baek e097bbdf11 refac 2025-09-13 02:09:57 +04:00
_00_ b1cba685f4 UPD: i18n es-ES Translation - dev_v0.6.28
### UPD: i18n es-ES Translation - dev_v0.6.28

- Translated added strings.
2025-09-13 00:04:30 +02:00
Timothy Jaeryang Baek 721ea9cbd8 refac: styling 2025-09-13 01:58:09 +04:00
Timothy Jaeryang Baek 6a67a2217c refac 2025-09-13 01:52:27 +04:00
Timothy Jaeryang Baek d7f43bfc1a refac 2025-09-13 01:50:24 +04:00
Timothy Jaeryang Baek 384a53b339 refac: styling 2025-09-13 01:48:14 +04:00
Timothy Jaeryang Baek 9a55547827 refac/enh: model default filter/feature 2025-09-13 01:23:27 +04:00
Timothy Jaeryang Baek 3288b19795 refac: styling 2025-09-13 00:37:45 +04:00
Timothy Jaeryang Baek 56a8973fa0 refac 2025-09-13 00:30:00 +04:00
Timothy Jaeryang Baek 680adea4e7 refac: styling 2025-09-13 00:27:04 +04:00
Timothy Jaeryang Baek d6094c2881 refac 2025-09-13 00:24:42 +04:00
Timothy Jaeryang Baek f6b1b075e1 refac 2025-09-13 00:03:14 +04:00
Timothy Jaeryang Baek 210197fd43 refac/fix: web/youtube file attachment handling 2025-09-13 00:02:48 +04:00
Timothy Jaeryang Baek 2227f24bd6 refac/enh: date format i18n 2025-09-12 23:53:23 +04:00
Timothy Jaeryang Baek 3f137f9bbc chore: format 2025-09-12 23:45:23 +04:00
Tim Jaeryang Baek 9f6872a8e1 Merge pull request #17404 from ShirasawaSama/patch-13
i18n: improve zh-TW translation
2025-09-12 23:41:40 +04:00
Timothy Jaeryang Baek c55f557828 refac/enh: model suggestion tooltip 2025-09-12 23:36:40 +04:00
Timothy Jaeryang Baek 3e1b14f570 refac/enh: optional vectordb deps 2025-09-12 23:33:08 +04:00
Timothy Jaeryang Baek f51a0b181f refac: styling 2025-09-12 23:28:31 +04:00
Timothy Jaeryang Baek a4ce1ebf4f refac: styling 2025-09-12 23:22:54 +04:00
Timothy Jaeryang Baek 766e8bc022 refac 2025-09-12 23:04:59 +04:00
Timothy Jaeryang Baek 5df9cad671 refac 2025-09-12 22:00:33 +04:00
Timothy Jaeryang Baek 81cefd8541 chore: bump 2025-09-12 21:52:46 +04:00
Timothy Jaeryang Baek 06c1426e14 refac: channel input 2025-09-12 21:43:05 +04:00
Timothy Jaeryang Baek b2623c9799 refac 2025-09-12 21:00:15 +04:00
Timothy Jaeryang Baek 153afd832c refac: deprecate textarea input 2025-09-12 20:54:34 +04:00
Timothy Jaeryang Baek 19e18bc461 refac 2025-09-12 20:37:42 +04:00
Timothy Jaeryang Baek 06a6855f84 refac 2025-09-12 20:35:14 +04:00
Timothy Jaeryang Baek 6b69c4da0f refac/enh: commands ui 2025-09-12 20:31:57 +04:00
Timothy Jaeryang Baek d973db829f refac 2025-09-12 15:57:04 +04:00
Timothy Jaeryang Baek 4977e6d50f refac 2025-09-12 15:56:31 +04:00
Timothy Jaeryang Baek 136972ccf0 refac: styling 2025-09-12 15:54:42 +04:00
Timothy Jaeryang Baek 96b8aaf83f refac 2025-09-12 15:41:12 +04:00
Shirasawa 097b5e4c0b i18n: improve zh-TW translation 2025-09-12 19:35:19 +08:00
Timothy Jaeryang Baek ca853ca465 refac/enh: sort toggle filter by default 2025-09-12 15:06:11 +04:00
Timothy Jaeryang Baek a68342d5a8 refac: input menu 2025-09-12 15:05:37 +04:00
Andrew Baek f704d5b0b3 Merge branch 'dev' of https://github.com/andrewbbaek/open-webui into dev 2025-09-12 19:23:46 +09:00
Tim Jaeryang Baek fe7b8d9617 Merge pull request #17401 from ShirasawaSama/i18n/improve-chinese-translation
i18n: improve zh-CN translation
2025-09-12 13:50:41 +04:00
Timothy Jaeryang Baek 042191372a refac: styling 2025-09-12 13:49:53 +04:00
Timothy Jaeryang Baek fbf9b3f1bb refac: styling 2025-09-12 13:38:02 +04:00
Shirasawa cba2e44de4 i18n: improve zh-TW translation 2025-09-12 17:01:08 +08:00
Shirasawa ff8100dc6f feat: improve zh-CN translation 2025-09-12 16:54:24 +08:00
Tim Jaeryang Baek a156033d3c Merge pull request #17284 from xyb/feishucn
feat: add Feishu OAuth integration
2025-09-12 12:48:45 +04:00
Tim Jaeryang Baek b7f268f163 Merge pull request #17399 from Ithanil/german_i18n
i18n: German 100% translated
2025-09-12 12:47:25 +04:00
Tim Jaeryang Baek d5c236aa8e Merge pull request #17385 from silentoplayz/tailwind-config-fix
fix: correct spelling typo in Tailwind CSS config (containerQuries → containerQueries)
2025-09-12 12:47:00 +04:00
Jan Kessler c9e69fb2f4 100% german translation (~ 250 strings added/changed) 2025-09-12 10:38:47 +02:00
Xie Yanbo ee82439e67 feat: add Feishu OAuth integration
Implement Feishu OAuth provider using standard client:
- Set up Feishu-specific endpoints for authorization, token, and userinfo
- Use user_id as sub claim for Feishu user identification
- Extract correct user information from nested 'data' field in Feishu responses

Configuration requirements:
- Set FEISHU_CLIENT_ID and FEISHU_CLIENT_SECRET environment variables to enable Feishu OAuth
- Set ENABLE_OAUTH_SIGNUP=true to allow automatic user creation after OAuth login
- Set DEFAULT_USER_ROLE=user to grant immediate access after OAuth registration
- Set OAUTH_MERGE_ACCOUNTS_BY_EMAIL=true to enable merging of existing user accounts with matching emails
2025-09-12 14:09:32 +08:00
silentoplayz fcd3c7a10f fix: spelling 2025-09-12 00:47:54 -04:00
Timothy Jaeryang Baek 051b6daa82 refac/fix: large file upload 2025-09-11 22:29:02 +04:00
Timothy Jaeryang Baek 6c0a5fa91c refac 2025-09-11 21:56:59 +04:00
Timothy Jaeryang Baek edc056351d refac 2025-09-11 21:38:01 +04:00
Tim Jaeryang Baek 7347f95b24 Merge pull request #17370 from rgaricano/dev-es_ES
UPD: i18n- Translation es-ES v0.6.28
2025-09-11 21:35:55 +04:00
Timothy Jaeryang Baek a6f25b99f9 refac 2025-09-11 21:34:40 +04:00
Timothy Jaeryang Baek 2185fc61c0 refac 2025-09-11 21:29:56 +04:00
_00_ bf3a140254 UPD: i18n- Translation es-ES v0.6.28 2025-09-11 17:31:35 +02:00
Timothy Jaeryang Baek bbe6564bfd refac 2025-09-11 17:31:28 +04:00
Tim Jaeryang Baek 95674162f9 Merge pull request #17344 from BoFFire/patch-1
i18n: update kabyle translation
2025-09-11 12:28:41 +04:00
Tim Jaeryang Baek 24f78fed21 Merge pull request #17356 from ShirasawaSama/patch-19
feat: Add text truncation to ChatItem title display
2025-09-11 12:28:25 +04:00
Timothy Jaeryang Baek cf72f5503f refac 2025-09-11 12:26:25 +04:00
Shirasawa 932f759410 feat: add text truncation to ChatItem title display 2025-09-11 15:45:55 +08:00
ButterflyOfFire d6668515f6 i18n: update kabyle translation
Hi,

Added Kabyle translation progress : 88%
2025-09-10 17:56:01 +01:00
Tim Jaeryang Baek a05966af96 Merge pull request #17340 from Ithanil/i18n_prompts
i18n: translate '*required' string in prompts modal + added german translations
2025-09-10 17:07:58 +04:00
Jan Kessler 3802ab9fd1 translate '*required' string in prompts modal + added german translations 2025-09-10 15:03:12 +02:00
Jesper Kristensen 4ca43004ed feat: Added support for redis as session storage 2025-09-10 14:10:40 +02:00
Aleix Dorca 7c0bf59c7c Update catalan translation.json 2025-09-05 17:17:20 +02:00
Andrew Baek b50ae5bf69 Update MessageInput.svelte 2025-08-31 14:48:37 +09:00
Andrew Baek 05223f720d Merge branch 'open-webui:dev' into dev 2025-08-29 22:12:04 +09:00
Andrew Baek ceaafbbfd2 Update groups.py 2025-08-27 03:22:34 +09:00
Andrew Baek 7b0ce47eb8 Merge branch 'dev' of https://github.com/andrewbbaek/open-webui into dev 2025-08-27 03:08:21 +09:00
Andrew Baek a64586f9c2 Merge branch 'dev' of https://github.com/andrewbbaek/open-webui into dev 2025-08-27 02:57:43 +09:00
Andrew Baek e214d59d10 Update groups.py fix issue #16870 2025-08-27 01:04:27 +09:00
silentoplayz 8df74dde80 fix 2025-08-23 15:19:35 -04:00
silentoplayz c6d80496ab feat: improve ollama model management experience
This commit introduces several improvements to the Ollama model management modal:

- Adds a cancel button to the model pulling operation, using the existing 'x' button pattern.
- Adds a cancel button to the "Update All" models operation, allowing the user to cancel the update for the currently processing model.
- Cleans up toast notifications when updating all models. A single toast is now shown at the beginning and a summary toast at the end, preventing notification spam.
- Refactors the `ManageOllama.svelte` component to support these new cancellation features.
- Adds tooltips to all buttons in the modal to improve clarity.
- Disables buttons when their corresponding input fields are empty to prevent accidental clicks.
2025-08-23 15:08:07 -04:00
817 changed files with 127214 additions and 39223 deletions
+7 -1
View File
@@ -11,7 +11,9 @@ body:
## Important Notes
- **Before submitting a bug report**: Please check the [Issues](https://github.com/open-webui/open-webui/issues) and [Discussions](https://github.com/open-webui/open-webui/discussions) sections to see if a similar issue has already been reported. If unsure, start a discussion first, as this helps us efficiently focus on improving the project. Duplicates may be closed without notice. **Please search for existing issues and discussions.**
- **Before submitting a bug report**: Please check the [Issues](https://github.com/open-webui/open-webui/issues) and [Discussions](https://github.com/open-webui/open-webui/discussions) sections to see if a similar issue has already been reported. If unsure, start a discussion first, as this helps us efficiently focus on improving the project. Duplicates may be closed without notice. **Please search for existing issues AND discussions. No matter open or closed.**
- Check for opened, **but also for (recently) CLOSED issues** as the issue you are trying to report **might already have been fixed on the dev branch!**
- **Respectful collaboration**: Open WebUI is a volunteer-driven project with a single maintainer and contributors who also have full-time jobs. Please be constructive and respectful in your communication.
@@ -19,6 +21,8 @@ body:
- **Bug Reproducibility**: If a bug cannot be reproduced using a `:main` or `:dev` Docker setup or with `pip install` on Python 3.11, community assistance may be required. In such cases, we will move it to the "[Issues](https://github.com/open-webui/open-webui/discussions/categories/issues)" Discussions section. Your help is appreciated!
- **Scope**: If you want to report a SECURITY VULNERABILITY, then do so through our [GitHub security page](https://github.com/open-webui/open-webui/security).
- type: checkboxes
id: issue-check
attributes:
@@ -29,6 +33,8 @@ body:
required: true
- label: I have searched for any existing and/or related discussions.
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 am using the latest version of Open WebUI.
required: true
+23 -5
View File
@@ -8,10 +8,21 @@ body:
value: |
## Important Notes
### Before submitting
Please check the [Issues](https://github.com/open-webui/open-webui/issues) or [Discussions](https://github.com/open-webui/open-webui/discussions) to see if a similar request has been posted.
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 you’re unsure, start a discussion post first.
This will help us efficiently focus on improving the project.
#### Scope
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**.
@@ -22,7 +33,6 @@ body:
We appreciate your time and ask that you **respect ours**.
### 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.
@@ -35,14 +45,22 @@ body:
label: Check Existing Issues
description: Please confirm that you've checked for existing similar requests
options:
- label: I have searched the existing issues and discussions.
- 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.
required: true
- type: checkboxes
id: feature-scope
attributes:
label: Verify Feature Scope
description: Please confirm the feature's scope is within the described scope
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.
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..."
placeholder: "Ex. I'm always frustrated when... / Not related to a problem"
validations:
required: true
- type: textarea
+27 -7
View File
@@ -1,17 +1,28 @@
<!--
⚠️ 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.
-->
# Pull Request Checklist
### Note to first-time contributors: Please open a discussion post in [Discussions](https://github.com/open-webui/open-webui/discussions) and describe your changes before submitting a pull request.
### 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.
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.
**Before submitting, make sure you've checked the following:**
- [ ] **Target branch:** Please verify that the pull request targets the `dev` branch.
- [ ] **Description:** Provide a concise description of the changes made in this pull request.
- [ ] **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:** Have you updated relevant documentation [Open WebUI Docs](https://github.com/open-webui/docs), or other documentation sources?
- [ ] **Dependencies:** Are there any new dependencies? Have you updated the dependency versions in the documentation?
- [ ] **Testing:** Have you written and run sufficient tests to validate the changes?
- [ ] **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?
- [ ] **Prefix:** To clearly categorize this pull request, prefix the pull request title using one of the following:
- [ ] **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
@@ -73,4 +84,13 @@
### 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.
-->
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.
+2 -1
View File
@@ -57,7 +57,8 @@ jobs:
git lfs install
git lfs track "*.ttf"
git lfs track "*.jpg"
rm demo.gif
rm demo.png
rm banner.png
git add .
git commit -m "GitHub deploy: ${{ github.sha }}"
git push --force https://open-webui:${HF_TOKEN}@huggingface.co/spaces/open-webui/open-webui main
+117
View File
@@ -95,6 +95,7 @@ jobs:
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 }}
@@ -141,6 +142,9 @@ jobs:
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
@@ -196,6 +200,7 @@ jobs:
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
@@ -243,6 +248,9 @@ jobs:
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
@@ -298,6 +306,7 @@ jobs:
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
@@ -401,6 +410,7 @@ jobs:
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
@@ -503,6 +513,7 @@ jobs:
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
@@ -798,3 +809,109 @@ jobs:
- 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 }}"
-2
View File
@@ -3,8 +3,6 @@ pnpm-lock.yaml
package-lock.json
yarn.lock
kubernetes/
# Copy of .gitignore
.DS_Store
node_modules
+1202
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -1,7 +1,7 @@
# Open WebUI Contributor License Agreement
# Contributor License Agreement
By submitting my contributions to Open WebUI, I grant Open WebUI full freedom to use my work in any way they choose, under any terms they like, both now and in the future. This approach helps ensure the project remains unified, flexible, and easy to maintain, while empowering Open WebUI to respond quickly to the needs of its users and the wider community.
By submitting my contributions to this repository in any form, I grant Open WebUI Inc. a perpetual, worldwide, irrevocable, royalty-free license, under copyright and patent, to use, modify, distribute, sublicense, and commercialize my work under any terms they choose, both now and in the future.
Taking part in this process means my work can be seamlessly integrated and combined with others, ensuring longevity and adaptability for everyone who benefits from the Open WebUI project. This collaborative approach strengthens the project’s future and helps guarantee that improvements can always be shared and distributed in the most effective way possible.
I represent that my contributions are my original work (or that I have sufficient rights to grant this license) and that I have the authority to enter into this agreement.
**_To the fullest extent permitted by law, my contributions are provided on an “as is” basis, with no warranties or guarantees of any kind, and I disclaim any liability for any issues or damages arising from their use or incorporation into the project, regardless of the type of legal claim._**
+17 -5
View File
@@ -13,6 +13,7 @@ ARG USE_CUDA_VER=cu128
# IMPORTANT: If you change the embedding model (sentence-transformers/all-MiniLM-L6-v2) and vice versa, you aren't able to use RAG Chat with your previous documents loaded in the WebUI! You need to re-embed them.
ARG USE_EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
ARG USE_RERANKING_MODEL=""
ARG USE_AUXILIARY_EMBEDDING_MODEL=TaylorAI/bge-micro-v2
# Tiktoken encoding name; models to use can be found at https://huggingface.co/models?library=tiktoken
ARG USE_TIKTOKEN_ENCODING_NAME="cl100k_base"
@@ -42,7 +43,7 @@ ENV APP_BUILD_HASH=${BUILD_HASH}
RUN npm run build
######## WebUI backend ########
FROM python:3.11-slim-bookworm AS base
FROM python:3.11.14-slim-bookworm AS base
# Use args
ARG USE_CUDA
@@ -52,9 +53,13 @@ ARG USE_SLIM
ARG USE_PERMISSION_HARDENING
ARG USE_EMBEDDING_MODEL
ARG USE_RERANKING_MODEL
ARG USE_AUXILIARY_EMBEDDING_MODEL
ARG UID
ARG GID
# Python settings
ENV PYTHONUNBUFFERED=1
## Basis ##
ENV ENV=prod \
PORT=8080 \
@@ -64,7 +69,8 @@ ENV ENV=prod \
USE_SLIM_DOCKER=${USE_SLIM} \
USE_CUDA_DOCKER_VER=${USE_CUDA_VER} \
USE_EMBEDDING_MODEL_DOCKER=${USE_EMBEDDING_MODEL} \
USE_RERANKING_MODEL_DOCKER=${USE_RERANKING_MODEL}
USE_RERANKING_MODEL_DOCKER=${USE_RERANKING_MODEL} \
USE_AUXILIARY_EMBEDDING_MODEL_DOCKER=${USE_AUXILIARY_EMBEDDING_MODEL}
## Basis URL Config ##
ENV OLLAMA_BASE_URL="/ollama" \
@@ -85,6 +91,7 @@ ENV WHISPER_MODEL="base" \
## RAG Embedding model settings ##
ENV RAG_EMBEDDING_MODEL="$USE_EMBEDDING_MODEL_DOCKER" \
RAG_RERANKING_MODEL="$USE_RERANKING_MODEL_DOCKER" \
AUXILIARY_EMBEDDING_MODEL="$USE_AUXILIARY_EMBEDDING_MODEL_DOCKER" \
SENTENCE_TRANSFORMERS_HOME="/app/backend/data/cache/embedding/models"
## Tiktoken model settings ##
@@ -121,7 +128,7 @@ RUN apt-get update && \
apt-get install -y --no-install-recommends \
git build-essential pandoc gcc netcat-openbsd curl jq \
python3-dev \
ffmpeg libsm6 libxext6 \
ffmpeg libsm6 libxext6 zstd \
&& rm -rf /var/lib/apt/lists/*
# install python dependencies
@@ -130,18 +137,23 @@ COPY --chown=$UID:$GID ./backend/requirements.txt ./requirements.txt
RUN pip3 install --no-cache-dir uv && \
if [ "$USE_CUDA" = "true" ]; then \
# If you use CUDA the whisper and embedding model will be downloaded on first use
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir && \
# fix: pin torch<=2.9.1 - torch 2.10.0 aarch64 wheels cause SIGILL on ARM devices (RPi 4 Cortex-A72) #21349
pip3 install 'torch<=2.9.1' torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir && \
uv pip install --system -r requirements.txt --no-cache-dir && \
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
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')"; \
else \
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \
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 && \
if [ "$USE_SLIM" != "true" ]; then \
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
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')"; \
fi; \
fi; \
mkdir -p /app/backend/data && chown -R $UID:$GID /app/backend/data/ && \
-35
View File
@@ -1,35 +0,0 @@
### Installing Both Ollama and Open WebUI Using Kustomize
For cpu-only pod
```bash
kubectl apply -f ./kubernetes/manifest/base
```
For gpu-enabled pod
```bash
kubectl apply -k ./kubernetes/manifest
```
### Installing Both Ollama and Open WebUI Using Helm
Package Helm file first
```bash
helm package ./kubernetes/helm/
```
For cpu-only pod
```bash
helm install ollama-webui ./ollama-webui-*.tgz
```
For gpu-enabled pod
```bash
helm install ollama-webui ./ollama-webui-*.tgz --set ollama.resources.limits.nvidia.com/gpu="1"
```
Check the `kubernetes/helm/values.yaml` file to know which parameters are available for customization
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2023-2025 Timothy Jaeryang Baek (Open WebUI)
Copyright (c) 2023- Open WebUI Inc. [Created by Timothy Jaeryang Baek]
All rights reserved.
Redistribution and use in source and binary forms, with or without
+11
View File
@@ -0,0 +1,11 @@
# Open WebUI Multi-License Notice
This repository contains code governed by multiple licenses based on the date and origin of contribution:
1. All code committed prior to commit a76068d69cd59568b920dfab85dc573dbbb8f131 is licensed under the MIT License (see LICENSE_HISTORY).
2. All code committed from commit a76068d69cd59568b920dfab85dc573dbbb8f131 up to and including commit 60d84a3aae9802339705826e9095e272e3c83623 is licensed under the BSD 3-Clause License (see LICENSE_HISTORY).
3. All code contributed or modified after commit 60d84a3aae9802339705826e9095e272e3c83623 is licensed under the Open WebUI License (see LICENSE).
For details on which commits are covered by which license, refer to LICENSE_HISTORY.
+25 -56
View File
@@ -10,14 +10,16 @@
[![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)
**Open WebUI is an [extensible](https://docs.openwebui.com/features/plugin/), feature-rich, and user-friendly self-hosted AI platform designed to operate entirely offline.** It supports various LLM runners like **Ollama** and **OpenAI-compatible APIs**, with **built-in inference engine** for RAG, making it a **powerful AI deployment solution**.
![Open WebUI Banner](./banner.png)
**Open WebUI is an [extensible](https://docs.openwebui.com/features/extensibility/plugin), feature-rich, and user-friendly self-hosted AI platform designed to operate entirely offline.** It supports various LLM runners like **Ollama** and **OpenAI-compatible APIs**, with **built-in inference engine** for RAG, making it a **powerful AI deployment solution**.
Passionate about open-source AI? [Join our team →](https://careers.openwebui.com/)
![Open WebUI Demo](./demo.gif)
![Open WebUI Demo](./demo.png)
> [!TIP]
> **Looking for an [Enterprise Plan](https://docs.openwebui.com/enterprise)?** – **[Speak with Our Sales Team Today!](mailto:sales@openwebui.com)**
> **Looking for an [Enterprise Plan](https://docs.openwebui.com/enterprise)?** – **[Speak with Our Sales Team Today!](https://docs.openwebui.com/enterprise)**
>
> Get **enhanced capabilities**, including **custom theming and branding**, **Service Level Agreement (SLA) support**, **Long-Term Support (LTS) versions**, and **more!**
@@ -31,32 +33,44 @@ For more information, be sure to check out our [Open WebUI Documentation](https:
- 🛡️ **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.
- 🔄 **SCIM 2.0 Support**: 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.
- 📱 **Responsive Design**: Enjoy a seamless experience across Desktop PC, Laptop, and Mobile devices.
- 📱 **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.
- ✒️🔢 **Full Markdown and LaTeX Support**: Elevate your LLM experience with comprehensive Markdown and LaTeX capabilities for enriched interaction.
- 🎤📹 **Hands-Free Voice/Video Call**: Experience seamless communication with integrated hands-free voice and video call features, allowing for a more dynamic and interactive chat environment.
- 🎤📹 **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.
- 🛠️ **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.
- 🐍 **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.
- 📚 **Local RAG Integration**: Dive into the future of chat interactions with groundbreaking Retrieval Augmented Generation (RAG) support. This feature seamlessly integrates document interactions into your chat experience. You can load documents directly into the chat or add files to your document library, effortlessly accessing them using the `#` command before a query.
- 💾 **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.
- 🔍 **Web Search for RAG**: Perform web searches using providers like `SearXNG`, `Google PSE`, `Brave Search`, `serpstack`, `serper`, `Serply`, `DuckDuckGo`, `TavilySearch`, `SearchApi` and `Bing` and inject the results directly into your chat experience.
- 📚 **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, External loaders). Load documents directly into chat or add files to your document library, effortlessly accessing them using the `#` command before a query.
- 🔍 **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.
- 🌐 **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.
- 🎨 **Image Generation Integration**: Seamlessly incorporate image generation capabilities using options such as AUTOMATIC1111 API or ComfyUI (local), and OpenAI's DALL-E (external), enriching your chat experience with dynamic visual content.
- 🎨 **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.
- ⚙️ **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.
- 🔐 **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.
- 🗄️ **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.
- 🔍 **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.
- 🔐 **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.
- ☁️ **Cloud-Native Integration**: Native support for Google Drive and OneDrive/SharePoint file picking, enabling seamless document import from enterprise cloud storage.
- 📊 **Production Observability**: Built-in OpenTelemetry support for traces, metrics, and logs, enabling comprehensive monitoring with your existing observability stack.
- ⚖️ **Horizontal Scalability**: Redis-backed session management and WebSocket support for multi-worker and multi-node deployments behind load balancers.
- 🌐🌍 **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!
- 🧩 **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.
@@ -65,43 +79,6 @@ For more information, be sure to check out our [Open WebUI Documentation](https:
Want to learn more about Open WebUI's features? Check out our [Open WebUI documentation](https://docs.openwebui.com/features) for a comprehensive overview!
## Sponsors 🙌
#### Emerald
<table>
<!-- <tr>
<td>
<a href="https://n8n.io/" target="_blank">
<img src="https://docs.openwebui.com/sponsors/logos/n8n.png" alt="n8n" style="width: 8rem; height: 8rem; border-radius: .75rem;" />
</a>
</td>
<td>
<a href="https://n8n.io/">n8n</a> • Does your interface have a backend yet?<br>Try <a href="https://n8n.io/">n8n</a>
</td>
</tr> -->
<tr>
<td>
<a href="https://tailscale.com/blog/self-host-a-local-ai-stack/?utm_source=OpenWebUI&utm_medium=paid-ad-placement&utm_campaign=OpenWebUI-Docs" target="_blank">
<img src="https://docs.openwebui.com/sponsors/logos/tailscale.png" alt="Tailscale" style="width: 8rem; height: 8rem; border-radius: .75rem;" />
</a>
</td>
<td>
<a href="https://tailscale.com/blog/self-host-a-local-ai-stack/?utm_source=OpenWebUI&utm_medium=paid-ad-placement&utm_campaign=OpenWebUI-Docs">Tailscale</a> • Connect self-hosted AI to any device with Tailscale
</td>
</tr>
<tr>
<td>
<a href="https://warp.dev/open-webui" target="_blank">
<img src="https://docs.openwebui.com/sponsors/logos/warp.png" alt="Warp" style="width: 8rem; height: 8rem; border-radius: .75rem;" />
</a>
</td>
<td>
<a href="https://warp.dev/open-webui">Warp</a> • The intelligent terminal for developers
</td>
</tr>
</table>
---
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!
@@ -195,7 +172,7 @@ After installation, you can access Open WebUI at [http://localhost:3000](http://
We offer various installation alternatives, including non-Docker native installation methods, Docker Compose, Kustomize, and Helm. Visit our [Open WebUI Documentation](https://docs.openwebui.com/getting-started/) or join our [Discord community](https://discord.gg/5rJgQTnV4s) for comprehensive guidance.
Look at the [Local Development Guide](https://docs.openwebui.com/getting-started/advanced-topics/development) for instructions on setting up a local development environment.
Look at the [Local Development Guide](https://docs.openwebui.com/getting-started/development) for instructions on setting up a local development environment.
### Troubleshooting
@@ -213,14 +190,6 @@ docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_BASE_URL=
### Keeping Your Docker Installation Up-to-Date
In case you want to update your local Docker installation to the latest version, you can do it with [Watchtower](https://containrrr.dev/watchtower/):
```bash
docker run --rm --volume /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower --run-once open-webui
```
In the last part of the command, replace `open-webui` with your container name if it is different.
Check our Updating Guide available in our [Open WebUI Documentation](https://docs.openwebui.com/getting-started/updating).
### Using the Dev Branch 🌙
@@ -248,7 +217,7 @@ Discover upcoming features on our roadmap in the [Open WebUI Documentation](http
## License 📜
This project is licensed under the [Open WebUI License](LICENSE), a revised BSD-3-Clause license. You receive all the same rights as the classic BSD-3 license: you can use, modify, and distribute the software, including in proprietary and commercial products, with minimal restrictions. The only additional requirement is to preserve the "Open WebUI" branding, as detailed in the LICENSE file. For full terms, see the [LICENSE](LICENSE) document. 📄
This project contains code under multiple licenses. The current codebase includes components licensed under the Open WebUI License with an additional requirement to preserve the "Open WebUI" branding, as well as prior contributions under their respective original licenses. For a detailed record of license changes and the applicable terms for each section of the code, please refer to [LICENSE_HISTORY](./LICENSE_HISTORY). For complete and updated licensing details, please see the [LICENSE](./LICENSE) and [LICENSE_HISTORY](./LICENSE_HISTORY) files.
## Support 💬
+1 -1
View File
@@ -1,3 +1,3 @@
export CORS_ALLOW_ORIGIN="http://localhost:5173"
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 '*' --reload
File diff suppressed because it is too large Load Diff
+6 -1
View File
@@ -38,13 +38,14 @@ class ERROR_MESSAGES(str, Enum):
ID_TAKEN = "Uh-oh! This id is already registered. Please choose another id string."
MODEL_ID_TAKEN = "Uh-oh! This model id is already registered. Please choose another model id string."
NAME_TAG_TAKEN = "Uh-oh! This name tag is already registered. Please choose another name tag string."
MODEL_ID_TOO_LONG = "The model id is too long. Please make sure your model id is less than 256 characters long."
INVALID_TOKEN = (
"Your session has expired or the token is invalid. Please sign in again."
)
INVALID_CRED = "The email or password provided is incorrect. Please check for typos and try logging in again."
INVALID_EMAIL_FORMAT = "The email format you entered is invalid. Please double-check and make sure you're using a valid email address (e.g., yourname@example.com)."
INVALID_PASSWORD = (
INCORRECT_PASSWORD = (
"The password provided is incorrect. Please check for typos and try again."
)
INVALID_TRUSTED_HEADER = "Your provider has not provided a trusted header. Please contact your administrator for assistance."
@@ -104,6 +105,10 @@ class ERROR_MESSAGES(str, Enum):
)
FILE_NOT_PROCESSED = "Extracted content is not available for this file. Please ensure that the file is processed before proceeding."
INVALID_PASSWORD = lambda err="": (
err if err else "The password does not meet the required validation criteria."
)
class TASKS(str, Enum):
def __str__(self) -> str:
+290 -39
View File
@@ -5,9 +5,14 @@ import os
import pkgutil
import sys
import shutil
import traceback
from datetime import datetime, timezone
from typing import Any
from uuid import uuid4
from pathlib import Path
from cryptography.hazmat.primitives import serialization
import re
import markdown
from bs4 import BeautifulSoup
@@ -70,9 +75,51 @@ except Exception:
# LOGGING
####################################
_LEVEL_MAP = {
"DEBUG": "debug",
"INFO": "info",
"WARNING": "warn",
"ERROR": "error",
"CRITICAL": "fatal",
}
class JSONFormatter(logging.Formatter):
"""Format log records as single-line JSON objects for structured logging."""
def format(self, record: logging.LogRecord) -> str:
log_entry: dict[str, Any] = {
"ts": datetime.fromtimestamp(record.created, tz=timezone.utc).isoformat(
timespec="milliseconds"
),
"level": _LEVEL_MAP.get(record.levelname, record.levelname.lower()),
"msg": record.getMessage(),
"caller": record.name,
}
if record.exc_info and record.exc_info[0] is not None:
log_entry["error"] = "".join(
traceback.format_exception(*record.exc_info)
).rstrip()
elif record.exc_text:
log_entry["error"] = record.exc_text
if record.stack_info:
log_entry["stacktrace"] = record.stack_info
return json.dumps(log_entry, ensure_ascii=False, default=str)
LOG_FORMAT = os.environ.get("LOG_FORMAT", "").lower()
GLOBAL_LOG_LEVEL = os.environ.get("GLOBAL_LOG_LEVEL", "").upper()
if GLOBAL_LOG_LEVEL in logging.getLevelNamesMapping():
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL, force=True)
if LOG_FORMAT == "json":
_handler = logging.StreamHandler(sys.stdout)
_handler.setFormatter(JSONFormatter())
logging.basicConfig(handlers=[_handler], level=GLOBAL_LOG_LEVEL, force=True)
else:
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL, force=True)
else:
GLOBAL_LOG_LEVEL = "INFO"
@@ -83,32 +130,7 @@ if "cuda_error" in locals():
log.exception(cuda_error)
del cuda_error
log_sources = [
"AUDIO",
"COMFYUI",
"CONFIG",
"DB",
"IMAGES",
"MAIN",
"MODELS",
"OLLAMA",
"OPENAI",
"RAG",
"WEBHOOK",
"SOCKET",
"OAUTH",
]
SRC_LOG_LEVELS = {}
for source in log_sources:
log_env_var = source + "_LOG_LEVEL"
SRC_LOG_LEVELS[source] = os.environ.get(log_env_var, "").upper()
if SRC_LOG_LEVELS[source] not in logging.getLevelNamesMapping():
SRC_LOG_LEVELS[source] = GLOBAL_LOG_LEVEL
log.info(f"{log_env_var}: {SRC_LOG_LEVELS[source]}")
log.setLevel(SRC_LOG_LEVELS["CONFIG"])
SRC_LOG_LEVELS = {} # Legacy variable, do not remove
WEBUI_NAME = os.environ.get("WEBUI_NAME", "Open WebUI")
if WEBUI_NAME != "Open WebUI":
@@ -135,8 +157,13 @@ else:
PACKAGE_DATA = {"version": "0.0.0"}
VERSION = PACKAGE_DATA["version"]
DEPLOYMENT_ID = os.environ.get("DEPLOYMENT_ID", "")
INSTANCE_ID = os.environ.get("INSTANCE_ID", str(uuid4()))
ENABLE_DB_MIGRATIONS = os.environ.get("ENABLE_DB_MIGRATIONS", "True").lower() == "true"
# Function to parse each section
def parse_section(section):
@@ -212,6 +239,35 @@ ENABLE_FORWARD_USER_INFO_HEADERS = (
os.environ.get("ENABLE_FORWARD_USER_INFO_HEADERS", "False").lower() == "true"
)
# Header names for user info forwarding (customizable via environment variables)
FORWARD_USER_INFO_HEADER_USER_NAME = os.environ.get(
"FORWARD_USER_INFO_HEADER_USER_NAME", "X-OpenWebUI-User-Name"
)
FORWARD_USER_INFO_HEADER_USER_ID = os.environ.get(
"FORWARD_USER_INFO_HEADER_USER_ID", "X-OpenWebUI-User-Id"
)
FORWARD_USER_INFO_HEADER_USER_EMAIL = os.environ.get(
"FORWARD_USER_INFO_HEADER_USER_EMAIL", "X-OpenWebUI-User-Email"
)
FORWARD_USER_INFO_HEADER_USER_ROLE = os.environ.get(
"FORWARD_USER_INFO_HEADER_USER_ROLE", "X-OpenWebUI-User-Role"
)
# Header name for chat ID forwarding (customizable via environment variable)
FORWARD_SESSION_INFO_HEADER_MESSAGE_ID = os.environ.get(
"FORWARD_SESSION_INFO_HEADER_MESSAGE_ID", "X-OpenWebUI-Message-Id"
)
FORWARD_SESSION_INFO_HEADER_CHAT_ID = os.environ.get(
"FORWARD_SESSION_INFO_HEADER_CHAT_ID", "X-OpenWebUI-Chat-Id"
)
# Experimental feature, may be removed in future
ENABLE_STAR_SESSIONS_MIDDLEWARE = (
os.environ.get("ENABLE_STAR_SESSIONS_MIDDLEWARE", "False").lower() == "true"
)
ENABLE_EASTER_EGGS = os.environ.get("ENABLE_EASTER_EGGS", "True").lower() == "true"
####################################
# WEBUI_BUILD_HASH
####################################
@@ -354,6 +410,16 @@ if DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL is not None:
except Exception:
DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL = 0.0
# When enabled, get_db_context reuses existing sessions; set to False to always create new sessions
DATABASE_ENABLE_SESSION_SHARING = (
os.environ.get("DATABASE_ENABLE_SESSION_SHARING", "False").lower() == "true"
)
# Enable public visibility of active user count (when disabled, only admins can see it)
ENABLE_PUBLIC_ACTIVE_USERS_COUNT = (
os.environ.get("ENABLE_PUBLIC_ACTIVE_USERS_COUNT", "True").lower() == "true"
)
RESET_CONFIG_ON_START = (
os.environ.get("RESET_CONFIG_ON_START", "False").lower() == "true"
)
@@ -364,6 +430,8 @@ ENABLE_REALTIME_CHAT_SAVE = (
ENABLE_QUERIES_CACHE = os.environ.get("ENABLE_QUERIES_CACHE", "False").lower() == "true"
RAG_SYSTEM_CONTEXT = os.environ.get("RAG_SYSTEM_CONTEXT", "False").lower() == "true"
####################################
# REDIS
####################################
@@ -385,6 +453,25 @@ try:
except ValueError:
REDIS_SENTINEL_MAX_RETRY_COUNT = 2
REDIS_SOCKET_CONNECT_TIMEOUT = os.environ.get("REDIS_SOCKET_CONNECT_TIMEOUT", "")
try:
REDIS_SOCKET_CONNECT_TIMEOUT = float(REDIS_SOCKET_CONNECT_TIMEOUT)
except ValueError:
REDIS_SOCKET_CONNECT_TIMEOUT = None
REDIS_RECONNECT_DELAY = os.environ.get("REDIS_RECONNECT_DELAY", "")
if REDIS_RECONNECT_DELAY == "":
REDIS_RECONNECT_DELAY = None
else:
try:
REDIS_RECONNECT_DELAY = float(REDIS_RECONNECT_DELAY)
if REDIS_RECONNECT_DELAY < 0:
REDIS_RECONNECT_DELAY = None
except Exception:
REDIS_RECONNECT_DELAY = None
####################################
# UVICORN WORKERS
####################################
@@ -412,6 +499,16 @@ ENABLE_SIGNUP_PASSWORD_CONFIRMATION = (
os.environ.get("ENABLE_SIGNUP_PASSWORD_CONFIRMATION", "False").lower() == "true"
)
####################################
# Admin Account Runtime Creation
####################################
# Optional env vars for creating an admin account on startup
# Useful for headless/automated deployments
WEBUI_ADMIN_EMAIL = os.environ.get("WEBUI_ADMIN_EMAIL", "")
WEBUI_ADMIN_PASSWORD = os.environ.get("WEBUI_ADMIN_PASSWORD", "")
WEBUI_ADMIN_NAME = os.environ.get("WEBUI_ADMIN_NAME", "Admin")
WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get(
"WEBUI_AUTH_TRUSTED_EMAIL_HEADER", None
)
@@ -421,6 +518,27 @@ WEBUI_AUTH_TRUSTED_GROUPS_HEADER = os.environ.get(
)
ENABLE_PASSWORD_VALIDATION = (
os.environ.get("ENABLE_PASSWORD_VALIDATION", "False").lower() == "true"
)
PASSWORD_VALIDATION_REGEX_PATTERN = os.environ.get(
"PASSWORD_VALIDATION_REGEX_PATTERN",
r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$",
)
try:
PASSWORD_VALIDATION_REGEX_PATTERN = rf"{PASSWORD_VALIDATION_REGEX_PATTERN}"
PASSWORD_VALIDATION_REGEX_PATTERN = re.compile(PASSWORD_VALIDATION_REGEX_PATTERN)
except Exception as e:
log.error(f"Invalid PASSWORD_VALIDATION_REGEX_PATTERN: {e}")
PASSWORD_VALIDATION_REGEX_PATTERN = re.compile(
r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$"
)
PASSWORD_VALIDATION_HINT = os.environ.get("PASSWORD_VALIDATION_HINT", "")
BYPASS_MODEL_ACCESS_CONTROL = (
os.environ.get("BYPASS_MODEL_ACCESS_CONTROL", "False").lower() == "true"
)
@@ -468,23 +586,49 @@ ENABLE_COMPRESSION_MIDDLEWARE = (
####################################
# OAUTH Configuration
####################################
ENABLE_OAUTH_EMAIL_FALLBACK = (
os.environ.get("ENABLE_OAUTH_EMAIL_FALLBACK", "False").lower() == "true"
)
ENABLE_OAUTH_ID_TOKEN_COOKIE = (
os.environ.get("ENABLE_OAUTH_ID_TOKEN_COOKIE", "True").lower() == "true"
)
OAUTH_CLIENT_INFO_ENCRYPTION_KEY = os.environ.get(
"OAUTH_CLIENT_INFO_ENCRYPTION_KEY", WEBUI_SECRET_KEY
)
OAUTH_SESSION_TOKEN_ENCRYPTION_KEY = os.environ.get(
"OAUTH_SESSION_TOKEN_ENCRYPTION_KEY", WEBUI_SECRET_KEY
)
# Maximum number of concurrent OAuth sessions per user per provider
# This prevents unbounded session growth while allowing multi-device usage
OAUTH_MAX_SESSIONS_PER_USER = int(os.environ.get("OAUTH_MAX_SESSIONS_PER_USER", "10"))
# Token Exchange Configuration
# Allows external apps to exchange OAuth tokens for OpenWebUI tokens
ENABLE_OAUTH_TOKEN_EXCHANGE = (
os.environ.get("ENABLE_OAUTH_TOKEN_EXCHANGE", "False").lower() == "true"
)
####################################
# SCIM Configuration
####################################
SCIM_ENABLED = os.environ.get("SCIM_ENABLED", "False").lower() == "true"
ENABLE_SCIM = (
os.environ.get("ENABLE_SCIM", os.environ.get("SCIM_ENABLED", "False")).lower()
== "true"
)
SCIM_TOKEN = os.environ.get("SCIM_TOKEN", "")
SCIM_AUTH_PROVIDER = os.environ.get("SCIM_AUTH_PROVIDER", "")
if ENABLE_SCIM and not SCIM_AUTH_PROVIDER:
log.warning(
"SCIM is enabled but SCIM_AUTH_PROVIDER is not set. "
"Set SCIM_AUTH_PROVIDER to the OAuth provider name (e.g. 'microsoft', 'oidc') "
"to enable externalId storage."
)
####################################
# LICENSE_KEY
@@ -502,21 +646,21 @@ LICENSE_PUBLIC_KEY = os.environ.get("LICENSE_PUBLIC_KEY", "")
pk = None
if LICENSE_PUBLIC_KEY:
pk = serialization.load_pem_public_key(
f"""
pk = serialization.load_pem_public_key(f"""
-----BEGIN PUBLIC KEY-----
{LICENSE_PUBLIC_KEY}
-----END PUBLIC KEY-----
""".encode(
"utf-8"
)
)
""".encode("utf-8"))
####################################
# MODELS
####################################
ENABLE_CUSTOM_MODEL_FALLBACK = (
os.environ.get("ENABLE_CUSTOM_MODEL_FALLBACK", "False").lower() == "true"
)
MODELS_CACHE_TTL = os.environ.get("MODELS_CACHE_TTL", "1")
if MODELS_CACHE_TTL == "":
MODELS_CACHE_TTL = None
@@ -531,6 +675,11 @@ else:
# CHAT
####################################
ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION = (
os.environ.get("ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION", "False").lower()
== "true"
)
CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = os.environ.get(
"CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE", "1"
)
@@ -547,16 +696,31 @@ else:
CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES = os.environ.get(
"CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES", "10"
"CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES", "30"
)
if CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES == "":
CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES = 10
CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES = 30
else:
try:
CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES = int(CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES)
except Exception:
CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES = 10
CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES = 30
CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE = os.environ.get(
"CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE", ""
)
if CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE == "":
CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE = None
else:
try:
CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE = int(
CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE
)
except Exception:
CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE = None
####################################
@@ -570,6 +734,24 @@ ENABLE_WEBSOCKET_SUPPORT = (
WEBSOCKET_MANAGER = os.environ.get("WEBSOCKET_MANAGER", "")
WEBSOCKET_REDIS_OPTIONS = os.environ.get("WEBSOCKET_REDIS_OPTIONS", "")
if WEBSOCKET_REDIS_OPTIONS == "":
if REDIS_SOCKET_CONNECT_TIMEOUT:
WEBSOCKET_REDIS_OPTIONS = {
"socket_connect_timeout": REDIS_SOCKET_CONNECT_TIMEOUT
}
else:
log.debug("No WEBSOCKET_REDIS_OPTIONS provided, defaulting to None")
WEBSOCKET_REDIS_OPTIONS = None
else:
try:
WEBSOCKET_REDIS_OPTIONS = json.loads(WEBSOCKET_REDIS_OPTIONS)
except Exception:
log.warning("Invalid WEBSOCKET_REDIS_OPTIONS, defaulting to None")
WEBSOCKET_REDIS_OPTIONS = None
WEBSOCKET_REDIS_URL = os.environ.get("WEBSOCKET_REDIS_URL", REDIS_URL)
WEBSOCKET_REDIS_CLUSTER = (
os.environ.get("WEBSOCKET_REDIS_CLUSTER", str(REDIS_CLUSTER)).lower() == "true"
@@ -584,7 +766,30 @@ except ValueError:
WEBSOCKET_SENTINEL_HOSTS = os.environ.get("WEBSOCKET_SENTINEL_HOSTS", "")
WEBSOCKET_SENTINEL_PORT = os.environ.get("WEBSOCKET_SENTINEL_PORT", "26379")
WEBSOCKET_SERVER_LOGGING = (
os.environ.get("WEBSOCKET_SERVER_LOGGING", "False").lower() == "true"
)
WEBSOCKET_SERVER_ENGINEIO_LOGGING = (
os.environ.get(
"WEBSOCKET_SERVER_ENGINEIO_LOGGING",
os.environ.get("WEBSOCKET_SERVER_LOGGING", "False"),
).lower()
== "true"
)
WEBSOCKET_SERVER_PING_TIMEOUT = os.environ.get("WEBSOCKET_SERVER_PING_TIMEOUT", "20")
try:
WEBSOCKET_SERVER_PING_TIMEOUT = int(WEBSOCKET_SERVER_PING_TIMEOUT)
except ValueError:
WEBSOCKET_SERVER_PING_TIMEOUT = 20
WEBSOCKET_SERVER_PING_INTERVAL = os.environ.get("WEBSOCKET_SERVER_PING_INTERVAL", "25")
try:
WEBSOCKET_SERVER_PING_INTERVAL = int(WEBSOCKET_SERVER_PING_INTERVAL)
except ValueError:
WEBSOCKET_SERVER_PING_INTERVAL = 25
REQUESTS_VERIFY = os.environ.get("REQUESTS_VERIFY", "True").lower() == "true"
AIOHTTP_CLIENT_TIMEOUT = os.environ.get("AIOHTTP_CLIENT_TIMEOUT", "")
@@ -635,6 +840,17 @@ AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL = (
)
RAG_EMBEDDING_TIMEOUT = os.environ.get("RAG_EMBEDDING_TIMEOUT", "")
if RAG_EMBEDDING_TIMEOUT == "":
RAG_EMBEDDING_TIMEOUT = None
else:
try:
RAG_EMBEDDING_TIMEOUT = int(RAG_EMBEDDING_TIMEOUT)
except Exception:
RAG_EMBEDDING_TIMEOUT = None
####################################
# SENTENCE TRANSFORMERS
####################################
@@ -679,6 +895,16 @@ else:
except Exception:
SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = None
# Whether to apply sigmoid normalization to CrossEncoder reranking scores.
# When enabled (default), scores are normalized to 0-1 range for proper
# relevance threshold behavior with MS MARCO models.
SENTENCE_TRANSFORMERS_CROSS_ENCODER_SIGMOID_ACTIVATION_FUNCTION = (
os.environ.get(
"SENTENCE_TRANSFORMERS_CROSS_ENCODER_SIGMOID_ACTIVATION_FUNCTION", "True"
).lower()
== "true"
)
####################################
# OFFLINE_MODE
####################################
@@ -695,8 +921,15 @@ if OFFLINE_MODE:
####################################
# AUDIT LOGGING
####################################
ENABLE_AUDIT_STDOUT = os.getenv("ENABLE_AUDIT_STDOUT", "False").lower() == "true"
ENABLE_AUDIT_LOGS_FILE = os.getenv("ENABLE_AUDIT_LOGS_FILE", "True").lower() == "true"
# Where to store log file
AUDIT_LOGS_FILE_PATH = f"{DATA_DIR}/audit.log"
# Defaults to the DATA_DIR/audit.log. To set AUDIT_LOGS_FILE_PATH you need to
# provide the whole path, like: /app/audit.log
AUDIT_LOGS_FILE_PATH = os.getenv("AUDIT_LOGS_FILE_PATH", f"{DATA_DIR}/audit.log")
# Maximum size of a file before rotating into a new log file
AUDIT_LOG_FILE_ROTATION_SIZE = os.getenv("AUDIT_LOG_FILE_ROTATION_SIZE", "10MB")
@@ -794,6 +1027,11 @@ OTEL_LOGS_OTLP_SPAN_EXPORTER = os.environ.get(
# TOOLS/FUNCTIONS PIP OPTIONS
####################################
ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS = (
os.environ.get("ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS", "True").lower()
== "true"
)
PIP_OPTIONS = os.getenv("PIP_OPTIONS", "").split()
PIP_PACKAGE_INDEX_OPTIONS = os.getenv("PIP_PACKAGE_INDEX_OPTIONS", "").split()
@@ -803,3 +1041,16 @@ PIP_PACKAGE_INDEX_OPTIONS = os.getenv("PIP_PACKAGE_INDEX_OPTIONS", "").split()
####################################
EXTERNAL_PWA_MANIFEST_URL = os.environ.get("EXTERNAL_PWA_MANIFEST_URL")
####################################
# GROUP DEFAULTS
####################################
# Controls the default "Who can share to this group" setting for new groups.
# Env var values: "true" (anyone), "false" (no one), "members" (only group members).
_default_group_share = (
os.environ.get("DEFAULT_GROUP_SHARE_PERMISSION", "members").strip().lower()
)
DEFAULT_GROUP_SHARE_PERMISSION = (
"members" if _default_group_share == "members" else _default_group_share == "true"
)
+73 -52
View File
@@ -19,6 +19,7 @@ from fastapi import (
from starlette.responses import Response, StreamingResponse
from open_webui.constants import ERROR_MESSAGES
from open_webui.socket.main import (
get_event_call,
get_event_emitter,
@@ -34,9 +35,8 @@ from open_webui.utils.plugin import (
get_function_module_from_cache,
)
from open_webui.utils.tools import get_tools
from open_webui.utils.access_control import has_access
from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL
from open_webui.env import GLOBAL_LOG_LEVEL
from open_webui.utils.misc import (
add_or_update_system_message,
@@ -50,18 +50,28 @@ from open_webui.utils.payload import (
apply_system_prompt_to_body,
)
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MAIN"])
def get_function_module_by_id(request: Request, pipe_id: str):
function_module, _, _ = get_function_module_from_cache(request, pipe_id)
if hasattr(function_module, "valves") and hasattr(function_module, "Valves"):
Valves = function_module.Valves
valves = Functions.get_function_valves_by_id(pipe_id)
function_module.valves = function_module.Valves(**(valves if valves else {}))
if valves:
try:
function_module.valves = Valves(
**{k: v for k, v in valves.items() if v is not None}
)
except Exception as e:
log.exception(f"Error loading valves for function {pipe_id}: {e}")
raise e
else:
function_module.valves = Valves()
return function_module
@@ -70,65 +80,75 @@ async def get_function_models(request):
pipe_models = []
for pipe in pipes:
function_module = get_function_module_by_id(request, pipe.id)
try:
function_module = get_function_module_by_id(request, pipe.id)
# Check if function is a manifold
if hasattr(function_module, "pipes"):
sub_pipes = []
has_user_valves = False
if hasattr(function_module, "UserValves"):
has_user_valves = True
# Handle pipes being a list, sync function, or async function
try:
if callable(function_module.pipes):
if asyncio.iscoroutinefunction(function_module.pipes):
sub_pipes = await function_module.pipes()
else:
sub_pipes = function_module.pipes()
else:
sub_pipes = function_module.pipes
except Exception as e:
log.exception(e)
# Check if function is a manifold
if hasattr(function_module, "pipes"):
sub_pipes = []
log.debug(
f"get_function_models: function '{pipe.id}' is a manifold of {sub_pipes}"
)
# Handle pipes being a list, sync function, or async function
try:
if callable(function_module.pipes):
if asyncio.iscoroutinefunction(function_module.pipes):
sub_pipes = await function_module.pipes()
else:
sub_pipes = function_module.pipes()
else:
sub_pipes = function_module.pipes
except Exception as e:
log.exception(e)
sub_pipes = []
for p in sub_pipes:
sub_pipe_id = f'{pipe.id}.{p["id"]}'
sub_pipe_name = p["name"]
log.debug(
f"get_function_models: function '{pipe.id}' is a manifold of {sub_pipes}"
)
if hasattr(function_module, "name"):
sub_pipe_name = f"{function_module.name}{sub_pipe_name}"
for p in sub_pipes:
sub_pipe_id = f'{pipe.id}.{p["id"]}'
sub_pipe_name = p["name"]
pipe_flag = {"type": pipe.type}
if hasattr(function_module, "name"):
sub_pipe_name = f"{function_module.name}{sub_pipe_name}"
pipe_flag = {"type": pipe.type}
pipe_models.append(
{
"id": sub_pipe_id,
"name": sub_pipe_name,
"object": "model",
"created": pipe.created_at,
"owned_by": "openai",
"pipe": pipe_flag,
"has_user_valves": has_user_valves,
}
)
else:
pipe_flag = {"type": "pipe"}
log.debug(
f"get_function_models: function '{pipe.id}' is a single pipe {{ 'id': {pipe.id}, 'name': {pipe.name} }}"
)
pipe_models.append(
{
"id": sub_pipe_id,
"name": sub_pipe_name,
"id": pipe.id,
"name": pipe.name,
"object": "model",
"created": pipe.created_at,
"owned_by": "openai",
"pipe": pipe_flag,
"has_user_valves": has_user_valves,
}
)
else:
pipe_flag = {"type": "pipe"}
log.debug(
f"get_function_models: function '{pipe.id}' is a single pipe {{ 'id': {pipe.id}, 'name': {pipe.name} }}"
)
pipe_models.append(
{
"id": pipe.id,
"name": pipe.name,
"object": "model",
"created": pipe.created_at,
"owned_by": "openai",
"pipe": pipe_flag,
}
)
except Exception as e:
log.exception(e)
continue
return pipe_models
@@ -221,10 +241,11 @@ async def generate_function_chat_completion(
oauth_token = None
try:
oauth_token = request.app.state.oauth_manager.get_oauth_token(
user.id,
request.cookies.get("oauth_session_id", None),
)
if request.cookies.get("oauth_session_id", None):
oauth_token = await request.app.state.oauth_manager.get_oauth_token(
user.id,
request.cookies.get("oauth_session_id", None),
)
except Exception as e:
log.error(f"Error getting OAuth token: {e}")
+15 -7
View File
@@ -9,23 +9,23 @@ from open_webui.env import (
OPEN_WEBUI_DIR,
DATABASE_URL,
DATABASE_SCHEMA,
SRC_LOG_LEVELS,
DATABASE_POOL_MAX_OVERFLOW,
DATABASE_POOL_RECYCLE,
DATABASE_POOL_SIZE,
DATABASE_POOL_TIMEOUT,
DATABASE_ENABLE_SQLITE_WAL,
DATABASE_ENABLE_SESSION_SHARING,
ENABLE_DB_MIGRATIONS,
)
from peewee_migrate import Router
from sqlalchemy import Dialect, create_engine, MetaData, event, types
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.orm import scoped_session, sessionmaker, Session
from sqlalchemy.pool import QueuePool, NullPool
from sqlalchemy.sql.type_api import _T
from typing_extensions import Self
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["DB"])
class JSONField(types.TypeDecorator):
@@ -77,7 +77,8 @@ def handle_peewee_migration(DATABASE_URL):
assert db.is_closed(), "Database connection is still open."
handle_peewee_migration(DATABASE_URL)
if ENABLE_DB_MIGRATIONS:
handle_peewee_migration(DATABASE_URL)
SQLALCHEMY_DATABASE_URL = DATABASE_URL
@@ -92,8 +93,6 @@ if SQLALCHEMY_DATABASE_URL.startswith("sqlite+sqlcipher://"):
# Extract database path from SQLCipher URL
db_path = SQLALCHEMY_DATABASE_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():
@@ -150,7 +149,7 @@ SessionLocal = sessionmaker(
)
metadata_obj = MetaData(schema=DATABASE_SCHEMA)
Base = declarative_base(metadata=metadata_obj)
Session = scoped_session(SessionLocal)
ScopedSession = scoped_session(SessionLocal)
def get_session():
@@ -162,3 +161,12 @@ def get_session():
get_db = contextmanager(get_session)
@contextmanager
def get_db_context(db: Optional[Session] = None):
if isinstance(db, Session) and DATABASE_ENABLE_SESSION_SHARING:
yield db
else:
with get_db() as session:
yield session
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
@@ -25,7 +25,6 @@ from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
-4
View File
@@ -2,7 +2,6 @@ import logging
import os
from contextvars import ContextVar
from open_webui.env import SRC_LOG_LEVELS
from peewee import *
from peewee import InterfaceError as PeeWeeInterfaceError
from peewee import PostgresqlDatabase
@@ -10,7 +9,6 @@ from playhouse.db_url import connect, parse
from playhouse.shortcuts import ReconnectMixin
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["DB"])
db_state_default = {"closed": None, "conn": None, "ctx": None, "transactions": None}
db_state = ContextVar("db_state", default=db_state_default.copy())
@@ -56,8 +54,6 @@ def register_connection(db_url):
# 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://", "")
if db_path.startswith("/"):
db_path = db_path[1:] # Remove leading slash for relative paths
# Use Peewee's native SqlCipherDatabase with encryption
db = SqlCipherDatabase(db_path, passphrase=database_password)
+784 -162
View File
File diff suppressed because it is too large Load Diff
+9 -1
View File
@@ -1,8 +1,9 @@
import logging
from logging.config import fileConfig
from alembic import context
from open_webui.models.auths import Auth
from open_webui.env import DATABASE_URL, DATABASE_PASSWORD
from open_webui.env import DATABASE_URL, DATABASE_PASSWORD, LOG_FORMAT
from sqlalchemy import engine_from_config, pool, create_engine
# this is the Alembic Config object, which provides
@@ -14,6 +15,13 @@ config = context.config
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.
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
@@ -0,0 +1,102 @@
"""Update messages and channel member table
Revision ID: 2f1211949ecc
Revises: 37f288994c47
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
# revision identifiers, used by Alembic.
revision: str = "2f1211949ecc"
down_revision: Union[str, None] = "37f288994c47"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# 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)
)
# 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))
def downgrade() -> None:
op.drop_column("channel_member", "updated_at")
op.drop_column("channel_member", "last_read_at")
op.drop_column("channel_member", "meta")
op.drop_column("channel_member", "data")
op.drop_column("channel_member", "is_channel_pinned")
op.drop_column("channel_member", "is_channel_muted")
op.drop_column("message", "pinned_by")
op.drop_column("message", "pinned_at")
op.drop_column("message", "is_pinned")
@@ -0,0 +1,247 @@
"""Add prompt history table
Revision ID: 374d2f66af06
Revises: c440947495f3
Create Date: 2026-01-23 17:15:00.000000
"""
from typing import Sequence, Union
import uuid
from alembic import op
import sqlalchemy as sa
revision: str = "374d2f66af06"
down_revision: Union[str, None] = "c440947495f3"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
conn = op.get_bind()
# 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.
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,
)
).fetchall()
except Exception:
# Fallback if table doesn't exist (new install)
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,
)
)
# 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
op.drop_table("prompt")
op.rename_table("prompt_new", "prompt")
def downgrade() -> None:
conn = op.get_bind()
# Step 1: Read new data
prompt_table = sa.table(
"prompt",
sa.column("command", sa.String()),
sa.column("name", sa.Text()),
sa.column("created_at", sa.BigInteger()),
sa.column("user_id", sa.Text()),
sa.column("content", sa.Text()),
sa.column("access_control", sa.JSON()),
)
try:
current_data = conn.execute(
sa.select(
prompt_table.c.command,
prompt_table.c.name,
prompt_table.c.created_at,
prompt_table.c.user_id,
prompt_table.c.content,
prompt_table.c.access_control,
)
).fetchall()
except Exception:
current_data = []
# Step 2: Drop history and table
op.drop_table("prompt_history")
op.drop_table("prompt")
# Step 3: Recreate old table (command as PK?)
# Assuming old schema:
op.create_table(
"prompt",
sa.Column("command", sa.String(), primary_key=True),
sa.Column("user_id", sa.String()),
sa.Column("title", sa.Text()),
sa.Column("content", sa.Text()),
sa.Column("timestamp", sa.BigInteger()),
sa.Column("access_control", sa.JSON()),
sa.Column("id", sa.Integer(), nullable=True),
)
# Step 4: Restore data
old_prompt_table = sa.table(
"prompt",
sa.column("command", sa.String()),
sa.column("user_id", sa.String()),
sa.column("title", sa.Text()),
sa.column("content", sa.Text()),
sa.column("timestamp", sa.BigInteger()),
sa.column("access_control", sa.JSON()),
)
for row in current_data:
command = row[0]
name = row[1]
created_at = row[2]
user_id = row[3]
content = row[4]
access_control = row[5]
# Restore leading /
old_command = (
"/" + command if command and not command.startswith("/") else command
)
conn.execute(
sa.insert(old_prompt_table).values(
command=old_command,
user_id=user_id,
title=name,
content=content,
timestamp=created_at,
access_control=access_control,
)
)
@@ -0,0 +1,145 @@
"""add_group_member_table
Revision ID: 37f288994c47
Revises: a5c220713937
Create Date: 2025-11-17 03:45:25.123939
"""
import uuid
import time
import json
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "37f288994c47"
down_revision: Union[str, None] = "a5c220713937"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# 1. Create new table
op.create_table(
"group_member",
sa.Column("id", sa.Text(), primary_key=True, unique=True, nullable=False),
sa.Column(
"group_id",
sa.Text(),
sa.ForeignKey("group.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column(
"user_id",
sa.Text(),
sa.ForeignKey("user.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column("created_at", sa.BigInteger(), nullable=True),
sa.Column("updated_at", sa.BigInteger(), nullable=True),
sa.UniqueConstraint("group_id", "user_id", name="uq_group_member_group_user"),
)
connection = op.get_bind()
# 2. Read existing group with user_ids JSON column
group_table = sa.Table(
"group",
sa.MetaData(),
sa.Column("id", sa.Text()),
sa.Column("user_ids", sa.JSON()), # JSON stored as text in SQLite + PG
)
results = connection.execute(
sa.select(group_table.c.id, group_table.c.user_ids)
).fetchall()
print(results)
# 3. Insert members into group_member table
gm_table = sa.Table(
"group_member",
sa.MetaData(),
sa.Column("id", sa.Text()),
sa.Column("group_id", sa.Text()),
sa.Column("user_id", sa.Text()),
sa.Column("created_at", sa.BigInteger()),
sa.Column("updated_at", sa.BigInteger()),
)
now = int(time.time())
for group_id, user_ids in results:
if not user_ids:
continue
if isinstance(user_ids, str):
try:
user_ids = json.loads(user_ids)
except Exception:
continue # skip invalid JSON
if not isinstance(user_ids, list):
continue
rows = [
{
"id": str(uuid.uuid4()),
"group_id": group_id,
"user_id": uid,
"created_at": now,
"updated_at": now,
}
for uid in user_ids
]
if rows:
connection.execute(gm_table.insert(), rows)
# 4. Optionally drop the old column
with op.batch_alter_table("group") as batch:
batch.drop_column("user_ids")
def downgrade():
# Reverse: restore user_ids column
with op.batch_alter_table("group") as batch:
batch.add_column(sa.Column("user_ids", sa.JSON()))
connection = op.get_bind()
gm_table = sa.Table(
"group_member",
sa.MetaData(),
sa.Column("group_id", sa.Text()),
sa.Column("user_id", sa.Text()),
sa.Column("created_at", sa.BigInteger()),
sa.Column("updated_at", sa.BigInteger()),
)
group_table = sa.Table(
"group",
sa.MetaData(),
sa.Column("id", sa.Text()),
sa.Column("user_ids", sa.JSON()),
)
# Build JSON arrays again
results = connection.execute(sa.select(group_table.c.id)).fetchall()
for (group_id,) in results:
members = connection.execute(
sa.select(gm_table.c.user_id).where(gm_table.c.group_id == group_id)
).fetchall()
member_ids = [m[0] for m in members]
connection.execute(
group_table.update()
.where(group_table.c.id == group_id)
.values(user_ids=member_ids)
)
# Drop the new table
op.drop_table("group_member")
@@ -11,7 +11,6 @@ from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "38d63c18f30f"
down_revision: Union[str, None] = "3af16a1c9fb6"
@@ -20,18 +19,46 @@ 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")
pk_columns = inspector.get_pk_constraint("user")["constrained_columns"]
id_column = next((col for col in columns if col["name"] == "id"), None)
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(), nullable=False),
sa.Column("user_id", sa.Text(), nullable=False),
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),
sa.PrimaryKeyConstraint("id"),
sa.ForeignKeyConstraint(["user_id"], ["user.id"], ondelete="CASCADE"),
)
# Create indexes for better performance
@@ -0,0 +1,169 @@
"""Add knowledge_file table
Revision ID: 3e0e00844bb0
Revises: 90ef40d4714e
Create Date: 2025-12-02 06:54:19.401334
"""
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
# revision identifiers, used by Alembic.
revision: str = "3e0e00844bb0"
down_revision: Union[str, None] = "90ef40d4714e"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table(
"knowledge_file",
sa.Column("id", sa.Text(), primary_key=True),
sa.Column("user_id", sa.Text(), nullable=False),
sa.Column(
"knowledge_id",
sa.Text(),
sa.ForeignKey("knowledge.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_knowledge_file_knowledge_id", "knowledge_id"),
sa.Index("ix_knowledge_file_file_id", "file_id"),
sa.Index("ix_knowledge_file_user_id", "user_id"),
# unique constraints
sa.UniqueConstraint(
"knowledge_id", "file_id", name="uq_knowledge_file_knowledge_file"
), # prevent duplicate entries
)
connection = op.get_bind()
# 2. Read existing group with user_ids JSON column
knowledge_table = sa.Table(
"knowledge",
sa.MetaData(),
sa.Column("id", sa.Text()),
sa.Column("user_id", sa.Text()),
sa.Column("data", sa.JSON()), # JSON stored as text in SQLite + PG
)
results = connection.execute(
sa.select(
knowledge_table.c.id, knowledge_table.c.user_id, knowledge_table.c.data
)
).fetchall()
# 3. Insert members into group_member table
kf_table = sa.Table(
"knowledge_file",
sa.MetaData(),
sa.Column("id", sa.Text()),
sa.Column("user_id", sa.Text()),
sa.Column("knowledge_id", sa.Text()),
sa.Column("file_id", sa.Text()),
sa.Column("created_at", sa.BigInteger()),
sa.Column("updated_at", sa.BigInteger()),
)
file_table = sa.Table(
"file",
sa.MetaData(),
sa.Column("id", sa.Text()),
)
now = int(time.time())
for knowledge_id, user_id, data in results:
if not data:
continue
if isinstance(data, str):
try:
data = json.loads(data)
except Exception:
continue # skip invalid JSON
if not isinstance(data, dict):
continue
file_ids = data.get("file_ids", [])
for file_id in file_ids:
file_exists = connection.execute(
sa.select(file_table.c.id).where(file_table.c.id == file_id)
).fetchone()
if not file_exists:
continue # skip non-existing files
row = {
"id": str(uuid.uuid4()),
"user_id": user_id,
"knowledge_id": knowledge_id,
"file_id": file_id,
"created_at": now,
"updated_at": now,
}
connection.execute(kf_table.insert().values(**row))
with op.batch_alter_table("knowledge") as batch:
batch.drop_column("data")
def downgrade() -> None:
# 1. Add back the old data column
op.add_column("knowledge", sa.Column("data", sa.JSON(), nullable=True))
connection = op.get_bind()
# 2. Read knowledge_file entries and reconstruct data JSON
knowledge_table = sa.Table(
"knowledge",
sa.MetaData(),
sa.Column("id", sa.Text()),
sa.Column("data", sa.JSON()),
)
kf_table = sa.Table(
"knowledge_file",
sa.MetaData(),
sa.Column("id", sa.Text()),
sa.Column("knowledge_id", sa.Text()),
sa.Column("file_id", sa.Text()),
)
results = connection.execute(sa.select(knowledge_table.c.id)).fetchall()
for (knowledge_id,) in results:
file_ids = connection.execute(
sa.select(kf_table.c.file_id).where(kf_table.c.knowledge_id == knowledge_id)
).fetchall()
file_ids_list = [fid for (fid,) in file_ids]
data_json = {"file_ids": file_ids_list}
connection.execute(
knowledge_table.update()
.where(knowledge_table.c.id == knowledge_id)
.values(data=data_json)
)
# 3. Drop the knowledge_file table
op.drop_table("knowledge_file")
@@ -0,0 +1,53 @@
"""Add channel file table
Revision ID: 6283dc0e4d8d
Revises: 3e0e00844bb0
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
# revision identifiers, used by Alembic.
revision: str = "6283dc0e4d8d"
down_revision: Union[str, None] = "3e0e00844bb0"
branch_labels: Union[str, Sequence[str], None] = None
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
)
def downgrade() -> None:
op.drop_table("channel_file")
@@ -11,7 +11,6 @@ import sqlalchemy as sa
from sqlalchemy.sql import table, column, select
import json
revision = "6a39f3d8e55c"
down_revision = "c0fbf31ca0db"
branch_labels = None
@@ -0,0 +1,48 @@
"""Update channel file and knowledge table
Revision ID: 81cc2ce44d79
Revises: 6283dc0e4d8d
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
# revision identifiers, used by Alembic.
revision: str = "81cc2ce44d79"
down_revision: Union[str, None] = "6283dc0e4d8d"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# 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,
)
)
# 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))
def downgrade() -> None:
# Remove message_id column from channel_file table
with op.batch_alter_table("channel_file", schema=None) as batch_op:
batch_op.drop_column("message_id")
# Remove data column from knowledge table
with op.batch_alter_table("knowledge", schema=None) as batch_op:
batch_op.drop_column("data")
@@ -0,0 +1,182 @@
"""Add chat_message table
Revision ID: 8452d01d26d7
Revises: 374d2f66af06
Create Date: 2026-02-01 04:00:00.000000
"""
import time
import json
import logging
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
log = logging.getLogger(__name__)
revision: str = "8452d01d26d7"
down_revision: Union[str, None] = "374d2f66af06"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Step 1: Create table
op.create_table(
"chat_message",
sa.Column("id", sa.Text(), primary_key=True),
sa.Column("chat_id", sa.Text(), nullable=False, index=True),
sa.Column("user_id", sa.Text(), index=True),
sa.Column("role", sa.Text(), nullable=False),
sa.Column("parent_id", sa.Text(), nullable=True),
sa.Column("content", sa.JSON(), nullable=True),
sa.Column("output", sa.JSON(), nullable=True),
sa.Column("model_id", sa.Text(), nullable=True, index=True),
sa.Column("files", sa.JSON(), nullable=True),
sa.Column("sources", sa.JSON(), nullable=True),
sa.Column("embeds", sa.JSON(), nullable=True),
sa.Column("done", sa.Boolean(), default=True),
sa.Column("status_history", sa.JSON(), nullable=True),
sa.Column("error", sa.JSON(), nullable=True),
sa.Column("usage", sa.JSON(), nullable=True),
sa.Column("created_at", sa.BigInteger(), index=True),
sa.Column("updated_at", sa.BigInteger()),
sa.ForeignKeyConstraint(["chat_id"], ["chat.id"], ondelete="CASCADE"),
)
# Create composite indexes
op.create_index(
"chat_message_chat_parent_idx", "chat_message", ["chat_id", "parent_id"]
)
op.create_index(
"chat_message_model_created_idx", "chat_message", ["model_id", "created_at"]
)
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()),
sa.column("user_id", sa.Text()),
sa.column("chat", sa.JSON()),
)
chat_message_table = sa.table(
"chat_message",
sa.column("id", sa.Text()),
sa.column("chat_id", sa.Text()),
sa.column("user_id", sa.Text()),
sa.column("role", sa.Text()),
sa.column("parent_id", sa.Text()),
sa.column("content", sa.JSON()),
sa.column("output", sa.JSON()),
sa.column("model_id", sa.Text()),
sa.column("files", sa.JSON()),
sa.column("sources", sa.JSON()),
sa.column("embeds", sa.JSON()),
sa.column("done", sa.Boolean()),
sa.column("status_history", sa.JSON()),
sa.column("error", sa.JSON()),
sa.column("usage", sa.JSON()),
sa.column("created_at", sa.BigInteger()),
sa.column("updated_at", sa.BigInteger()),
)
# Fetch all chats (excluding shared chats which have user_id starting with 'shared-')
chats = conn.execute(
sa.select(chat_table.c.id, chat_table.c.user_id, chat_table.c.chat).where(
~chat_table.c.user_id.like("shared-%")
)
).fetchall()
now = int(time.time())
messages_inserted = 0
messages_failed = 0
for chat_row in chats:
chat_id = chat_row[0]
user_id = chat_row[1]
chat_data = chat_row[2]
if not chat_data:
continue
# Handle both string and dict chat data
if isinstance(chat_data, str):
try:
chat_data = json.loads(chat_data)
except Exception:
continue
history = chat_data.get("history", {})
messages = history.get("messages", {})
for message_id, message in messages.items():
if not isinstance(message, dict):
continue
role = message.get("role")
if not role:
continue
timestamp = message.get("timestamp", now)
try:
timestamp = int(float(timestamp))
except Exception as e:
timestamp = now
# Normalize timestamp: convert ms to seconds, validate range
if timestamp > 10_000_000_000:
timestamp = timestamp // 1000
# Must be after 2020 and not too far in the future
if timestamp < 1577836800 or timestamp > now + 86400:
timestamp = now
# Use savepoint to allow individual insert failures without aborting transaction
savepoint = conn.begin_nested()
try:
conn.execute(
sa.insert(chat_message_table).values(
id=f"{chat_id}-{message_id}",
chat_id=chat_id,
user_id=user_id,
role=role,
parent_id=message.get("parentId"),
content=message.get("content"),
output=message.get("output"),
model_id=message.get("model"),
files=message.get("files"),
sources=message.get("sources"),
embeds=message.get("embeds"),
done=message.get("done", True),
status_history=message.get("statusHistory"),
error=message.get("error"),
created_at=timestamp,
updated_at=timestamp,
)
)
savepoint.commit()
messages_inserted += 1
except Exception as e:
savepoint.rollback()
messages_failed += 1
log.warning(f"Failed to insert message {message_id}: {e}")
continue
log.info(
f"Backfilled {messages_inserted} messages into chat_message table ({messages_failed} failed)"
)
def downgrade() -> None:
op.drop_index("chat_message_user_created_idx", table_name="chat_message")
op.drop_index("chat_message_model_created_idx", table_name="chat_message")
op.drop_index("chat_message_chat_parent_idx", table_name="chat_message")
op.drop_table("chat_message")
@@ -0,0 +1,80 @@
"""Update channel and channel members table
Revision ID: 90ef40d4714e
Revises: b10670c03dd5
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
# revision identifiers, used by Alembic.
revision: str = "90ef40d4714e"
down_revision: Union[str, None] = "b10670c03dd5"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# 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))
# 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)
)
# 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
def downgrade() -> None:
# Downgrade 'channel' table
op.drop_column("channel", "is_private")
op.drop_column("channel", "archived_at")
op.drop_column("channel", "archived_by")
op.drop_column("channel", "deleted_at")
op.drop_column("channel", "deleted_by")
op.drop_column("channel", "updated_by")
# Downgrade 'channel_member' table
op.drop_column("channel_member", "role")
op.drop_column("channel_member", "invited_by")
op.drop_column("channel_member", "invited_at")
# Drop 'channel_webhook' table
op.drop_table("channel_webhook")
pass
@@ -0,0 +1,45 @@
"""Add skill table
Revision ID: a1b2c3d4e5f6
Revises: f1e2d3c4b5a6
Create Date: 2026-02-11 09:30:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from open_webui.migrations.util import get_existing_tables
revision: str = "a1b2c3d4e5f6"
down_revision: Union[str, None] = "f1e2d3c4b5a6"
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())
if "skill" not in existing_tables:
op.create_table(
"skill",
sa.Column("id", sa.String(), nullable=False, primary_key=True),
sa.Column("user_id", sa.String(), nullable=False),
sa.Column("name", sa.Text(), nullable=False, unique=True),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("content", sa.Text(), nullable=False),
sa.Column("meta", sa.JSON(), nullable=True),
sa.Column("is_active", sa.Boolean(), nullable=False),
sa.Column("updated_at", sa.BigInteger(), nullable=False),
sa.Column("created_at", sa.BigInteger(), nullable=False),
)
op.create_index("idx_skill_user_id", "skill", ["user_id"])
op.create_index("idx_skill_updated_at", "skill", ["updated_at"])
def downgrade() -> None:
op.drop_index("idx_skill_updated_at", table_name="skill")
op.drop_index("idx_skill_user_id", table_name="skill")
op.drop_table("skill")
@@ -0,0 +1,34 @@
"""Add reply_to_id column to message
Revision ID: a5c220713937
Revises: 38d63c18f30f
Create Date: 2025-09-27 02:24:18.058455
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "a5c220713937"
down_revision: Union[str, None] = "38d63c18f30f"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# 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
def downgrade() -> None:
# Remove 'reply_to_id' column from the 'message' table
op.drop_column("message", "reply_to_id")
pass
@@ -0,0 +1,249 @@
"""Update user table
Revision ID: b10670c03dd5
Revises: 2f1211949ecc
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
# revision identifiers, used by Alembic.
revision: str = "b10670c03dd5"
down_revision: Union[str, None] = "2f1211949ecc"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
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.
"""
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
idx_info = conn.execute(
sa.text(f"PRAGMA index_info('{index_name}')")
).fetchall()
indexed_cols = [row[2] for row in idx_info] # col names
if column_name in indexed_cols:
conn.execute(sa.text(f"DROP INDEX IF EXISTS {index_name}"))
def _convert_column_to_json(table: str, column: str):
conn = op.get_bind()
dialect = conn.dialect.name
# 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()
for row in rows:
uid, raw = row
if raw is None:
parsed = None
else:
try:
parsed = json.loads(raw)
except Exception:
parsed = None # fallback safe behavior
conn.execute(
sa.text(f'UPDATE "{table}" SET {column}_json = :val WHERE id = :id'),
{"val": json.dumps(parsed) if parsed else None, "id": uid},
)
# 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:
# PostgreSQL supports direct CAST
op.alter_column(
table,
column,
type_=sa.JSON(),
postgresql_using=f"{column}::json",
)
def _convert_column_to_text(table: str, column: str):
conn = op.get_bind()
dialect = conn.dialect.name
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()
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},
)
op.drop_column(table, column)
op.alter_column(table, f"{column}_text", new_column_name=column)
else:
op.alter_column(
table,
column,
type_=sa.Text(),
postgresql_using=f"to_json({column})::text",
)
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()
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
oauth_json = json.dumps({provider: {"sub": sub}})
conn.execute(
sa.text('UPDATE "user" SET oauth = :oauth WHERE id = :id'),
{"oauth": oauth_json, "id": uid},
)
users_with_keys = conn.execute(
sa.text('SELECT id, api_key FROM "user" WHERE api_key IS NOT NULL')
).fetchall()
now = int(time.time())
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,
},
)
if conn.dialect.name == "sqlite":
_drop_sqlite_indexes_for_column("user", "api_key", conn)
_drop_sqlite_indexes_for_column("user", "oauth_sub", conn)
with op.batch_alter_table("user") as batch_op:
batch_op.drop_column("api_key")
batch_op.drop_column("oauth_sub")
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()
for uid, oauth in users:
try:
data = 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},
)
op.drop_column("user", "oauth")
# --- 2. 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()
for uid, key in keys:
conn.execute(
sa.text('UPDATE "user" SET api_key = :key WHERE id = :id'),
{"key": key, "id": uid},
)
# Drop new table
op.drop_table("api_key")
with op.batch_alter_table("user") as batch_op:
batch_op.drop_column("profile_banner_image_url")
batch_op.drop_column("timezone")
batch_op.drop_column("presence_state")
batch_op.drop_column("status_emoji")
batch_op.drop_column("status_message")
batch_op.drop_column("status_expires_at")
# Convert info (JSON) → TEXT
_convert_column_to_text("user", "info")
# Convert settings (JSON) → TEXT
_convert_column_to_text("user", "settings")
@@ -0,0 +1,26 @@
"""add scim column to user table
Revision ID: b2c3d4e5f6a7
Revises: a1b2c3d4e5f6
Create Date: 2026-02-13 14:19:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "b2c3d4e5f6a7"
down_revision: Union[str, None] = "a1b2c3d4e5f6"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.add_column("user", sa.Column("scim", sa.JSON(), nullable=True))
def downgrade() -> None:
op.drop_column("user", "scim")
@@ -12,7 +12,6 @@ import json
from sqlalchemy.sql import table, column
from sqlalchemy import String, Text, JSON, and_
revision = "c29facfe716b"
down_revision = "c69f45358db4"
branch_labels = None
@@ -0,0 +1,56 @@
"""Add chat_file table
Revision ID: c440947495f3
Revises: 81cc2ce44d79
Create Date: 2025-12-21 20:27:41.694897
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "c440947495f3"
down_revision: Union[str, None] = "81cc2ce44d79"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table(
"chat_file",
sa.Column("id", sa.Text(), primary_key=True),
sa.Column("user_id", sa.Text(), nullable=False),
sa.Column(
"chat_id",
sa.Text(),
sa.ForeignKey("chat.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column(
"file_id",
sa.Text(),
sa.ForeignKey("file.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column("message_id", sa.Text(), nullable=True),
sa.Column("created_at", sa.BigInteger(), nullable=False),
sa.Column("updated_at", sa.BigInteger(), nullable=False),
# indexes
sa.Index("ix_chat_file_chat_id", "chat_id"),
sa.Index("ix_chat_file_file_id", "file_id"),
sa.Index("ix_chat_file_message_id", "message_id"),
sa.Index("ix_chat_file_user_id", "user_id"),
# unique constraints
sa.UniqueConstraint(
"chat_id", "file_id", name="uq_chat_file_chat_file"
), # prevent duplicate entries
)
pass
def downgrade() -> None:
op.drop_table("chat_file")
pass
@@ -0,0 +1,369 @@
"""Add access_grant table
Revision ID: f1e2d3c4b5a6
Revises: 8452d01d26d7
Create Date: 2026-02-05 10:00:00.000000
Migrates from JSON access_control columns to normalized access_grant table.
Access control semantics:
- NULL: Public access (all users can read) -> insert user:* for read
- {}: Private/owner-only (no grants) -> insert nothing
- {read: {...}, write: {...}}: Custom permissions -> insert specific grants
"""
from typing import Sequence, Union
import time
import uuid
from alembic import op
import sqlalchemy as sa
from open_webui.migrations.util import get_existing_tables
revision: str = "f1e2d3c4b5a6"
down_revision: Union[str, None] = "8452d01d26d7"
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())
# Create access_grant table
if "access_grant" not in existing_tables:
op.create_table(
"access_grant",
sa.Column("id", sa.Text(), nullable=False, primary_key=True),
sa.Column("resource_type", sa.Text(), nullable=False),
sa.Column("resource_id", sa.Text(), nullable=False),
sa.Column("principal_type", sa.Text(), nullable=False),
sa.Column("principal_id", sa.Text(), nullable=False),
sa.Column("permission", sa.Text(), nullable=False),
sa.Column("created_at", sa.BigInteger(), nullable=False),
sa.UniqueConstraint(
"resource_type",
"resource_id",
"principal_type",
"principal_id",
"permission",
name="uq_access_grant_grant",
),
)
op.create_index(
"idx_access_grant_resource",
"access_grant",
["resource_type", "resource_id"],
)
op.create_index(
"idx_access_grant_principal",
"access_grant",
["principal_type", "principal_id"],
)
# Backfill existing access_control JSON data
conn = op.get_bind()
# Tables with access_control JSON columns: (table_name, resource_type)
resource_tables = [
("knowledge", "knowledge"),
("prompt", "prompt"),
("tool", "tool"),
("model", "model"),
("note", "note"),
("channel", "channel"),
("file", "file"),
]
now = int(time.time())
inserted = set()
for table_name, resource_type in resource_tables:
if table_name not in existing_tables:
continue
# Query all rows
try:
result = conn.execute(
sa.text(f'SELECT id, access_control FROM "{table_name}"')
)
rows = result.fetchall()
except Exception:
continue
for row in rows:
resource_id = row[0]
access_control_json = row[1]
# Handle NULL or JSON "null" = public access (user:* for read)
# Could be Python None (SQL NULL) or string "null" (JSON null)
# EXCEPTION: files with NULL are PRIVATE (owner-only), not public
is_null = (
access_control_json is None
or access_control_json == "null"
or (
isinstance(access_control_json, str)
and access_control_json.strip().lower() == "null"
)
)
if is_null:
# Files: NULL = private (no entry needed, owner has implicit access)
# Other resources: NULL = public (insert user:* for read)
if resource_type == "file":
continue # Private - no entry needed
key = (resource_type, resource_id, "user", "*", "read")
if key not in inserted:
try:
conn.execute(
sa.text("""
INSERT INTO access_grant (id, resource_type, resource_id, principal_type, principal_id, permission, created_at)
VALUES (:id, :resource_type, :resource_id, :principal_type, :principal_id, :permission, :created_at)
"""),
{
"id": str(uuid.uuid4()),
"resource_type": resource_type,
"resource_id": resource_id,
"principal_type": "user",
"principal_id": "*",
"permission": "read",
"created_at": now,
},
)
inserted.add(key)
except Exception:
pass
continue
# Handle JSON parsing
if isinstance(access_control_json, str):
import json
try:
access_control_json = json.loads(access_control_json)
except Exception:
continue
# Handle {} = private/owner-only - NO entries needed
# Owner access is implicit, no grants to store
if not access_control_json or not isinstance(access_control_json, dict):
continue
# Check if it's effectively empty (no read/write keys with content)
read_data = access_control_json.get("read", {})
write_data = access_control_json.get("write", {})
has_read_grants = read_data.get("group_ids", []) or read_data.get(
"user_ids", []
)
has_write_grants = write_data.get("group_ids", []) or write_data.get(
"user_ids", []
)
if not has_read_grants and not has_write_grants:
# Empty permissions = private, no grants needed
continue
# Extract permissions and insert into access_grant table
for permission in ["read", "write"]:
perm_data = access_control_json.get(permission, {})
if not perm_data:
continue
for group_id in perm_data.get("group_ids", []):
key = (resource_type, resource_id, "group", group_id, permission)
if key in inserted:
continue
try:
conn.execute(
sa.text("""
INSERT INTO access_grant (id, resource_type, resource_id, principal_type, principal_id, permission, created_at)
VALUES (:id, :resource_type, :resource_id, :principal_type, :principal_id, :permission, :created_at)
"""),
{
"id": str(uuid.uuid4()),
"resource_type": resource_type,
"resource_id": resource_id,
"principal_type": "group",
"principal_id": group_id,
"permission": permission,
"created_at": now,
},
)
inserted.add(key)
except Exception:
pass
for user_id in perm_data.get("user_ids", []):
key = (resource_type, resource_id, "user", user_id, permission)
if key in inserted:
continue
try:
conn.execute(
sa.text("""
INSERT INTO access_grant (id, resource_type, resource_id, principal_type, principal_id, permission, created_at)
VALUES (:id, :resource_type, :resource_id, :principal_type, :principal_id, :permission, :created_at)
"""),
{
"id": str(uuid.uuid4()),
"resource_type": resource_type,
"resource_id": resource_id,
"principal_type": "user",
"principal_id": user_id,
"permission": permission,
"created_at": now,
},
)
inserted.add(key)
except Exception:
pass
# Drop access_control columns from resource tables
for table_name, _ in resource_tables:
if table_name not in existing_tables:
continue
try:
with op.batch_alter_table(table_name) as batch:
batch.drop_column("access_control")
except Exception:
pass
def downgrade() -> None:
import json
conn = op.get_bind()
# Resource tables mapping: (table_name, resource_type)
resource_tables = [
("knowledge", "knowledge"),
("prompt", "prompt"),
("tool", "tool"),
("model", "model"),
("note", "note"),
("channel", "channel"),
("file", "file"),
]
# Step 1: Re-add access_control columns to resource tables
for table_name, _ in resource_tables:
try:
with op.batch_alter_table(table_name) as batch:
batch.add_column(sa.Column("access_control", sa.JSON(), nullable=True))
except Exception:
pass
# Step 2: Query access_grant table and reconstruct JSON for each resource
for table_name, resource_type in resource_tables:
try:
# Get all grants for this resource type
result = conn.execute(
sa.text("""
SELECT resource_id, principal_type, principal_id, permission
FROM access_grant
WHERE resource_type = :resource_type
"""),
{"resource_type": resource_type},
)
rows = result.fetchall()
except Exception:
continue
# Group by resource_id and reconstruct JSON structure
resource_grants = {}
for row in rows:
resource_id = row[0]
principal_type = row[1]
principal_id = row[2]
permission = row[3]
if resource_id not in resource_grants:
resource_grants[resource_id] = {
"is_public": False,
"read": {"group_ids": [], "user_ids": []},
"write": {"group_ids": [], "user_ids": []},
}
# Handle public access (user:* for read)
if (
principal_type == "user"
and principal_id == "*"
and permission == "read"
):
resource_grants[resource_id]["is_public"] = True
continue
# Add to appropriate list
if permission in ["read", "write"]:
if principal_type == "group":
if (
principal_id
not in resource_grants[resource_id][permission]["group_ids"]
):
resource_grants[resource_id][permission]["group_ids"].append(
principal_id
)
elif principal_type == "user":
if (
principal_id
not in resource_grants[resource_id][permission]["user_ids"]
):
resource_grants[resource_id][permission]["user_ids"].append(
principal_id
)
# Step 3: Update each resource with reconstructed JSON
for resource_id, grants in resource_grants.items():
if grants["is_public"]:
# Public = NULL
access_control_value = None
elif (
not grants["read"]["group_ids"]
and not grants["read"]["user_ids"]
and not grants["write"]["group_ids"]
and not grants["write"]["user_ids"]
):
# No grants = should not happen (would mean no entries), default to {}
access_control_value = json.dumps({})
else:
# Custom permissions
access_control_value = json.dumps(
{
"read": grants["read"],
"write": grants["write"],
}
)
try:
conn.execute(
sa.text(
f'UPDATE "{table_name}" SET access_control = :access_control WHERE id = :id'
),
{"access_control": access_control_value, "id": resource_id},
)
except Exception:
pass
# Step 4: Set all resources WITHOUT entries to private
# For files: NULL means private (owner-only), so leave as NULL
# For other resources: {} means private, so update to {}
if resource_type != "file":
try:
conn.execute(
sa.text(f"""
UPDATE "{table_name}"
SET access_control = :private_value
WHERE id NOT IN (
SELECT DISTINCT resource_id FROM access_grant WHERE resource_type = :resource_type
)
AND access_control IS NULL
"""),
{"private_value": json.dumps({}), "resource_type": resource_type},
)
except Exception:
pass
# For files, NULL stays NULL - no action needed
# Step 5: Drop the access_grant table
op.drop_index("idx_access_grant_principal", table_name="access_grant")
op.drop_index("idx_access_grant_resource", table_name="access_grant")
op.drop_table("access_grant")
+900
View File
@@ -0,0 +1,900 @@
import logging
import time
import uuid
from typing import Optional
from sqlalchemy.orm import Session
from open_webui.internal.db import Base, get_db_context
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Column, Text, UniqueConstraint, or_, and_
from sqlalchemy.dialects.postgresql import JSONB
log = logging.getLogger(__name__)
####################
# AccessGrant DB Schema
####################
class AccessGrant(Base):
__tablename__ = "access_grant"
id = Column(Text, primary_key=True)
resource_type = Column(
Text, nullable=False
) # "knowledge", "model", "prompt", "tool", "note", "channel", "file"
resource_id = Column(Text, nullable=False)
principal_type = Column(Text, nullable=False) # "user" or "group"
principal_id = Column(
Text, nullable=False
) # user_id, group_id, or "*" (wildcard for public)
permission = Column(Text, nullable=False) # "read" or "write"
created_at = Column(BigInteger, nullable=False)
__table_args__ = (
UniqueConstraint(
"resource_type",
"resource_id",
"principal_type",
"principal_id",
"permission",
name="uq_access_grant_grant",
),
)
class AccessGrantModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: str
resource_type: str
resource_id: str
principal_type: str
principal_id: str
permission: str
created_at: int
class AccessGrantResponse(BaseModel):
"""Slim grant model for API responses — resource context is implicit from the parent."""
id: str
principal_type: str
principal_id: str
permission: str
@classmethod
def from_grant(cls, grant: "AccessGrantModel") -> "AccessGrantResponse":
return cls(
id=grant.id,
principal_type=grant.principal_type,
principal_id=grant.principal_id,
permission=grant.permission,
)
####################
# Conversion utilities
####################
def access_control_to_grants(
resource_type: str,
resource_id: str,
access_control: Optional[dict],
) -> list[dict]:
"""
Convert an old-style access_control JSON dict to a flat list of grant dicts.
Semantics:
- None → public read (user:* read) — except files which are private
- {} → private/owner-only (no grants)
- {read: {group_ids, user_ids}, write: {group_ids, user_ids}} → specific grants
Returns a list of dicts with keys: resource_type, resource_id, principal_type, principal_id, permission
"""
grants = []
if access_control is None:
# NULL → public read (user:* for read)
# Exception: files with NULL are private (owner-only), no grants needed
if resource_type != "file":
grants.append(
{
"resource_type": resource_type,
"resource_id": resource_id,
"principal_type": "user",
"principal_id": "*",
"permission": "read",
}
)
return grants
# {} → private/owner-only, no grants
if not access_control:
return grants
# Parse structured permissions
for permission in ["read", "write"]:
perm_data = access_control.get(permission, {})
if not perm_data:
continue
for group_id in perm_data.get("group_ids", []):
grants.append(
{
"resource_type": resource_type,
"resource_id": resource_id,
"principal_type": "group",
"principal_id": group_id,
"permission": permission,
}
)
for user_id in perm_data.get("user_ids", []):
grants.append(
{
"resource_type": resource_type,
"resource_id": resource_id,
"principal_type": "user",
"principal_id": user_id,
"permission": permission,
}
)
return grants
def normalize_access_grants(access_grants: Optional[list]) -> list[dict]:
"""
Normalize direct access_grants payloads from API forms.
Keeps only valid grants and removes duplicates by
(principal_type, principal_id, permission).
"""
if not access_grants:
return []
deduped = {}
for grant in access_grants:
if isinstance(grant, BaseModel):
grant = grant.model_dump()
if not isinstance(grant, dict):
continue
principal_type = grant.get("principal_type")
principal_id = grant.get("principal_id")
permission = grant.get("permission")
if principal_type not in ("user", "group"):
continue
if permission not in ("read", "write"):
continue
if not isinstance(principal_id, str) or not principal_id:
continue
key = (principal_type, principal_id, permission)
deduped[key] = {
"id": (
grant.get("id")
if isinstance(grant.get("id"), str) and grant.get("id")
else str(uuid.uuid4())
),
"principal_type": principal_type,
"principal_id": principal_id,
"permission": permission,
}
return list(deduped.values())
def has_public_read_access_grant(access_grants: Optional[list]) -> bool:
"""
Returns True when a direct grant list includes wildcard public-read.
"""
for grant in normalize_access_grants(access_grants):
if (
grant["principal_type"] == "user"
and grant["principal_id"] == "*"
and grant["permission"] == "read"
):
return True
return False
def has_user_access_grant(access_grants: Optional[list]) -> bool:
"""
Returns True when a direct grant list includes any non-wildcard user grant.
"""
for grant in normalize_access_grants(access_grants):
if grant["principal_type"] == "user" and grant["principal_id"] != "*":
return True
return False
def strip_user_access_grants(access_grants: Optional[list]) -> list:
"""
Remove all non-wildcard user grants from the list.
Keeps group grants and the public wildcard (user:*) intact.
"""
if not access_grants:
return []
return [
grant
for grant in access_grants
if not (
(
grant.get("principal_type")
if isinstance(grant, dict)
else getattr(grant, "principal_type", None)
)
== "user"
and (
grant.get("principal_id")
if isinstance(grant, dict)
else getattr(grant, "principal_id", None)
)
!= "*"
)
]
def grants_to_access_control(grants: list) -> Optional[dict]:
"""
Convert a list of grant objects (AccessGrantModel or AccessGrantResponse)
back to the old-style access_control JSON dict for backward compatibility.
Semantics:
- [] (empty) → {} (private/owner-only)
- Contains user:*:read → None (public), but write grants are preserved
- Otherwise → {read: {group_ids, user_ids}, write: {group_ids, user_ids}}
Note: "public" (user:*:read) still allows additional write permissions
to coexist. When the wildcard read is present the function returns None
for the legacy dict, so callers that need write info should inspect the
grants list directly.
"""
if not grants:
return {} # No grants = private/owner-only
result = {
"read": {"group_ids": [], "user_ids": []},
"write": {"group_ids": [], "user_ids": []},
}
is_public = False
for grant in grants:
if (
grant.principal_type == "user"
and grant.principal_id == "*"
and grant.permission == "read"
):
is_public = True
continue # Don't add wildcard to user_ids list
if grant.permission not in ("read", "write"):
continue
if grant.principal_type == "group":
if grant.principal_id not in result[grant.permission]["group_ids"]:
result[grant.permission]["group_ids"].append(grant.principal_id)
elif grant.principal_type == "user":
if grant.principal_id not in result[grant.permission]["user_ids"]:
result[grant.permission]["user_ids"].append(grant.principal_id)
if is_public:
return None # Public read access
return result
####################
# Table Operations
####################
class AccessGrantsTable:
def grant_access(
self,
resource_type: str,
resource_id: str,
principal_type: str,
principal_id: str,
permission: str,
db: Optional[Session] = None,
) -> Optional[AccessGrantModel]:
"""Add a single access grant. Idempotent (ignores duplicates)."""
with get_db_context(db) as db:
# Check for existing grant
existing = (
db.query(AccessGrant)
.filter_by(
resource_type=resource_type,
resource_id=resource_id,
principal_type=principal_type,
principal_id=principal_id,
permission=permission,
)
.first()
)
if existing:
return AccessGrantModel.model_validate(existing)
grant = AccessGrant(
id=str(uuid.uuid4()),
resource_type=resource_type,
resource_id=resource_id,
principal_type=principal_type,
principal_id=principal_id,
permission=permission,
created_at=int(time.time()),
)
db.add(grant)
db.commit()
db.refresh(grant)
return AccessGrantModel.model_validate(grant)
def revoke_access(
self,
resource_type: str,
resource_id: str,
principal_type: str,
principal_id: str,
permission: str,
db: Optional[Session] = None,
) -> bool:
"""Remove a single access grant."""
with get_db_context(db) as db:
deleted = (
db.query(AccessGrant)
.filter_by(
resource_type=resource_type,
resource_id=resource_id,
principal_type=principal_type,
principal_id=principal_id,
permission=permission,
)
.delete()
)
db.commit()
return deleted > 0
def revoke_all_access(
self,
resource_type: str,
resource_id: str,
db: Optional[Session] = None,
) -> int:
"""Remove all access grants for a resource."""
with get_db_context(db) as db:
deleted = (
db.query(AccessGrant)
.filter_by(
resource_type=resource_type,
resource_id=resource_id,
)
.delete()
)
db.commit()
return deleted
def set_access_control(
self,
resource_type: str,
resource_id: str,
access_control: Optional[dict],
db: Optional[Session] = None,
) -> list[AccessGrantModel]:
"""
Replace all grants for a resource from an access_control JSON dict.
This is the primary bridge for backward compat with the frontend.
"""
with get_db_context(db) as db:
# Delete all existing grants for this resource
db.query(AccessGrant).filter_by(
resource_type=resource_type,
resource_id=resource_id,
).delete()
# Convert JSON to grant dicts
grant_dicts = access_control_to_grants(
resource_type, resource_id, access_control
)
# Insert new grants
results = []
for grant_dict in grant_dicts:
grant = AccessGrant(
id=str(uuid.uuid4()),
**grant_dict,
created_at=int(time.time()),
)
db.add(grant)
results.append(grant)
db.commit()
return [AccessGrantModel.model_validate(g) for g in results]
def set_access_grants(
self,
resource_type: str,
resource_id: str,
access_grants: Optional[list],
db: Optional[Session] = None,
) -> list[AccessGrantModel]:
"""
Replace all grants for a resource from a direct access_grants list.
"""
with get_db_context(db) as db:
db.query(AccessGrant).filter_by(
resource_type=resource_type,
resource_id=resource_id,
).delete()
normalized_grants = normalize_access_grants(access_grants)
results = []
for grant_dict in normalized_grants:
grant = AccessGrant(
id=str(uuid.uuid4()),
resource_type=resource_type,
resource_id=resource_id,
principal_type=grant_dict["principal_type"],
principal_id=grant_dict["principal_id"],
permission=grant_dict["permission"],
created_at=int(time.time()),
)
db.add(grant)
results.append(grant)
db.commit()
return [AccessGrantModel.model_validate(g) for g in results]
def get_access_control(
self,
resource_type: str,
resource_id: str,
db: Optional[Session] = None,
) -> Optional[dict]:
"""
Reconstruct the old-style access_control JSON dict from grants.
For backward compat with the frontend.
"""
with get_db_context(db) as db:
grants = (
db.query(AccessGrant)
.filter_by(
resource_type=resource_type,
resource_id=resource_id,
)
.all()
)
grant_models = [AccessGrantModel.model_validate(g) for g in grants]
return grants_to_access_control(grant_models)
def get_grants_by_resource(
self,
resource_type: str,
resource_id: str,
db: Optional[Session] = None,
) -> list[AccessGrantModel]:
"""Get all grants for a specific resource."""
with get_db_context(db) as db:
grants = (
db.query(AccessGrant)
.filter_by(
resource_type=resource_type,
resource_id=resource_id,
)
.all()
)
return [AccessGrantModel.model_validate(g) for g in grants]
def get_grants_by_resources(
self,
resource_type: str,
resource_ids: list[str],
db: Optional[Session] = None,
) -> dict[str, list[AccessGrantModel]]:
"""Batch-fetch grants for multiple resources. Returns {resource_id: [grants]}."""
if not resource_ids:
return {}
with get_db_context(db) as db:
grants = (
db.query(AccessGrant)
.filter(
AccessGrant.resource_type == resource_type,
AccessGrant.resource_id.in_(resource_ids),
)
.all()
)
result: dict[str, list[AccessGrantModel]] = {
rid: [] for rid in resource_ids
}
for g in grants:
result[g.resource_id].append(AccessGrantModel.model_validate(g))
return result
def has_access(
self,
user_id: str,
resource_type: str,
resource_id: str,
permission: str = "read",
user_group_ids: Optional[set[str]] = None,
db: Optional[Session] = None,
) -> bool:
"""
Check if a user has the specified permission on a resource.
Access is granted if any of the following is true:
- There's a grant for user:* (public) with the requested permission
- There's a grant for the specific user with the requested permission
- There's a grant for any of the user's groups with the requested permission
"""
with get_db_context(db) as db:
# Build conditions for matching grants
conditions = [
# Public access
and_(
AccessGrant.principal_type == "user",
AccessGrant.principal_id == "*",
),
# Direct user access
and_(
AccessGrant.principal_type == "user",
AccessGrant.principal_id == user_id,
),
]
# Group access
if user_group_ids is None:
from open_webui.models.groups import Groups
user_groups = Groups.get_groups_by_member_id(user_id, db=db)
user_group_ids = {group.id for group in user_groups}
if user_group_ids:
conditions.append(
and_(
AccessGrant.principal_type == "group",
AccessGrant.principal_id.in_(user_group_ids),
)
)
exists = (
db.query(AccessGrant)
.filter(
AccessGrant.resource_type == resource_type,
AccessGrant.resource_id == resource_id,
AccessGrant.permission == permission,
or_(*conditions),
)
.first()
)
return exists is not None
def get_accessible_resource_ids(
self,
user_id: str,
resource_type: str,
resource_ids: list[str],
permission: str = "read",
user_group_ids: Optional[set[str]] = None,
db: Optional[Session] = None,
) -> set[str]:
"""
Batch check: return the subset of resource_ids that the user can access.
This replaces calling has_access() in a loop (N+1) with a single query.
"""
if not resource_ids:
return set()
with get_db_context(db) as db:
conditions = [
and_(
AccessGrant.principal_type == "user",
AccessGrant.principal_id == "*",
),
and_(
AccessGrant.principal_type == "user",
AccessGrant.principal_id == user_id,
),
]
if user_group_ids is None:
from open_webui.models.groups import Groups
user_groups = Groups.get_groups_by_member_id(user_id, db=db)
user_group_ids = {group.id for group in user_groups}
if user_group_ids:
conditions.append(
and_(
AccessGrant.principal_type == "group",
AccessGrant.principal_id.in_(user_group_ids),
)
)
rows = (
db.query(AccessGrant.resource_id)
.filter(
AccessGrant.resource_type == resource_type,
AccessGrant.resource_id.in_(resource_ids),
AccessGrant.permission == permission,
or_(*conditions),
)
.distinct()
.all()
)
return {row[0] for row in rows}
def get_users_with_access(
self,
resource_type: str,
resource_id: str,
permission: str = "read",
db: Optional[Session] = None,
) -> list:
"""
Get all users who have the specified permission on a resource.
Returns a list of UserModel instances.
"""
from open_webui.models.users import Users, UserModel
from open_webui.models.groups import Groups
with get_db_context(db) as db:
grants = (
db.query(AccessGrant)
.filter_by(
resource_type=resource_type,
resource_id=resource_id,
permission=permission,
)
.all()
)
# Check for public access
for grant in grants:
if grant.principal_type == "user" and grant.principal_id == "*":
result = Users.get_users(filter={"roles": ["!pending"]}, db=db)
return result.get("users", [])
user_ids_with_access = set()
for grant in grants:
if grant.principal_type == "user":
user_ids_with_access.add(grant.principal_id)
elif grant.principal_type == "group":
group_user_ids = Groups.get_group_user_ids_by_id(
grant.principal_id, db=db
)
if group_user_ids:
user_ids_with_access.update(group_user_ids)
if not user_ids_with_access:
return []
return Users.get_users_by_user_ids(list(user_ids_with_access), db=db)
def has_permission_filter(
self,
db,
query,
DocumentModel,
filter: dict,
resource_type: str,
permission: str = "read",
):
"""
Apply access control filtering to a SQLAlchemy query by JOINing with access_grant.
This replaces the old JSON-column-based filtering with a proper relational JOIN.
"""
group_ids = filter.get("group_ids", [])
user_id = filter.get("user_id")
if permission == "read_only":
return self._has_read_only_permission_filter(
db, query, DocumentModel, filter, resource_type
)
# Build principal conditions
principal_conditions = []
if group_ids or user_id:
# Public access: user:* read
principal_conditions.append(
and_(
AccessGrant.principal_type == "user",
AccessGrant.principal_id == "*",
)
)
if user_id:
# Owner always has access
principal_conditions.append(DocumentModel.user_id == user_id)
# Direct user grant
principal_conditions.append(
and_(
AccessGrant.principal_type == "user",
AccessGrant.principal_id == user_id,
)
)
if group_ids:
# Group grants
principal_conditions.append(
and_(
AccessGrant.principal_type == "group",
AccessGrant.principal_id.in_(group_ids),
)
)
if not principal_conditions:
return query
# LEFT JOIN access_grant and filter
# We use a subquery approach to avoid duplicates from multiple matching grants
from sqlalchemy import exists as sa_exists, select
grant_exists = (
select(AccessGrant.id)
.where(
AccessGrant.resource_type == resource_type,
AccessGrant.resource_id == DocumentModel.id,
AccessGrant.permission == permission,
or_(
and_(
AccessGrant.principal_type == "user",
AccessGrant.principal_id == "*",
),
*(
[
and_(
AccessGrant.principal_type == "user",
AccessGrant.principal_id == user_id,
)
]
if user_id
else []
),
*(
[
and_(
AccessGrant.principal_type == "group",
AccessGrant.principal_id.in_(group_ids),
)
]
if group_ids
else []
),
),
)
.correlate(DocumentModel)
.exists()
)
# Owner OR has a matching grant
owner_or_grant = [grant_exists]
if user_id:
owner_or_grant.append(DocumentModel.user_id == user_id)
query = query.filter(or_(*owner_or_grant))
return query
def _has_read_only_permission_filter(
self,
db,
query,
DocumentModel,
filter: dict,
resource_type: str,
):
"""
Filter for items where user has read BUT NOT write access.
Public items are NOT considered read_only.
"""
group_ids = filter.get("group_ids", [])
user_id = filter.get("user_id")
from sqlalchemy import exists as sa_exists, select
# Has read grant (not public)
read_grant_exists = (
select(AccessGrant.id)
.where(
AccessGrant.resource_type == resource_type,
AccessGrant.resource_id == DocumentModel.id,
AccessGrant.permission == "read",
or_(
*(
[
and_(
AccessGrant.principal_type == "user",
AccessGrant.principal_id == user_id,
)
]
if user_id
else []
),
*(
[
and_(
AccessGrant.principal_type == "group",
AccessGrant.principal_id.in_(group_ids),
)
]
if group_ids
else []
),
),
)
.correlate(DocumentModel)
.exists()
)
# Does NOT have write grant
write_grant_exists = (
select(AccessGrant.id)
.where(
AccessGrant.resource_type == resource_type,
AccessGrant.resource_id == DocumentModel.id,
AccessGrant.permission == "write",
or_(
*(
[
and_(
AccessGrant.principal_type == "user",
AccessGrant.principal_id == user_id,
)
]
if user_id
else []
),
*(
[
and_(
AccessGrant.principal_type == "group",
AccessGrant.principal_id.in_(group_ids),
)
]
if group_ids
else []
),
),
)
.correlate(DocumentModel)
.exists()
)
# Is NOT public
public_grant_exists = (
select(AccessGrant.id)
.where(
AccessGrant.resource_type == resource_type,
AccessGrant.resource_id == DocumentModel.id,
AccessGrant.permission == "read",
AccessGrant.principal_type == "user",
AccessGrant.principal_id == "*",
)
.correlate(DocumentModel)
.exists()
)
conditions = [read_grant_exists, ~write_grant_exists, ~public_grant_exists]
# Not owner
if user_id:
conditions.append(DocumentModel.user_id != user_id)
query = query.filter(and_(*conditions))
return query
AccessGrants = AccessGrantsTable()
+59 -40
View File
@@ -2,15 +2,14 @@ import logging
import uuid
from typing import Optional
from open_webui.internal.db import Base, get_db
from open_webui.models.users import UserModel, Users
from open_webui.env import SRC_LOG_LEVELS
from pydantic import BaseModel
from sqlalchemy.orm import Session
from open_webui.internal.db import Base, JSONField, get_db, get_db_context
from open_webui.models.users import User, UserModel, UserProfileImageResponse, Users
from open_webui.utils.validate import validate_profile_image_url
from pydantic import BaseModel, field_validator
from sqlalchemy import Boolean, Column, String, Text
from open_webui.utils.auth import verify_password
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
####################
# DB MODEL
@@ -20,7 +19,7 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
class Auth(Base):
__tablename__ = "auth"
id = Column(String, primary_key=True)
id = Column(String, primary_key=True, unique=True)
email = Column(String)
password = Column(Text)
active = Column(Boolean)
@@ -47,15 +46,7 @@ class ApiKey(BaseModel):
api_key: Optional[str] = None
class UserResponse(BaseModel):
id: str
email: str
name: str
role: str
profile_image_url: str
class SigninResponse(Token, UserResponse):
class SigninResponse(Token, UserProfileImageResponse):
pass
@@ -84,6 +75,13 @@ class SignupForm(BaseModel):
password: str
profile_image_url: Optional[str] = "/user.png"
@field_validator("profile_image_url")
@classmethod
def check_profile_image_url(cls, v: Optional[str]) -> Optional[str]:
if v is not None:
return validate_profile_image_url(v)
return v
class AddUserForm(SignupForm):
role: Optional[str] = "pending"
@@ -97,9 +95,10 @@ class AuthsTable:
name: str,
profile_image_url: str = "/user.png",
role: str = "pending",
oauth_sub: Optional[str] = None,
oauth: Optional[dict] = None,
db: Optional[Session] = None,
) -> Optional[UserModel]:
with get_db() as db:
with get_db_context(db) as db:
log.info("insert_new_auth")
id = str(uuid.uuid4())
@@ -111,7 +110,7 @@ class AuthsTable:
db.add(result)
user = Users.insert_new_user(
id, name, email, profile_image_url, role, oauth_sub
id, name, email, profile_image_url, role, oauth=oauth, db=db
)
db.commit()
@@ -122,18 +121,20 @@ class AuthsTable:
else:
return None
def authenticate_user(self, email: str, password: str) -> Optional[UserModel]:
def authenticate_user(
self, email: str, verify_password: callable, db: Optional[Session] = None
) -> Optional[UserModel]:
log.info(f"authenticate_user: {email}")
user = Users.get_user_by_email(email)
user = Users.get_user_by_email(email, db=db)
if not user:
return None
try:
with get_db() as db:
with get_db_context(db) as db:
auth = db.query(Auth).filter_by(id=user.id, active=True).first()
if auth:
if verify_password(password, auth.password):
if verify_password(auth.password):
return user
else:
return None
@@ -142,32 +143,45 @@ class AuthsTable:
except Exception:
return None
def authenticate_user_by_api_key(self, api_key: str) -> Optional[UserModel]:
log.info(f"authenticate_user_by_api_key: {api_key}")
def authenticate_user_by_api_key(
self, api_key: str, db: Optional[Session] = None
) -> Optional[UserModel]:
log.info(f"authenticate_user_by_api_key")
# if no api_key, return None
if not api_key:
return None
try:
user = Users.get_user_by_api_key(api_key)
user = Users.get_user_by_api_key(api_key, db=db)
return user if user else None
except Exception:
return False
def authenticate_user_by_email(self, email: str) -> Optional[UserModel]:
def authenticate_user_by_email(
self, email: str, db: Optional[Session] = None
) -> Optional[UserModel]:
log.info(f"authenticate_user_by_email: {email}")
try:
with get_db() as db:
auth = db.query(Auth).filter_by(email=email, active=True).first()
if auth:
user = Users.get_user_by_id(auth.id)
return user
with get_db_context(db) as db:
# Single JOIN query instead of two separate queries
result = (
db.query(Auth, User)
.join(User, Auth.id == User.id)
.filter(Auth.email == email, Auth.active == True)
.first()
)
if result:
_, user = result
return UserModel.model_validate(user)
return None
except Exception:
return None
def update_user_password_by_id(self, id: str, new_password: str) -> bool:
def update_user_password_by_id(
self, id: str, new_password: str, db: Optional[Session] = None
) -> bool:
try:
with get_db() as db:
with get_db_context(db) as db:
result = (
db.query(Auth).filter_by(id=id).update({"password": new_password})
)
@@ -176,20 +190,25 @@ class AuthsTable:
except Exception:
return False
def update_email_by_id(self, id: str, email: str) -> bool:
def update_email_by_id(
self, id: str, email: str, db: Optional[Session] = None
) -> bool:
try:
with get_db() as db:
with get_db_context(db) as db:
result = db.query(Auth).filter_by(id=id).update({"email": email})
db.commit()
return True if result == 1 else False
if result == 1:
Users.update_user_by_id(id, {"email": email}, db=db)
return True
return False
except Exception:
return False
def delete_auth_by_id(self, id: str) -> bool:
def delete_auth_by_id(self, id: str, db: Optional[Session] = None) -> bool:
try:
with get_db() as db:
with get_db_context(db) as db:
# Delete User
result = Users.delete_user_by_id(id)
result = Users.delete_user_by_id(id, db=db)
if result:
db.query(Auth).filter_by(id=id).delete()
File diff suppressed because it is too large Load Diff
+644
View File
@@ -0,0 +1,644 @@
import json
import time
import uuid
from typing import Any, Optional
from sqlalchemy.orm import Session
from open_webui.internal.db import Base, get_db_context
from pydantic import BaseModel, ConfigDict
from sqlalchemy import (
BigInteger,
Boolean,
Column,
ForeignKey,
Text,
JSON,
Index,
func,
)
####################
# Helpers
####################
def _normalize_timestamp(timestamp: int) -> float:
"""Normalize and validate timestamp. Returns current time if invalid."""
now = time.time()
# Convert milliseconds to seconds if needed
if timestamp > 10_000_000_000:
timestamp = timestamp / 1000
# Validate: must be after 2020 and not in the future (with 1 day tolerance)
min_valid = 1577836800 # 2020-01-01 00:00:00 UTC
max_valid = now + 86400 # 1 day in the future (clock skew tolerance)
if timestamp < min_valid or timestamp > max_valid:
return now
return timestamp
####################
# ChatMessage DB Schema
####################
class ChatMessage(Base):
__tablename__ = "chat_message"
# Identity
id = Column(Text, primary_key=True)
chat_id = Column(
Text, ForeignKey("chat.id", ondelete="CASCADE"), nullable=False, index=True
)
user_id = Column(Text, index=True)
# Structure
role = Column(Text, nullable=False) # user, assistant, system
parent_id = Column(Text, nullable=True)
# Content
content = Column(JSON, nullable=True) # Can be str or list of blocks
output = Column(JSON, nullable=True)
# Model (for assistant messages)
model_id = Column(Text, nullable=True, index=True)
# Attachments
files = Column(JSON, nullable=True)
sources = Column(JSON, nullable=True)
embeds = Column(JSON, nullable=True)
# Status
done = Column(Boolean, default=True)
status_history = Column(JSON, nullable=True)
error = Column(JSON, nullable=True)
# Usage (tokens, timing, etc.)
usage = Column(JSON, nullable=True)
# Timestamps
created_at = Column(BigInteger, index=True)
updated_at = Column(BigInteger)
__table_args__ = (
Index("chat_message_chat_parent_idx", "chat_id", "parent_id"),
Index("chat_message_model_created_idx", "model_id", "created_at"),
Index("chat_message_user_created_idx", "user_id", "created_at"),
)
####################
# Pydantic Models
####################
class ChatMessageModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: str
chat_id: str
user_id: str
role: str
parent_id: Optional[str] = None
content: Optional[Any] = None # str or list of blocks
output: Optional[list] = None
model_id: Optional[str] = None
files: Optional[list] = None
sources: Optional[list] = None
embeds: Optional[list] = None
done: bool = True
status_history: Optional[list] = None
error: Optional[dict | str] = None
usage: Optional[dict] = None
created_at: int
updated_at: int
####################
# Table Operations
####################
class ChatMessageTable:
def upsert_message(
self,
message_id: str,
chat_id: str,
user_id: str,
data: dict,
db: Optional[Session] = None,
) -> Optional[ChatMessageModel]:
"""Insert or update a chat message."""
with get_db_context(db) as db:
now = int(time.time())
timestamp = data.get("timestamp", now)
# Use composite ID: {chat_id}-{message_id}
composite_id = f"{chat_id}-{message_id}"
existing = db.get(ChatMessage, composite_id)
if existing:
# Update existing
if "role" in data:
existing.role = data["role"]
if "parent_id" in data:
existing.parent_id = data.get("parent_id") or data.get("parentId")
if "content" in data:
existing.content = data.get("content")
if "output" in data:
existing.output = data.get("output")
if "model_id" in data or "model" in data:
existing.model_id = data.get("model_id") or data.get("model")
if "files" in data:
existing.files = data.get("files")
if "sources" in data:
existing.sources = data.get("sources")
if "embeds" in data:
existing.embeds = data.get("embeds")
if "done" in data:
existing.done = data.get("done", True)
if "status_history" in data or "statusHistory" in data:
existing.status_history = data.get("status_history") or data.get(
"statusHistory"
)
if "error" in data:
existing.error = data.get("error")
# Extract usage - check direct field first, then info.usage
usage = data.get("usage")
if not usage:
info = data.get("info", {})
usage = info.get("usage") if info else None
if usage:
existing.usage = usage
existing.updated_at = now
db.commit()
db.refresh(existing)
return ChatMessageModel.model_validate(existing)
else:
# Insert new
# Extract usage - check direct field first, then info.usage
usage = data.get("usage")
if not usage:
info = data.get("info", {})
usage = info.get("usage") if info else None
message = ChatMessage(
id=composite_id,
chat_id=chat_id,
user_id=user_id,
role=data.get("role", "user"),
parent_id=data.get("parent_id") or data.get("parentId"),
content=data.get("content"),
output=data.get("output"),
model_id=data.get("model_id") or data.get("model"),
files=data.get("files"),
sources=data.get("sources"),
embeds=data.get("embeds"),
done=data.get("done", True),
status_history=data.get("status_history")
or data.get("statusHistory"),
error=data.get("error"),
usage=usage,
created_at=timestamp,
updated_at=now,
)
db.add(message)
db.commit()
db.refresh(message)
return ChatMessageModel.model_validate(message)
def get_message_by_id(
self, id: str, db: Optional[Session] = None
) -> Optional[ChatMessageModel]:
with get_db_context(db) as db:
message = db.get(ChatMessage, id)
return ChatMessageModel.model_validate(message) if message else None
def get_messages_by_chat_id(
self, chat_id: str, db: Optional[Session] = None
) -> list[ChatMessageModel]:
with get_db_context(db) as db:
messages = (
db.query(ChatMessage)
.filter_by(chat_id=chat_id)
.order_by(ChatMessage.created_at.asc())
.all()
)
return [ChatMessageModel.model_validate(message) for message in messages]
def get_messages_by_user_id(
self,
user_id: str,
skip: int = 0,
limit: int = 50,
db: Optional[Session] = None,
) -> list[ChatMessageModel]:
with get_db_context(db) as db:
messages = (
db.query(ChatMessage)
.filter_by(user_id=user_id)
.order_by(ChatMessage.created_at.desc())
.offset(skip)
.limit(limit)
.all()
)
return [ChatMessageModel.model_validate(message) for message in messages]
def get_messages_by_model_id(
self,
model_id: str,
start_date: Optional[int] = None,
end_date: Optional[int] = None,
skip: int = 0,
limit: int = 100,
db: Optional[Session] = None,
) -> list[ChatMessageModel]:
with get_db_context(db) as db:
query = db.query(ChatMessage).filter_by(model_id=model_id)
if start_date:
query = query.filter(ChatMessage.created_at >= start_date)
if end_date:
query = query.filter(ChatMessage.created_at <= end_date)
messages = (
query.order_by(ChatMessage.created_at.desc())
.offset(skip)
.limit(limit)
.all()
)
return [ChatMessageModel.model_validate(message) for message in messages]
def get_chat_ids_by_model_id(
self,
model_id: str,
start_date: Optional[int] = None,
end_date: Optional[int] = None,
skip: int = 0,
limit: int = 50,
db: Optional[Session] = None,
) -> list[str]:
"""Get distinct chat_ids that used a specific model."""
with get_db_context(db) as db:
query = db.query(
ChatMessage.chat_id,
func.max(ChatMessage.created_at).label("last_message_at"),
).filter(ChatMessage.model_id == model_id)
if start_date:
query = query.filter(ChatMessage.created_at >= start_date)
if end_date:
query = query.filter(ChatMessage.created_at <= end_date)
# Group by chat_id and order by most recent message in each chat
chat_ids = (
query.group_by(ChatMessage.chat_id)
.order_by(func.max(ChatMessage.created_at).desc())
.offset(skip)
.limit(limit)
.all()
)
return [chat_id for chat_id, _ in chat_ids]
def delete_messages_by_chat_id(
self, chat_id: str, db: Optional[Session] = None
) -> bool:
with get_db_context(db) as db:
db.query(ChatMessage).filter_by(chat_id=chat_id).delete()
db.commit()
return True
# Analytics methods
def get_message_count_by_model(
self,
start_date: Optional[int] = None,
end_date: Optional[int] = None,
group_id: Optional[str] = None,
db: Optional[Session] = None,
) -> dict[str, int]:
with get_db_context(db) as db:
from sqlalchemy import func
from open_webui.models.groups import GroupMember
query = db.query(
ChatMessage.model_id, func.count(ChatMessage.id).label("count")
).filter(
ChatMessage.role == "assistant",
ChatMessage.model_id.isnot(None),
~ChatMessage.user_id.like("shared-%"),
)
if start_date:
query = query.filter(ChatMessage.created_at >= start_date)
if end_date:
query = query.filter(ChatMessage.created_at <= end_date)
if group_id:
group_users = (
db.query(GroupMember.user_id)
.filter(GroupMember.group_id == group_id)
.subquery()
)
query = query.filter(ChatMessage.user_id.in_(group_users))
results = query.group_by(ChatMessage.model_id).all()
return {row.model_id: row.count for row in results}
def get_token_usage_by_model(
self,
start_date: Optional[int] = None,
end_date: Optional[int] = None,
group_id: Optional[str] = None,
db: Optional[Session] = None,
) -> dict[str, dict]:
"""Aggregate token usage by model using database-level aggregation."""
with get_db_context(db) as db:
from sqlalchemy import func, cast, Integer
from open_webui.models.groups import GroupMember
dialect = db.bind.dialect.name
if dialect == "sqlite":
input_tokens = cast(
func.json_extract(ChatMessage.usage, "$.input_tokens"), Integer
)
output_tokens = cast(
func.json_extract(ChatMessage.usage, "$.output_tokens"), Integer
)
elif dialect == "postgresql":
# Use json_extract_path_text for PostgreSQL JSON columns
input_tokens = cast(
func.json_extract_path_text(ChatMessage.usage, "input_tokens"),
Integer,
)
output_tokens = cast(
func.json_extract_path_text(ChatMessage.usage, "output_tokens"),
Integer,
)
else:
raise NotImplementedError(f"Unsupported dialect: {dialect}")
query = db.query(
ChatMessage.model_id,
func.coalesce(func.sum(input_tokens), 0).label("input_tokens"),
func.coalesce(func.sum(output_tokens), 0).label("output_tokens"),
func.count(ChatMessage.id).label("message_count"),
).filter(
ChatMessage.role == "assistant",
ChatMessage.model_id.isnot(None),
ChatMessage.usage.isnot(None),
~ChatMessage.user_id.like("shared-%"),
)
if start_date:
query = query.filter(ChatMessage.created_at >= start_date)
if end_date:
query = query.filter(ChatMessage.created_at <= end_date)
if group_id:
group_users = (
db.query(GroupMember.user_id)
.filter(GroupMember.group_id == group_id)
.subquery()
)
query = query.filter(ChatMessage.user_id.in_(group_users))
results = query.group_by(ChatMessage.model_id).all()
return {
row.model_id: {
"input_tokens": row.input_tokens,
"output_tokens": row.output_tokens,
"total_tokens": row.input_tokens + row.output_tokens,
"message_count": row.message_count,
}
for row in results
}
def get_token_usage_by_user(
self,
start_date: Optional[int] = None,
end_date: Optional[int] = None,
db: Optional[Session] = None,
) -> dict[str, dict]:
"""Aggregate token usage by user using database-level aggregation."""
with get_db_context(db) as db:
from sqlalchemy import func, cast, Integer
dialect = db.bind.dialect.name
if dialect == "sqlite":
input_tokens = cast(
func.json_extract(ChatMessage.usage, "$.input_tokens"), Integer
)
output_tokens = cast(
func.json_extract(ChatMessage.usage, "$.output_tokens"), Integer
)
elif dialect == "postgresql":
# Use json_extract_path_text for PostgreSQL JSON columns
input_tokens = cast(
func.json_extract_path_text(ChatMessage.usage, "input_tokens"),
Integer,
)
output_tokens = cast(
func.json_extract_path_text(ChatMessage.usage, "output_tokens"),
Integer,
)
else:
raise NotImplementedError(f"Unsupported dialect: {dialect}")
query = db.query(
ChatMessage.user_id,
func.coalesce(func.sum(input_tokens), 0).label("input_tokens"),
func.coalesce(func.sum(output_tokens), 0).label("output_tokens"),
func.count(ChatMessage.id).label("message_count"),
).filter(
ChatMessage.role == "assistant",
ChatMessage.user_id.isnot(None),
ChatMessage.usage.isnot(None),
~ChatMessage.user_id.like("shared-%"),
)
if start_date:
query = query.filter(ChatMessage.created_at >= start_date)
if end_date:
query = query.filter(ChatMessage.created_at <= end_date)
results = query.group_by(ChatMessage.user_id).all()
return {
row.user_id: {
"input_tokens": row.input_tokens,
"output_tokens": row.output_tokens,
"total_tokens": row.input_tokens + row.output_tokens,
"message_count": row.message_count,
}
for row in results
}
def get_message_count_by_user(
self,
start_date: Optional[int] = None,
end_date: Optional[int] = None,
group_id: Optional[str] = None,
db: Optional[Session] = None,
) -> dict[str, int]:
with get_db_context(db) as db:
from sqlalchemy import func
from open_webui.models.groups import GroupMember
query = db.query(
ChatMessage.user_id, func.count(ChatMessage.id).label("count")
).filter(~ChatMessage.user_id.like("shared-%"))
if start_date:
query = query.filter(ChatMessage.created_at >= start_date)
if end_date:
query = query.filter(ChatMessage.created_at <= end_date)
if group_id:
group_users = (
db.query(GroupMember.user_id)
.filter(GroupMember.group_id == group_id)
.subquery()
)
query = query.filter(ChatMessage.user_id.in_(group_users))
results = query.group_by(ChatMessage.user_id).all()
return {row.user_id: row.count for row in results}
def get_message_count_by_chat(
self,
start_date: Optional[int] = None,
end_date: Optional[int] = None,
group_id: Optional[str] = None,
db: Optional[Session] = None,
) -> dict[str, int]:
with get_db_context(db) as db:
from sqlalchemy import func
from open_webui.models.groups import GroupMember
query = db.query(
ChatMessage.chat_id, func.count(ChatMessage.id).label("count")
).filter(~ChatMessage.user_id.like("shared-%"))
if start_date:
query = query.filter(ChatMessage.created_at >= start_date)
if end_date:
query = query.filter(ChatMessage.created_at <= end_date)
if group_id:
group_users = (
db.query(GroupMember.user_id)
.filter(GroupMember.group_id == group_id)
.subquery()
)
query = query.filter(ChatMessage.user_id.in_(group_users))
results = query.group_by(ChatMessage.chat_id).all()
return {row.chat_id: row.count for row in results}
def get_daily_message_counts_by_model(
self,
start_date: Optional[int] = None,
end_date: Optional[int] = None,
group_id: Optional[str] = None,
db: Optional[Session] = None,
) -> dict[str, dict[str, int]]:
"""Get message counts grouped by day and model."""
with get_db_context(db) as db:
from datetime import datetime, timedelta
from open_webui.models.groups import GroupMember
query = db.query(ChatMessage.created_at, ChatMessage.model_id).filter(
ChatMessage.role == "assistant",
ChatMessage.model_id.isnot(None),
~ChatMessage.user_id.like("shared-%"),
)
if start_date:
query = query.filter(ChatMessage.created_at >= start_date)
if end_date:
query = query.filter(ChatMessage.created_at <= end_date)
if group_id:
group_users = (
db.query(GroupMember.user_id)
.filter(GroupMember.group_id == group_id)
.subquery()
)
query = query.filter(ChatMessage.user_id.in_(group_users))
results = query.all()
# Group by date -> model -> count
daily_counts: dict[str, dict[str, int]] = {}
for timestamp, model_id in results:
date_str = datetime.fromtimestamp(
_normalize_timestamp(timestamp)
).strftime("%Y-%m-%d")
if date_str not in daily_counts:
daily_counts[date_str] = {}
daily_counts[date_str][model_id] = (
daily_counts[date_str].get(model_id, 0) + 1
)
# Fill in missing days
if start_date and end_date:
current = datetime.fromtimestamp(_normalize_timestamp(start_date))
end_dt = datetime.fromtimestamp(_normalize_timestamp(end_date))
while current <= end_dt:
date_str = current.strftime("%Y-%m-%d")
if date_str not in daily_counts:
daily_counts[date_str] = {}
current += timedelta(days=1)
return daily_counts
def get_hourly_message_counts_by_model(
self,
start_date: Optional[int] = None,
end_date: Optional[int] = None,
db: Optional[Session] = None,
) -> dict[str, dict[str, int]]:
"""Get message counts grouped by hour and model."""
with get_db_context(db) as db:
from datetime import datetime, timedelta
query = db.query(ChatMessage.created_at, ChatMessage.model_id).filter(
ChatMessage.role == "assistant",
ChatMessage.model_id.isnot(None),
~ChatMessage.user_id.like("shared-%"),
)
if start_date:
query = query.filter(ChatMessage.created_at >= start_date)
if end_date:
query = query.filter(ChatMessage.created_at <= end_date)
results = query.all()
# Group by hour -> model -> count
hourly_counts: dict[str, dict[str, int]] = {}
for timestamp, model_id in results:
hour_str = datetime.fromtimestamp(
_normalize_timestamp(timestamp)
).strftime("%Y-%m-%d %H:00")
if hour_str not in hourly_counts:
hourly_counts[hour_str] = {}
hourly_counts[hour_str][model_id] = (
hourly_counts[hour_str].get(model_id, 0) + 1
)
# Fill in missing hours
if start_date and end_date:
current = datetime.fromtimestamp(
_normalize_timestamp(start_date)
).replace(minute=0, second=0, microsecond=0)
end_dt = datetime.fromtimestamp(_normalize_timestamp(end_date))
while current <= end_dt:
hour_str = current.strftime("%Y-%m-%d %H:00")
if hour_str not in hourly_counts:
hourly_counts[hour_str] = {}
current += timedelta(hours=1)
return hourly_counts
ChatMessages = ChatMessageTable()
File diff suppressed because it is too large Load Diff
+278 -41
View File
@@ -3,15 +3,14 @@ import time
import uuid
from typing import Optional
from open_webui.internal.db import Base, get_db
from open_webui.models.chats import Chats
from sqlalchemy.orm import Session
from open_webui.internal.db import Base, JSONField, get_db, get_db_context
from open_webui.models.users import User
from open_webui.env import SRC_LOG_LEVELS
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Column, Text, JSON, Boolean
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
####################
@@ -21,7 +20,7 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
class Feedback(Base):
__tablename__ = "feedback"
id = Column(Text, primary_key=True)
id = Column(Text, primary_key=True, unique=True)
user_id = Column(Text)
version = Column(BigInteger, default=0)
type = Column(Text)
@@ -62,6 +61,20 @@ class FeedbackResponse(BaseModel):
updated_at: int
class FeedbackIdResponse(BaseModel):
id: str
user_id: str
created_at: int
updated_at: int
class LeaderboardFeedbackData(BaseModel):
"""Minimal feedback data for leaderboard computation (excludes snapshot/meta)."""
id: str
data: Optional[dict] = None
class RatingData(BaseModel):
rating: Optional[str | int] = None
model_id: Optional[str] = None
@@ -92,11 +105,44 @@ class FeedbackForm(BaseModel):
model_config = ConfigDict(extra="allow")
class UserResponse(BaseModel):
id: str
name: str
email: str
role: str = "pending"
last_active_at: int # timestamp in epoch
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
model_config = ConfigDict(from_attributes=True)
class FeedbackUserResponse(FeedbackResponse):
user: Optional[UserResponse] = None
class FeedbackListResponse(BaseModel):
items: list[FeedbackUserResponse]
total: int
class ModelHistoryEntry(BaseModel):
date: str
won: int
lost: int
class ModelHistoryResponse(BaseModel):
model_id: str
history: list[ModelHistoryEntry]
class FeedbackTable:
def insert_new_feedback(
self, user_id: str, form_data: FeedbackForm
self, user_id: str, form_data: FeedbackForm, db: Optional[Session] = None
) -> Optional[FeedbackModel]:
with get_db() as db:
with get_db_context(db) as db:
id = str(uuid.uuid4())
feedback = FeedbackModel(
**{
@@ -121,9 +167,11 @@ class FeedbackTable:
log.exception(f"Error creating a new feedback: {e}")
return None
def get_feedback_by_id(self, id: str) -> Optional[FeedbackModel]:
def get_feedback_by_id(
self, id: str, db: Optional[Session] = None
) -> Optional[FeedbackModel]:
try:
with get_db() as db:
with get_db_context(db) as db:
feedback = db.query(Feedback).filter_by(id=id).first()
if not feedback:
return None
@@ -132,10 +180,10 @@ class FeedbackTable:
return None
def get_feedback_by_id_and_user_id(
self, id: str, user_id: str
self, id: str, user_id: str, db: Optional[Session] = None
) -> Optional[FeedbackModel]:
try:
with get_db() as db:
with get_db_context(db) as db:
feedback = db.query(Feedback).filter_by(id=id, user_id=user_id).first()
if not feedback:
return None
@@ -143,8 +191,93 @@ class FeedbackTable:
except Exception:
return None
def get_all_feedbacks(self) -> list[FeedbackModel]:
with get_db() as db:
def get_feedbacks_by_chat_id(
self, chat_id: str, db: Optional[Session] = None
) -> list[FeedbackModel]:
"""Get all feedbacks for a specific chat."""
try:
with get_db_context(db) as db:
# meta.chat_id stores the chat reference
feedbacks = (
db.query(Feedback)
.filter(Feedback.meta["chat_id"].as_string() == chat_id)
.order_by(Feedback.created_at.desc())
.all()
)
return [FeedbackModel.model_validate(fb) for fb in feedbacks]
except Exception:
return []
def get_feedback_items(
self,
filter: dict = {},
skip: int = 0,
limit: int = 30,
db: Optional[Session] = None,
) -> FeedbackListResponse:
with get_db_context(db) as db:
query = db.query(Feedback, User).join(User, Feedback.user_id == User.id)
if filter:
order_by = filter.get("order_by")
direction = filter.get("direction")
if order_by == "username":
if direction == "asc":
query = query.order_by(User.name.asc())
else:
query = query.order_by(User.name.desc())
elif order_by == "model_id":
# it's stored in feedback.data['model_id']
if direction == "asc":
query = query.order_by(
Feedback.data["model_id"].as_string().asc()
)
else:
query = query.order_by(
Feedback.data["model_id"].as_string().desc()
)
elif order_by == "rating":
# it's stored in feedback.data['rating']
if direction == "asc":
query = query.order_by(
Feedback.data["rating"].as_string().asc()
)
else:
query = query.order_by(
Feedback.data["rating"].as_string().desc()
)
elif order_by == "updated_at":
if direction == "asc":
query = query.order_by(Feedback.updated_at.asc())
else:
query = query.order_by(Feedback.updated_at.desc())
else:
query = query.order_by(Feedback.created_at.desc())
# Count BEFORE pagination
total = query.count()
if skip:
query = query.offset(skip)
if limit:
query = query.limit(limit)
items = query.all()
feedbacks = []
for feedback, user in items:
feedback_model = FeedbackModel.model_validate(feedback)
user_model = UserResponse.model_validate(user)
feedbacks.append(
FeedbackUserResponse(**feedback_model.model_dump(), user=user_model)
)
return FeedbackListResponse(items=feedbacks, total=total)
def get_all_feedbacks(self, db: Optional[Session] = None) -> list[FeedbackModel]:
with get_db_context(db) as db:
return [
FeedbackModel.model_validate(feedback)
for feedback in db.query(Feedback)
@@ -152,8 +285,110 @@ class FeedbackTable:
.all()
]
def get_feedbacks_by_type(self, type: str) -> list[FeedbackModel]:
with get_db() as db:
def get_all_feedback_ids(
self, db: Optional[Session] = None
) -> list[FeedbackIdResponse]:
with get_db_context(db) as db:
return [
FeedbackIdResponse(
id=row.id,
user_id=row.user_id,
created_at=row.created_at,
updated_at=row.updated_at,
)
for row in db.query(
Feedback.id,
Feedback.user_id,
Feedback.created_at,
Feedback.updated_at,
)
.order_by(Feedback.updated_at.desc())
.all()
]
def get_feedbacks_for_leaderboard(
self, db: Optional[Session] = None
) -> list[LeaderboardFeedbackData]:
"""Fetch only id and data for leaderboard computation (excludes snapshot/meta)."""
with get_db_context(db) as db:
return [
LeaderboardFeedbackData(id=row.id, data=row.data)
for row in db.query(Feedback.id, Feedback.data).all()
]
def get_model_evaluation_history(
self, model_id: str, days: int = 30, db: Optional[Session] = None
) -> list[ModelHistoryEntry]:
"""
Get daily wins/losses for a specific model over the past N days.
If days=0, returns all time data starting from first feedback.
Returns: [{"date": "2026-01-08", "won": 5, "lost": 2}, ...]
"""
from datetime import datetime, timedelta
from collections import defaultdict
with get_db_context(db) as db:
if days == 0:
# All time - no cutoff
rows = db.query(Feedback.created_at, Feedback.data).all()
else:
cutoff = int(time.time()) - (days * 86400)
rows = (
db.query(Feedback.created_at, Feedback.data)
.filter(Feedback.created_at >= cutoff)
.all()
)
daily_counts = defaultdict(lambda: {"won": 0, "lost": 0})
first_date = None
for created_at, data in rows:
if not data:
continue
if data.get("model_id") != model_id:
continue
rating_str = str(data.get("rating", ""))
if rating_str not in ("1", "-1"):
continue
date_str = datetime.fromtimestamp(created_at).strftime("%Y-%m-%d")
if rating_str == "1":
daily_counts[date_str]["won"] += 1
else:
daily_counts[date_str]["lost"] += 1
# Track first date for this model
if first_date is None or date_str < first_date:
first_date = date_str
# Generate date range
result = []
today = datetime.now().date()
if days == 0 and first_date:
# All time: start from first feedback date
start_date = datetime.strptime(first_date, "%Y-%m-%d").date()
num_days = (today - start_date).days + 1
else:
# Fixed range
num_days = days
start_date = today - timedelta(days=days - 1)
for i in range(num_days):
d = start_date + timedelta(days=i)
date_str = d.strftime("%Y-%m-%d")
counts = daily_counts.get(date_str, {"won": 0, "lost": 0})
result.append(
ModelHistoryEntry(date=date_str, won=counts["won"], lost=counts["lost"])
)
return result
def get_feedbacks_by_type(
self, type: str, db: Optional[Session] = None
) -> list[FeedbackModel]:
with get_db_context(db) as db:
return [
FeedbackModel.model_validate(feedback)
for feedback in db.query(Feedback)
@@ -162,8 +397,10 @@ class FeedbackTable:
.all()
]
def get_feedbacks_by_user_id(self, user_id: str) -> list[FeedbackModel]:
with get_db() as db:
def get_feedbacks_by_user_id(
self, user_id: str, db: Optional[Session] = None
) -> list[FeedbackModel]:
with get_db_context(db) as db:
return [
FeedbackModel.model_validate(feedback)
for feedback in db.query(Feedback)
@@ -173,9 +410,9 @@ class FeedbackTable:
]
def update_feedback_by_id(
self, id: str, form_data: FeedbackForm
self, id: str, form_data: FeedbackForm, db: Optional[Session] = None
) -> Optional[FeedbackModel]:
with get_db() as db:
with get_db_context(db) as db:
feedback = db.query(Feedback).filter_by(id=id).first()
if not feedback:
return None
@@ -193,9 +430,13 @@ class FeedbackTable:
return FeedbackModel.model_validate(feedback)
def update_feedback_by_id_and_user_id(
self, id: str, user_id: str, form_data: FeedbackForm
self,
id: str,
user_id: str,
form_data: FeedbackForm,
db: Optional[Session] = None,
) -> Optional[FeedbackModel]:
with get_db() as db:
with get_db_context(db) as db:
feedback = db.query(Feedback).filter_by(id=id, user_id=user_id).first()
if not feedback:
return None
@@ -212,8 +453,8 @@ class FeedbackTable:
db.commit()
return FeedbackModel.model_validate(feedback)
def delete_feedback_by_id(self, id: str) -> bool:
with get_db() as db:
def delete_feedback_by_id(self, id: str, db: Optional[Session] = None) -> bool:
with get_db_context(db) as db:
feedback = db.query(Feedback).filter_by(id=id).first()
if not feedback:
return False
@@ -221,8 +462,10 @@ class FeedbackTable:
db.commit()
return True
def delete_feedback_by_id_and_user_id(self, id: str, user_id: str) -> bool:
with get_db() as db:
def delete_feedback_by_id_and_user_id(
self, id: str, user_id: str, db: Optional[Session] = None
) -> bool:
with get_db_context(db) as db:
feedback = db.query(Feedback).filter_by(id=id, user_id=user_id).first()
if not feedback:
return False
@@ -230,25 +473,19 @@ class FeedbackTable:
db.commit()
return True
def delete_feedbacks_by_user_id(self, user_id: str) -> bool:
with get_db() as db:
feedbacks = db.query(Feedback).filter_by(user_id=user_id).all()
if not feedbacks:
return False
for feedback in feedbacks:
db.delete(feedback)
def delete_feedbacks_by_user_id(
self, user_id: str, db: Optional[Session] = None
) -> bool:
with get_db_context(db) as db:
result = db.query(Feedback).filter_by(user_id=user_id).delete()
db.commit()
return True
return result > 0
def delete_all_feedbacks(self) -> bool:
with get_db() as db:
feedbacks = db.query(Feedback).all()
if not feedbacks:
return False
for feedback in feedbacks:
db.delete(feedback)
def delete_all_feedbacks(self, db: Optional[Session] = None) -> bool:
with get_db_context(db) as db:
result = db.query(Feedback).delete()
db.commit()
return True
return result > 0
Feedbacks = FeedbackTable()
+196 -41
View File
@@ -2,13 +2,12 @@ import logging
import time
from typing import Optional
from open_webui.internal.db import Base, JSONField, get_db
from open_webui.env import SRC_LOG_LEVELS
from pydantic import BaseModel, ConfigDict
from sqlalchemy.orm import Session
from open_webui.internal.db import Base, JSONField, get_db, get_db_context
from pydantic import BaseModel, ConfigDict, model_validator
from sqlalchemy import BigInteger, Column, String, Text, JSON
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
####################
# Files DB Schema
@@ -17,7 +16,7 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
class File(Base):
__tablename__ = "file"
id = Column(String, primary_key=True)
id = Column(String, primary_key=True, unique=True)
user_id = Column(String)
hash = Column(Text, nullable=True)
@@ -27,8 +26,6 @@ class File(Base):
data = Column(JSON, nullable=True)
meta = Column(JSON, nullable=True)
access_control = Column(JSON, nullable=True)
created_at = Column(BigInteger)
updated_at = Column(BigInteger)
@@ -46,8 +43,6 @@ class FileModel(BaseModel):
data: Optional[dict] = None
meta: Optional[dict] = None
access_control: Optional[dict] = None
created_at: Optional[int] # timestamp in epoch
updated_at: Optional[int] # timestamp in epoch
@@ -64,6 +59,25 @@ class FileMeta(BaseModel):
model_config = ConfigDict(extra="allow")
@model_validator(mode="before")
@classmethod
def sanitize_meta(cls, data):
"""Sanitize metadata fields to handle malformed legacy data."""
if not isinstance(data, dict):
return data
# Handle content_type that may be a list like ['application/pdf', None]
content_type = data.get("content_type")
if isinstance(content_type, list):
# Extract first non-None string value
data["content_type"] = next(
(item for item in content_type if isinstance(item, str)), None
)
elif content_type is not None and not isinstance(content_type, str):
data["content_type"] = None
return data
class FileModelResponse(BaseModel):
id: str
@@ -75,14 +89,15 @@ class FileModelResponse(BaseModel):
meta: FileMeta
created_at: int # timestamp in epoch
updated_at: int # timestamp in epoch
updated_at: Optional[int] = None # timestamp in epoch, optional for legacy files
model_config = ConfigDict(extra="allow")
class FileMetadataResponse(BaseModel):
id: str
meta: dict
hash: Optional[str] = None
meta: Optional[dict] = None
created_at: int # timestamp in epoch
updated_at: int # timestamp in epoch
@@ -94,12 +109,24 @@ class FileForm(BaseModel):
path: str
data: dict = {}
meta: dict = {}
access_control: Optional[dict] = None
class FileUpdateForm(BaseModel):
hash: Optional[str] = None
data: Optional[dict] = None
meta: Optional[dict] = None
class FileListResponse(BaseModel):
items: list[FileModel]
total: int
class FilesTable:
def insert_new_file(self, user_id: str, form_data: FileForm) -> Optional[FileModel]:
with get_db() as db:
def insert_new_file(
self, user_id: str, form_data: FileForm, db: Optional[Session] = None
) -> Optional[FileModel]:
with get_db_context(db) as db:
file = FileModel(
**{
**form_data.model_dump(),
@@ -122,20 +149,41 @@ class FilesTable:
log.exception(f"Error inserting a new file: {e}")
return None
def get_file_by_id(self, id: str) -> Optional[FileModel]:
with get_db() as db:
def get_file_by_id(
self, id: str, db: Optional[Session] = None
) -> Optional[FileModel]:
try:
with get_db_context(db) as db:
try:
file = db.get(File, id)
return FileModel.model_validate(file)
except Exception:
return None
except Exception:
return None
def get_file_by_id_and_user_id(
self, id: str, user_id: str, db: Optional[Session] = None
) -> Optional[FileModel]:
with get_db_context(db) as db:
try:
file = db.get(File, id)
return FileModel.model_validate(file)
file = db.query(File).filter_by(id=id, user_id=user_id).first()
if file:
return FileModel.model_validate(file)
else:
return None
except Exception:
return None
def get_file_metadata_by_id(self, id: str) -> Optional[FileMetadataResponse]:
with get_db() as db:
def get_file_metadata_by_id(
self, id: str, db: Optional[Session] = None
) -> Optional[FileMetadataResponse]:
with get_db_context(db) as db:
try:
file = db.get(File, id)
return FileMetadataResponse(
id=file.id,
hash=file.hash,
meta=file.meta,
created_at=file.created_at,
updated_at=file.updated_at,
@@ -143,12 +191,14 @@ class FilesTable:
except Exception:
return None
def get_files(self) -> list[FileModel]:
with get_db() as db:
def get_files(self, db: Optional[Session] = None) -> list[FileModel]:
with get_db_context(db) as db:
return [FileModel.model_validate(file) for file in db.query(File).all()]
def check_access_by_user_id(self, id, user_id, permission="write") -> bool:
file = self.get_file_by_id(id)
def check_access_by_user_id(
self, id, user_id, permission="write", db: Optional[Session] = None
) -> bool:
file = self.get_file_by_id(id, db=db)
if not file:
return False
if file.user_id == user_id:
@@ -156,8 +206,10 @@ class FilesTable:
# Implement additional access control logic here as needed
return False
def get_files_by_ids(self, ids: list[str]) -> list[FileModel]:
with get_db() as db:
def get_files_by_ids(
self, ids: list[str], db: Optional[Session] = None
) -> list[FileModel]:
with get_db_context(db) as db:
return [
FileModel.model_validate(file)
for file in db.query(File)
@@ -166,62 +218,165 @@ class FilesTable:
.all()
]
def get_file_metadatas_by_ids(self, ids: list[str]) -> list[FileMetadataResponse]:
with get_db() as db:
def get_file_metadatas_by_ids(
self, ids: list[str], db: Optional[Session] = None
) -> list[FileMetadataResponse]:
with get_db_context(db) as db:
return [
FileMetadataResponse(
id=file.id,
hash=file.hash,
meta=file.meta,
created_at=file.created_at,
updated_at=file.updated_at,
)
for file in db.query(File)
for file in db.query(
File.id, File.hash, File.meta, File.created_at, File.updated_at
)
.filter(File.id.in_(ids))
.order_by(File.updated_at.desc())
.all()
]
def get_files_by_user_id(self, user_id: str) -> list[FileModel]:
with get_db() as db:
def get_files_by_user_id(
self, user_id: str, db: Optional[Session] = None
) -> list[FileModel]:
with get_db_context(db) as db:
return [
FileModel.model_validate(file)
for file in db.query(File).filter_by(user_id=user_id).all()
]
def update_file_hash_by_id(self, id: str, hash: str) -> Optional[FileModel]:
with get_db() as db:
@staticmethod
def _glob_to_like_pattern(glob: str) -> str:
"""
Convert a glob/fnmatch pattern to a SQL LIKE pattern.
Escapes SQL special characters and converts glob wildcards:
- `*` becomes `%` (match any sequence of characters)
- `?` becomes `_` (match exactly one character)
Args:
glob: A glob pattern (e.g., "*.txt", "file?.doc")
Returns:
A SQL LIKE compatible pattern with proper escaping.
"""
# Escape SQL special characters first, then convert glob wildcards
pattern = glob.replace("\\", "\\\\")
pattern = pattern.replace("%", "\\%")
pattern = pattern.replace("_", "\\_")
pattern = pattern.replace("*", "%")
pattern = pattern.replace("?", "_")
return pattern
def search_files(
self,
user_id: Optional[str] = None,
filename: str = "*",
skip: int = 0,
limit: int = 100,
db: Optional[Session] = None,
) -> list[FileModel]:
"""
Search files with glob pattern matching, optional user filter, and pagination.
Args:
user_id: Filter by user ID. If None, returns files for all users.
filename: Glob pattern to match filenames (e.g., "*.txt"). Default "*" matches all.
skip: Number of results to skip for pagination.
limit: Maximum number of results to return.
db: Optional database session.
Returns:
List of matching FileModel objects, ordered by updated_at descending.
"""
with get_db_context(db) as db:
query = db.query(File)
if user_id:
query = query.filter_by(user_id=user_id)
pattern = self._glob_to_like_pattern(filename)
if pattern != "%":
query = query.filter(File.filename.ilike(pattern, escape="\\"))
return [
FileModel.model_validate(file)
for file in query.order_by(File.updated_at.desc())
.offset(skip)
.limit(limit)
.all()
]
def update_file_by_id(
self, id: str, form_data: FileUpdateForm, db: Optional[Session] = None
) -> Optional[FileModel]:
with get_db_context(db) as db:
try:
file = db.query(File).filter_by(id=id).first()
if form_data.hash is not None:
file.hash = form_data.hash
if form_data.data is not None:
file.data = {**(file.data if file.data else {}), **form_data.data}
if form_data.meta is not None:
file.meta = {**(file.meta if file.meta else {}), **form_data.meta}
file.updated_at = int(time.time())
db.commit()
return FileModel.model_validate(file)
except Exception as e:
log.exception(f"Error updating file completely by id: {e}")
return None
def update_file_hash_by_id(
self, id: str, hash: Optional[str], db: Optional[Session] = None
) -> Optional[FileModel]:
with get_db_context(db) as db:
try:
file = db.query(File).filter_by(id=id).first()
file.hash = hash
file.updated_at = int(time.time())
db.commit()
return FileModel.model_validate(file)
except Exception:
return None
def update_file_data_by_id(self, id: str, data: dict) -> Optional[FileModel]:
with get_db() as db:
def update_file_data_by_id(
self, id: str, data: dict, db: Optional[Session] = None
) -> Optional[FileModel]:
with get_db_context(db) as db:
try:
file = db.query(File).filter_by(id=id).first()
file.data = {**(file.data if file.data else {}), **data}
file.updated_at = int(time.time())
db.commit()
return FileModel.model_validate(file)
except Exception as e:
return None
def update_file_metadata_by_id(self, id: str, meta: dict) -> Optional[FileModel]:
with get_db() as db:
def update_file_metadata_by_id(
self, id: str, meta: dict, db: Optional[Session] = None
) -> Optional[FileModel]:
with get_db_context(db) as db:
try:
file = db.query(File).filter_by(id=id).first()
file.meta = {**(file.meta if file.meta else {}), **meta}
file.updated_at = int(time.time())
db.commit()
return FileModel.model_validate(file)
except Exception:
return None
def delete_file_by_id(self, id: str) -> bool:
with get_db() as db:
return False
def delete_file_by_id(self, id: str, db: Optional[Session] = None) -> bool:
with get_db_context(db) as db:
try:
db.query(File).filter_by(id=id).delete()
db.commit()
@@ -230,8 +385,8 @@ class FilesTable:
except Exception:
return False
def delete_all_files(self) -> bool:
with get_db() as db:
def delete_all_files(self, db: Optional[Session] = None) -> bool:
with get_db_context(db) as db:
try:
db.query(File).delete()
db.commit()
+60 -31
View File
@@ -7,13 +7,11 @@ import re
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Column, Text, JSON, Boolean, func
from sqlalchemy.orm import Session
from open_webui.internal.db import Base, get_db
from open_webui.env import SRC_LOG_LEVELS
from open_webui.internal.db import Base, JSONField, get_db, get_db_context
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
####################
@@ -23,7 +21,7 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
class Folder(Base):
__tablename__ = "folder"
id = Column(Text, primary_key=True)
id = Column(Text, primary_key=True, unique=True)
parent_id = Column(Text, nullable=True)
user_id = Column(Text)
name = Column(Text)
@@ -50,6 +48,20 @@ class FolderModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
class FolderMetadataResponse(BaseModel):
icon: Optional[str] = None
class FolderNameIdResponse(BaseModel):
id: str
name: str
meta: Optional[FolderMetadataResponse] = None
parent_id: Optional[str] = None
is_expanded: bool = False
created_at: int
updated_at: int
####################
# Forms
####################
@@ -71,9 +83,13 @@ class FolderUpdateForm(BaseModel):
class FolderTable:
def insert_new_folder(
self, user_id: str, form_data: FolderForm, parent_id: Optional[str] = None
self,
user_id: str,
form_data: FolderForm,
parent_id: Optional[str] = None,
db: Optional[Session] = None,
) -> Optional[FolderModel]:
with get_db() as db:
with get_db_context(db) as db:
id = str(uuid.uuid4())
folder = FolderModel(
**{
@@ -99,10 +115,10 @@ class FolderTable:
return None
def get_folder_by_id_and_user_id(
self, id: str, user_id: str
self, id: str, user_id: str, db: Optional[Session] = None
) -> Optional[FolderModel]:
try:
with get_db() as db:
with get_db_context(db) as db:
folder = db.query(Folder).filter_by(id=id, user_id=user_id).first()
if not folder:
@@ -113,15 +129,15 @@ class FolderTable:
return None
def get_children_folders_by_id_and_user_id(
self, id: str, user_id: str
self, id: str, user_id: str, db: Optional[Session] = None
) -> Optional[list[FolderModel]]:
try:
with get_db() as db:
with get_db_context(db) as db:
folders = []
def get_children(folder):
children = self.get_folders_by_parent_id_and_user_id(
folder.id, user_id
folder.id, user_id, db=db
)
for child in children:
get_children(child)
@@ -136,18 +152,24 @@ class FolderTable:
except Exception:
return None
def get_folders_by_user_id(self, user_id: str) -> list[FolderModel]:
with get_db() as db:
def get_folders_by_user_id(
self, user_id: str, db: Optional[Session] = None
) -> list[FolderModel]:
with get_db_context(db) as db:
return [
FolderModel.model_validate(folder)
for folder in db.query(Folder).filter_by(user_id=user_id).all()
]
def get_folder_by_parent_id_and_user_id_and_name(
self, parent_id: Optional[str], user_id: str, name: str
self,
parent_id: Optional[str],
user_id: str,
name: str,
db: Optional[Session] = None,
) -> Optional[FolderModel]:
try:
with get_db() as db:
with get_db_context(db) as db:
# Check if folder exists
folder = (
db.query(Folder)
@@ -165,9 +187,9 @@ class FolderTable:
return None
def get_folders_by_parent_id_and_user_id(
self, parent_id: Optional[str], user_id: str
self, parent_id: Optional[str], user_id: str, db: Optional[Session] = None
) -> list[FolderModel]:
with get_db() as db:
with get_db_context(db) as db:
return [
FolderModel.model_validate(folder)
for folder in db.query(Folder)
@@ -180,9 +202,10 @@ class FolderTable:
id: str,
user_id: str,
parent_id: str,
db: Optional[Session] = None,
) -> Optional[FolderModel]:
try:
with get_db() as db:
with get_db_context(db) as db:
folder = db.query(Folder).filter_by(id=id, user_id=user_id).first()
if not folder:
@@ -199,10 +222,14 @@ class FolderTable:
return
def update_folder_by_id_and_user_id(
self, id: str, user_id: str, form_data: FolderUpdateForm
self,
id: str,
user_id: str,
form_data: FolderUpdateForm,
db: Optional[Session] = None,
) -> Optional[FolderModel]:
try:
with get_db() as db:
with get_db_context(db) as db:
folder = db.query(Folder).filter_by(id=id, user_id=user_id).first()
if not folder:
@@ -245,10 +272,10 @@ class FolderTable:
return
def update_folder_is_expanded_by_id_and_user_id(
self, id: str, user_id: str, is_expanded: bool
self, id: str, user_id: str, is_expanded: bool, db: Optional[Session] = None
) -> Optional[FolderModel]:
try:
with get_db() as db:
with get_db_context(db) as db:
folder = db.query(Folder).filter_by(id=id, user_id=user_id).first()
if not folder:
@@ -264,10 +291,12 @@ class FolderTable:
log.error(f"update_folder: {e}")
return
def delete_folder_by_id_and_user_id(self, id: str, user_id: str) -> list[str]:
def delete_folder_by_id_and_user_id(
self, id: str, user_id: str, db: Optional[Session] = None
) -> list[str]:
try:
folder_ids = []
with get_db() as db:
with get_db_context(db) as db:
folder = db.query(Folder).filter_by(id=id, user_id=user_id).first()
if not folder:
return folder_ids
@@ -277,7 +306,7 @@ class FolderTable:
# Delete all children folders
def delete_children(folder):
folder_children = self.get_folders_by_parent_id_and_user_id(
folder.id, user_id
folder.id, user_id, db=db
)
for folder_child in folder_children:
@@ -302,7 +331,7 @@ class FolderTable:
return name.strip().lower()
def search_folders_by_names(
self, user_id: str, queries: list[str]
self, user_id: str, queries: list[str], db: Optional[Session] = None
) -> list[FolderModel]:
"""
Search for folders for a user where the name matches any of the queries, treating _ and space as equivalent, case-insensitive.
@@ -312,7 +341,7 @@ class FolderTable:
return []
results = {}
with get_db() as db:
with get_db_context(db) as db:
folders = db.query(Folder).filter_by(user_id=user_id).all()
for folder in folders:
if self.normalize_folder_name(folder.name) in normalized_queries:
@@ -320,7 +349,7 @@ class FolderTable:
# get children folders
children = self.get_children_folders_by_id_and_user_id(
folder.id, user_id
folder.id, user_id, db=db
)
for child in children:
results[child.id] = child
@@ -333,14 +362,14 @@ class FolderTable:
return results
def search_folders_by_name_contains(
self, user_id: str, query: str
self, user_id: str, query: str, db: Optional[Session] = None
) -> list[FolderModel]:
"""
Partial match: normalized name contains (as substring) the normalized query.
"""
normalized_query = self.normalize_folder_name(query)
results = []
with get_db() as db:
with get_db_context(db) as db:
folders = db.query(Folder).filter_by(user_id=user_id).all()
for folder in folders:
norm_name = self.normalize_folder_name(folder.name)
+124 -36
View File
@@ -2,14 +2,13 @@ import logging
import time
from typing import Optional
from open_webui.internal.db import Base, JSONField, get_db
from open_webui.models.users import Users
from open_webui.env import SRC_LOG_LEVELS
from sqlalchemy.orm import Session
from open_webui.internal.db import Base, JSONField, get_db, get_db_context
from open_webui.models.users import Users, UserModel
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Boolean, Column, String, Text, Index
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
####################
# Functions DB Schema
@@ -19,7 +18,7 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
class Function(Base):
__tablename__ = "function"
id = Column(String, primary_key=True)
id = Column(String, primary_key=True, unique=True)
user_id = Column(String)
name = Column(Text)
type = Column(Text)
@@ -37,6 +36,7 @@ class Function(Base):
class FunctionMeta(BaseModel):
description: Optional[str] = None
manifest: Optional[dict] = {}
model_config = ConfigDict(extra="allow")
class FunctionModel(BaseModel):
@@ -75,6 +75,10 @@ class FunctionWithValvesModel(BaseModel):
####################
class FunctionUserResponse(FunctionModel):
user: Optional[UserModel] = None
class FunctionResponse(BaseModel):
id: str
user_id: str
@@ -100,7 +104,11 @@ class FunctionValves(BaseModel):
class FunctionsTable:
def insert_new_function(
self, user_id: str, type: str, form_data: FunctionForm
self,
user_id: str,
type: str,
form_data: FunctionForm,
db: Optional[Session] = None,
) -> Optional[FunctionModel]:
function = FunctionModel(
**{
@@ -113,7 +121,7 @@ class FunctionsTable:
)
try:
with get_db() as db:
with get_db_context(db) as db:
result = Function(**function.model_dump())
db.add(result)
db.commit()
@@ -127,11 +135,14 @@ class FunctionsTable:
return None
def sync_functions(
self, user_id: str, functions: list[FunctionWithValvesModel]
self,
user_id: str,
functions: list[FunctionWithValvesModel],
db: Optional[Session] = None,
) -> list[FunctionWithValvesModel]:
# Synchronize functions for a user by updating existing ones, inserting new ones, and removing those that are no longer present.
try:
with get_db() as db:
with get_db_context(db) as db:
# Get existing functions
existing_functions = db.query(Function).all()
existing_ids = {func.id for func in existing_functions}
@@ -174,18 +185,39 @@ class FunctionsTable:
log.exception(f"Error syncing functions for user {user_id}: {e}")
return []
def get_function_by_id(self, id: str) -> Optional[FunctionModel]:
def get_function_by_id(
self, id: str, db: Optional[Session] = None
) -> Optional[FunctionModel]:
try:
with get_db() as db:
with get_db_context(db) as db:
function = db.get(Function, id)
return FunctionModel.model_validate(function)
except Exception:
return None
def get_functions_by_ids(
self, ids: list[str], db: Optional[Session] = None
) -> list[FunctionModel]:
"""
Batch fetch multiple functions by their IDs in a single query.
Returns functions in the same order as the input IDs (None entries filtered out).
"""
if not ids:
return []
try:
with get_db_context(db) as db:
functions = db.query(Function).filter(Function.id.in_(ids)).all()
# Create a dict for O(1) lookup
func_dict = {f.id: FunctionModel.model_validate(f) for f in functions}
# Return in original order, filtering out any not found
return [func_dict[id] for id in ids if id in func_dict]
except Exception:
return []
def get_functions(
self, active_only=False, include_valves=False
self, active_only=False, include_valves=False, db: Optional[Session] = None
) -> list[FunctionModel | FunctionWithValvesModel]:
with get_db() as db:
with get_db_context(db) as db:
if active_only:
functions = db.query(Function).filter_by(is_active=True).all()
@@ -202,10 +234,34 @@ class FunctionsTable:
FunctionModel.model_validate(function) for function in functions
]
def get_function_list(
self, db: Optional[Session] = None
) -> list[FunctionUserResponse]:
with get_db_context(db) as db:
functions = db.query(Function).order_by(Function.updated_at.desc()).all()
user_ids = list(set(func.user_id for func in functions))
users = Users.get_users_by_user_ids(user_ids, db=db) if user_ids else []
users_dict = {user.id: user for user in users}
return [
FunctionUserResponse.model_validate(
{
**FunctionModel.model_validate(func).model_dump(),
"user": (
users_dict.get(func.user_id).model_dump()
if func.user_id in users_dict
else None
),
}
)
for func in functions
]
def get_functions_by_type(
self, type: str, active_only=False
self, type: str, active_only=False, db: Optional[Session] = None
) -> list[FunctionModel]:
with get_db() as db:
with get_db_context(db) as db:
if active_only:
return [
FunctionModel.model_validate(function)
@@ -219,8 +275,10 @@ class FunctionsTable:
for function in db.query(Function).filter_by(type=type).all()
]
def get_global_filter_functions(self) -> list[FunctionModel]:
with get_db() as db:
def get_global_filter_functions(
self, db: Optional[Session] = None
) -> list[FunctionModel]:
with get_db_context(db) as db:
return [
FunctionModel.model_validate(function)
for function in db.query(Function)
@@ -228,8 +286,10 @@ class FunctionsTable:
.all()
]
def get_global_action_functions(self) -> list[FunctionModel]:
with get_db() as db:
def get_global_action_functions(
self, db: Optional[Session] = None
) -> list[FunctionModel]:
with get_db_context(db) as db:
return [
FunctionModel.model_validate(function)
for function in db.query(Function)
@@ -237,8 +297,10 @@ class FunctionsTable:
.all()
]
def get_function_valves_by_id(self, id: str) -> Optional[dict]:
with get_db() as db:
def get_function_valves_by_id(
self, id: str, db: Optional[Session] = None
) -> Optional[dict]:
with get_db_context(db) as db:
try:
function = db.get(Function, id)
return function.valves if function.valves else {}
@@ -247,24 +309,47 @@ class FunctionsTable:
return None
def update_function_valves_by_id(
self, id: str, valves: dict
self, id: str, valves: dict, db: Optional[Session] = None
) -> Optional[FunctionValves]:
with get_db() as db:
with get_db_context(db) as db:
try:
function = db.get(Function, id)
function.valves = valves
function.updated_at = int(time.time())
db.commit()
db.refresh(function)
return self.get_function_by_id(id)
return FunctionModel.model_validate(function)
except Exception:
return None
def update_function_metadata_by_id(
self, id: str, metadata: dict, db: Optional[Session] = None
) -> Optional[FunctionModel]:
with get_db_context(db) as db:
try:
function = db.get(Function, id)
if function:
if function.meta:
function.meta = {**function.meta, **metadata}
else:
function.meta = metadata
function.updated_at = int(time.time())
db.commit()
db.refresh(function)
return FunctionModel.model_validate(function)
else:
return None
except Exception as e:
log.exception(f"Error updating function metadata by id {id}: {e}")
return None
def get_user_valves_by_id_and_user_id(
self, id: str, user_id: str
self, id: str, user_id: str, db: Optional[Session] = None
) -> Optional[dict]:
try:
user = Users.get_user_by_id(user_id)
user = Users.get_user_by_id(user_id, db=db)
user_settings = user.settings.model_dump() if user.settings else {}
# Check if user has "functions" and "valves" settings
@@ -279,10 +364,10 @@ class FunctionsTable:
return None
def update_user_valves_by_id_and_user_id(
self, id: str, user_id: str, valves: dict
self, id: str, user_id: str, valves: dict, db: Optional[Session] = None
) -> Optional[dict]:
try:
user = Users.get_user_by_id(user_id)
user = Users.get_user_by_id(user_id, db=db)
user_settings = user.settings.model_dump() if user.settings else {}
# Check if user has "functions" and "valves" settings
@@ -294,7 +379,7 @@ class FunctionsTable:
user_settings["functions"]["valves"][id] = valves
# Update the user settings in the database
Users.update_user_by_id(user_id, {"settings": user_settings})
Users.update_user_by_id(user_id, {"settings": user_settings}, db=db)
return user_settings["functions"]["valves"][id]
except Exception as e:
@@ -303,8 +388,10 @@ class FunctionsTable:
)
return None
def update_function_by_id(self, id: str, updated: dict) -> Optional[FunctionModel]:
with get_db() as db:
def update_function_by_id(
self, id: str, updated: dict, db: Optional[Session] = None
) -> Optional[FunctionModel]:
with get_db_context(db) as db:
try:
db.query(Function).filter_by(id=id).update(
{
@@ -313,12 +400,13 @@ class FunctionsTable:
}
)
db.commit()
return self.get_function_by_id(id)
function = db.get(Function, id)
return FunctionModel.model_validate(function) if function else None
except Exception:
return None
def deactivate_all_functions(self) -> Optional[bool]:
with get_db() as db:
def deactivate_all_functions(self, db: Optional[Session] = None) -> Optional[bool]:
with get_db_context(db) as db:
try:
db.query(Function).update(
{
@@ -331,8 +419,8 @@ class FunctionsTable:
except Exception:
return None
def delete_function_by_id(self, id: str) -> bool:
with get_db() as db:
def delete_function_by_id(self, id: str, db: Optional[Session] = None) -> bool:
with get_db_context(db) as db:
try:
db.query(Function).filter_by(id=id).delete()
db.commit()
+441 -107
View File
@@ -4,18 +4,29 @@ import time
from typing import Optional
import uuid
from open_webui.internal.db import Base, get_db
from open_webui.env import SRC_LOG_LEVELS
from sqlalchemy.orm import Session
from open_webui.internal.db import Base, JSONField, get_db, get_db_context
from open_webui.env import DEFAULT_GROUP_SHARE_PERMISSION
from open_webui.models.files import FileMetadataResponse
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Column, String, Text, JSON, func
from sqlalchemy import (
BigInteger,
Column,
String,
Text,
JSON,
and_,
func,
ForeignKey,
cast,
or_,
select,
)
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
####################
# UserGroup DB Schema
@@ -35,14 +46,12 @@ class Group(Base):
meta = Column(JSON, nullable=True)
permissions = Column(JSON, nullable=True)
user_ids = Column(JSON, nullable=True)
created_at = Column(BigInteger)
updated_at = Column(BigInteger)
class GroupModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: str
user_id: str
@@ -53,52 +62,95 @@ class GroupModel(BaseModel):
meta: Optional[dict] = None
permissions: Optional[dict] = None
user_ids: list[str] = []
created_at: int # timestamp in epoch
updated_at: int # timestamp in epoch
model_config = ConfigDict(from_attributes=True)
class GroupMember(Base):
__tablename__ = "group_member"
id = Column(Text, unique=True, primary_key=True)
group_id = Column(
Text,
ForeignKey("group.id", ondelete="CASCADE"),
nullable=False,
)
user_id = Column(Text, nullable=False)
created_at = Column(BigInteger, nullable=True)
updated_at = Column(BigInteger, nullable=True)
class GroupMemberModel(BaseModel):
id: str
group_id: str
user_id: str
created_at: Optional[int] = None # timestamp in epoch
updated_at: Optional[int] = None # timestamp in epoch
####################
# Forms
####################
class GroupResponse(BaseModel):
class GroupResponse(GroupModel):
member_count: Optional[int] = None
class GroupInfoResponse(BaseModel):
id: str
user_id: str
name: str
description: str
permissions: Optional[dict] = None
data: Optional[dict] = None
meta: Optional[dict] = None
user_ids: list[str] = []
created_at: int # timestamp in epoch
updated_at: int # timestamp in epoch
member_count: Optional[int] = None
created_at: int
updated_at: int
class GroupForm(BaseModel):
name: str
description: str
permissions: Optional[dict] = None
data: Optional[dict] = None
class UserIdsForm(BaseModel):
user_ids: Optional[list[str]] = None
class GroupUpdateForm(GroupForm, UserIdsForm):
class GroupUpdateForm(GroupForm):
pass
class GroupListResponse(BaseModel):
items: list[GroupResponse] = []
total: int = 0
class GroupTable:
def _ensure_default_share_config(self, group_data: dict) -> dict:
"""Ensure the group data dict has a default share config if not already set."""
if "data" not in group_data or group_data["data"] is None:
group_data["data"] = {}
if "config" not in group_data["data"]:
group_data["data"]["config"] = {}
if "share" not in group_data["data"]["config"]:
group_data["data"]["config"]["share"] = DEFAULT_GROUP_SHARE_PERMISSION
return group_data
def insert_new_group(
self, user_id: str, form_data: GroupForm
self, user_id: str, form_data: GroupForm, db: Optional[Session] = None
) -> Optional[GroupModel]:
with get_db() as db:
with get_db_context(db) as db:
group_data = self._ensure_default_share_config(
form_data.model_dump(exclude_none=True)
)
group = GroupModel(
**{
**form_data.model_dump(exclude_none=True),
**group_data,
"id": str(uuid.uuid4()),
"user_id": user_id,
"created_at": int(time.time()),
@@ -119,48 +171,274 @@ class GroupTable:
except Exception:
return None
def get_groups(self) -> list[GroupModel]:
with get_db() as db:
def get_all_groups(self, db: Optional[Session] = None) -> list[GroupModel]:
with get_db_context(db) as db:
groups = db.query(Group).order_by(Group.updated_at.desc()).all()
return [GroupModel.model_validate(group) for group in groups]
def get_groups(self, filter, db: Optional[Session] = None) -> list[GroupResponse]:
with get_db_context(db) as db:
member_count = (
select(func.count(GroupMember.user_id))
.where(GroupMember.group_id == Group.id)
.correlate(Group)
.scalar_subquery()
.label("member_count")
)
query = db.query(Group, member_count)
if filter:
if "query" in filter:
query = query.filter(Group.name.ilike(f"%{filter['query']}%"))
# When share filter is present, member check is handled in the share logic
if "share" in filter:
share_value = filter["share"]
member_id = filter.get("member_id")
json_share = Group.data["config"]["share"]
json_share_str = json_share.as_string()
json_share_lower = func.lower(json_share_str)
if share_value:
anyone_can_share = or_(
Group.data.is_(None),
json_share_str.is_(None),
json_share_lower == "true",
json_share_lower == "1", # Handle SQLite boolean true
)
if member_id:
member_groups_select = select(GroupMember.group_id).where(
GroupMember.user_id == member_id
)
members_only_and_is_member = and_(
json_share_lower == "members",
Group.id.in_(member_groups_select),
)
query = query.filter(
or_(anyone_can_share, members_only_and_is_member)
)
else:
query = query.filter(anyone_can_share)
else:
query = query.filter(
and_(Group.data.isnot(None), json_share_lower == "false")
)
else:
# Only apply member_id filter when share filter is NOT present
if "member_id" in filter:
query = query.filter(
Group.id.in_(
select(GroupMember.group_id).where(
GroupMember.user_id == filter["member_id"]
)
)
)
results = query.order_by(Group.updated_at.desc()).all()
return [
GroupModel.model_validate(group)
for group in db.query(Group).order_by(Group.updated_at.desc()).all()
GroupResponse.model_validate(
{
**GroupModel.model_validate(group).model_dump(),
"member_count": count or 0,
}
)
for group, count in results
]
def get_groups_by_member_id(self, user_id: str) -> list[GroupModel]:
with get_db() as db:
def search_groups(
self,
filter: Optional[dict] = None,
skip: int = 0,
limit: int = 30,
db: Optional[Session] = None,
) -> GroupListResponse:
with get_db_context(db) as db:
query = db.query(Group)
if filter:
if "query" in filter:
query = query.filter(Group.name.ilike(f"%{filter['query']}%"))
if "member_id" in filter:
query = query.filter(
Group.id.in_(
select(GroupMember.group_id).where(
GroupMember.user_id == filter["member_id"]
)
)
)
if "share" in filter:
share_value = filter["share"]
query = query.filter(
Group.data.op("->>")("share") == str(share_value)
)
total = query.count()
member_count = (
select(func.count(GroupMember.user_id))
.where(GroupMember.group_id == Group.id)
.correlate(Group)
.scalar_subquery()
.label("member_count")
)
results = (
query.add_columns(member_count)
.order_by(Group.updated_at.desc())
.offset(skip)
.limit(limit)
.all()
)
return {
"items": [
GroupResponse.model_validate(
{
**GroupModel.model_validate(group).model_dump(),
"member_count": count or 0,
}
)
for group, count in results
],
"total": total,
}
def get_groups_by_member_id(
self, user_id: str, db: Optional[Session] = None
) -> list[GroupModel]:
with get_db_context(db) as db:
return [
GroupModel.model_validate(group)
for group in db.query(Group)
.filter(
func.json_array_length(Group.user_ids) > 0
) # Ensure array exists
.filter(
Group.user_ids.cast(String).like(f'%"{user_id}"%')
) # String-based check
.join(GroupMember, GroupMember.group_id == Group.id)
.filter(GroupMember.user_id == user_id)
.order_by(Group.updated_at.desc())
.all()
]
def get_group_by_id(self, id: str) -> Optional[GroupModel]:
def get_groups_by_member_ids(
self, user_ids: list[str], db: Optional[Session] = None
) -> dict[str, list[GroupModel]]:
"""Fetch groups for multiple users in a single query to avoid N+1."""
with get_db_context(db) as db:
# Query GroupMember joined with Group, filtering by user_ids
results = (
db.query(GroupMember.user_id, Group)
.join(Group, Group.id == GroupMember.group_id)
.filter(GroupMember.user_id.in_(user_ids))
.order_by(Group.updated_at.desc())
.all()
)
# Group groups by user_id
user_groups: dict[str, list[GroupModel]] = {uid: [] for uid in user_ids}
for user_id, group in results:
user_groups[user_id].append(GroupModel.model_validate(group))
return user_groups
def get_group_by_id(
self, id: str, db: Optional[Session] = None
) -> Optional[GroupModel]:
try:
with get_db() as db:
with get_db_context(db) as db:
group = db.query(Group).filter_by(id=id).first()
return GroupModel.model_validate(group) if group else None
except Exception:
return None
def get_group_user_ids_by_id(self, id: str) -> Optional[str]:
group = self.get_group_by_id(id)
if group:
return group.user_ids
else:
return None
def get_group_user_ids_by_id(
self, id: str, db: Optional[Session] = None
) -> list[str]:
with get_db_context(db) as db:
members = (
db.query(GroupMember.user_id).filter(GroupMember.group_id == id).all()
)
if not members:
return []
return [m[0] for m in members]
def get_group_user_ids_by_ids(
self, group_ids: list[str], db: Optional[Session] = None
) -> dict[str, list[str]]:
with get_db_context(db) as db:
members = (
db.query(GroupMember.group_id, GroupMember.user_id)
.filter(GroupMember.group_id.in_(group_ids))
.all()
)
group_user_ids: dict[str, list[str]] = {
group_id: [] for group_id in group_ids
}
for group_id, user_id in members:
group_user_ids[group_id].append(user_id)
return group_user_ids
def set_group_user_ids_by_id(
self, group_id: str, user_ids: list[str], db: Optional[Session] = None
) -> None:
with get_db_context(db) as db:
# Delete existing members
db.query(GroupMember).filter(GroupMember.group_id == group_id).delete()
# Insert new members
now = int(time.time())
new_members = [
GroupMember(
id=str(uuid.uuid4()),
group_id=group_id,
user_id=user_id,
created_at=now,
updated_at=now,
)
for user_id in user_ids
]
db.add_all(new_members)
db.commit()
def get_group_member_count_by_id(
self, id: str, db: Optional[Session] = None
) -> int:
with get_db_context(db) as db:
count = (
db.query(func.count(GroupMember.user_id))
.filter(GroupMember.group_id == id)
.scalar()
)
return count if count else 0
def get_group_member_counts_by_ids(
self, ids: list[str], db: Optional[Session] = None
) -> dict[str, int]:
if not ids:
return {}
with get_db_context(db) as db:
rows = (
db.query(GroupMember.group_id, func.count(GroupMember.user_id))
.filter(GroupMember.group_id.in_(ids))
.group_by(GroupMember.group_id)
.all()
)
return {group_id: count for group_id, count in rows}
def update_group_by_id(
self, id: str, form_data: GroupUpdateForm, overwrite: bool = False
self,
id: str,
form_data: GroupUpdateForm,
overwrite: bool = False,
db: Optional[Session] = None,
) -> Optional[GroupModel]:
try:
with get_db() as db:
with get_db_context(db) as db:
db.query(Group).filter_by(id=id).update(
{
**form_data.model_dump(exclude_none=True),
@@ -168,22 +446,22 @@ class GroupTable:
}
)
db.commit()
return self.get_group_by_id(id=id)
return self.get_group_by_id(id=id, db=db)
except Exception as e:
log.exception(e)
return None
def delete_group_by_id(self, id: str) -> bool:
def delete_group_by_id(self, id: str, db: Optional[Session] = None) -> bool:
try:
with get_db() as db:
with get_db_context(db) as db:
db.query(Group).filter_by(id=id).delete()
db.commit()
return True
except Exception:
return False
def delete_all_groups(self) -> bool:
with get_db() as db:
def delete_all_groups(self, db: Optional[Session] = None) -> bool:
with get_db_context(db) as db:
try:
db.query(Group).delete()
db.commit()
@@ -192,36 +470,47 @@ class GroupTable:
except Exception:
return False
def remove_user_from_all_groups(self, user_id: str) -> bool:
with get_db() as db:
def remove_user_from_all_groups(
self, user_id: str, db: Optional[Session] = None
) -> bool:
with get_db_context(db) as db:
try:
groups = self.get_groups_by_member_id(user_id)
# Find all groups the user belongs to
groups = (
db.query(Group)
.join(GroupMember, GroupMember.group_id == Group.id)
.filter(GroupMember.user_id == user_id)
.all()
)
# Remove the user from each group
for group in groups:
group.user_ids.remove(user_id)
db.query(Group).filter_by(id=group.id).update(
{
"user_ids": group.user_ids,
"updated_at": int(time.time()),
}
)
db.commit()
db.query(GroupMember).filter(
GroupMember.group_id == group.id, GroupMember.user_id == user_id
).delete()
db.query(Group).filter_by(id=group.id).update(
{"updated_at": int(time.time())}
)
db.commit()
return True
except Exception:
db.rollback()
return False
def create_groups_by_group_names(
self, user_id: str, group_names: list[str]
self, user_id: str, group_names: list[str], db: Optional[Session] = None
) -> list[GroupModel]:
# check for existing groups
existing_groups = self.get_groups()
existing_groups = self.get_all_groups(db=db)
existing_group_names = {group.name for group in existing_groups}
new_groups = []
with get_db() as db:
with get_db_context(db) as db:
for group_name in group_names:
if group_name not in existing_group_names:
new_group = GroupModel(
@@ -229,6 +518,11 @@ class GroupTable:
user_id=user_id,
name=group_name,
description="",
data={
"config": {
"share": DEFAULT_GROUP_SHARE_PERMISSION,
}
},
created_at=int(time.time()),
updated_at=int(time.time()),
)
@@ -243,96 +537,136 @@ class GroupTable:
continue
return new_groups
def sync_groups_by_group_names(self, user_id: str, group_names: list[str]) -> bool:
with get_db() as db:
def sync_groups_by_group_names(
self, user_id: str, group_names: list[str], db: Optional[Session] = None
) -> bool:
with get_db_context(db) as db:
try:
groups = db.query(Group).filter(Group.name.in_(group_names)).all()
group_ids = [group.id for group in groups]
now = int(time.time())
# Remove user from groups not in the new list
existing_groups = self.get_groups_by_member_id(user_id)
# 1. Groups that SHOULD contain the user
target_groups = (
db.query(Group).filter(Group.name.in_(group_names)).all()
)
target_group_ids = {g.id for g in target_groups}
for group in existing_groups:
if group.id not in group_ids:
group.user_ids.remove(user_id)
db.query(Group).filter_by(id=group.id).update(
{
"user_ids": group.user_ids,
"updated_at": int(time.time()),
}
# 2. Groups the user is CURRENTLY in
existing_group_ids = {
g.id
for g in db.query(Group)
.join(GroupMember, GroupMember.group_id == Group.id)
.filter(GroupMember.user_id == user_id)
.all()
}
# 3. Determine adds + removals
groups_to_add = target_group_ids - existing_group_ids
groups_to_remove = existing_group_ids - target_group_ids
# 4. Remove in one bulk delete
if groups_to_remove:
db.query(GroupMember).filter(
GroupMember.user_id == user_id,
GroupMember.group_id.in_(groups_to_remove),
).delete(synchronize_session=False)
db.query(Group).filter(Group.id.in_(groups_to_remove)).update(
{"updated_at": now}, synchronize_session=False
)
# 5. Bulk insert missing memberships
for group_id in groups_to_add:
db.add(
GroupMember(
id=str(uuid.uuid4()),
group_id=group_id,
user_id=user_id,
created_at=now,
updated_at=now,
)
)
# Add user to new groups
for group in groups:
if user_id not in group.user_ids:
group.user_ids.append(user_id)
db.query(Group).filter_by(id=group.id).update(
{
"user_ids": group.user_ids,
"updated_at": int(time.time()),
}
)
if groups_to_add:
db.query(Group).filter(Group.id.in_(groups_to_add)).update(
{"updated_at": now}, synchronize_session=False
)
db.commit()
return True
except Exception as e:
log.exception(e)
db.rollback()
return False
def add_users_to_group(
self, id: str, user_ids: Optional[list[str]] = None
self,
id: str,
user_ids: Optional[list[str]] = None,
db: Optional[Session] = None,
) -> Optional[GroupModel]:
try:
with get_db() as db:
with get_db_context(db) as db:
group = db.query(Group).filter_by(id=id).first()
if not group:
return None
group_user_ids = group.user_ids
if not group_user_ids or not isinstance(group_user_ids, list):
group_user_ids = []
now = int(time.time())
group_user_ids = list(set(group_user_ids)) # Deduplicate
for user_id in user_ids or []:
try:
db.add(
GroupMember(
id=str(uuid.uuid4()),
group_id=id,
user_id=user_id,
created_at=now,
updated_at=now,
)
)
db.flush() # Detect unique constraint violation early
except Exception:
db.rollback() # Clear failed INSERT
db.begin() # Start a new transaction
continue # Duplicate → ignore
for user_id in user_ids:
if user_id not in group_user_ids:
group_user_ids.append(user_id)
group.user_ids = group_user_ids
group.updated_at = int(time.time())
group.updated_at = now
db.commit()
db.refresh(group)
return GroupModel.model_validate(group)
except Exception as e:
log.exception(e)
return None
def remove_users_from_group(
self, id: str, user_ids: Optional[list[str]] = None
self,
id: str,
user_ids: Optional[list[str]] = None,
db: Optional[Session] = None,
) -> Optional[GroupModel]:
try:
with get_db() as db:
with get_db_context(db) as db:
group = db.query(Group).filter_by(id=id).first()
if not group:
return None
group_user_ids = group.user_ids
if not group_user_ids or not isinstance(group_user_ids, list):
if not user_ids:
return GroupModel.model_validate(group)
group_user_ids = list(set(group_user_ids)) # Deduplicate
# Remove users from group_member in batch
db.query(GroupMember).filter(
GroupMember.group_id == id, GroupMember.user_id.in_(user_ids)
).delete(synchronize_session=False)
for user_id in user_ids:
if user_id in group_user_ids:
group_user_ids.remove(user_id)
group.user_ids = group_user_ids
# Update group timestamp
group.updated_at = int(time.time())
db.commit()
db.refresh(group)
return GroupModel.model_validate(group)
except Exception as e:
log.exception(e)
return None
+563 -70
View File
@@ -4,21 +4,33 @@ import time
from typing import Optional
import uuid
from open_webui.internal.db import Base, get_db
from open_webui.env import SRC_LOG_LEVELS
from sqlalchemy.orm import Session
from open_webui.internal.db import Base, JSONField, get_db, get_db_context
from open_webui.models.files import FileMetadataResponse
from open_webui.models.files import (
File,
FileModel,
FileMetadataResponse,
FileModelResponse,
)
from open_webui.models.groups import Groups
from open_webui.models.users import Users, UserResponse
from open_webui.models.users import User, UserModel, Users, UserResponse
from open_webui.models.access_grants import AccessGrantModel, AccessGrants
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Column, String, Text, JSON
from open_webui.utils.access_control import has_access
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy import (
BigInteger,
Column,
ForeignKey,
String,
Text,
JSON,
UniqueConstraint,
or_,
)
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
####################
# Knowledge DB Schema
@@ -34,26 +46,8 @@ class Knowledge(Base):
name = Column(Text)
description = Column(Text)
data = Column(JSON, nullable=True)
meta = Column(JSON, nullable=True)
access_control = Column(JSON, nullable=True) # Controls data access levels.
# Defines access control rules for this entry.
# - `None`: Public access, available to all users with the "user" role.
# - `{}`: Private access, restricted exclusively to the owner.
# - Custom permissions: Specific access control for reading and writing;
# Can specify group or user-level restrictions:
# {
# "read": {
# "group_ids": ["group_id1", "group_id2"],
# "user_ids": ["user_id1", "user_id2"]
# },
# "write": {
# "group_ids": ["group_id1", "group_id2"],
# "user_ids": ["user_id1", "user_id2"]
# }
# }
created_at = Column(BigInteger)
updated_at = Column(BigInteger)
@@ -67,20 +61,50 @@ class KnowledgeModel(BaseModel):
name: str
description: str
data: Optional[dict] = None
meta: Optional[dict] = None
access_control: Optional[dict] = None
access_grants: list[AccessGrantModel] = Field(default_factory=list)
created_at: int # timestamp in epoch
updated_at: int # timestamp in epoch
class KnowledgeFile(Base):
__tablename__ = "knowledge_file"
id = Column(Text, unique=True, primary_key=True)
knowledge_id = Column(
Text, ForeignKey("knowledge.id", ondelete="CASCADE"), nullable=False
)
file_id = Column(Text, ForeignKey("file.id", ondelete="CASCADE"), nullable=False)
user_id = Column(Text, nullable=False)
created_at = Column(BigInteger, nullable=False)
updated_at = Column(BigInteger, nullable=False)
__table_args__ = (
UniqueConstraint(
"knowledge_id", "file_id", name="uq_knowledge_file_knowledge_file"
),
)
class KnowledgeFileModel(BaseModel):
id: str
knowledge_id: str
file_id: str
user_id: str
created_at: int # timestamp in epoch
updated_at: int # timestamp in epoch
model_config = ConfigDict(from_attributes=True)
####################
# Forms
####################
class KnowledgeUserModel(KnowledgeModel):
user: Optional[UserResponse] = None
@@ -90,53 +114,96 @@ class KnowledgeResponse(KnowledgeModel):
class KnowledgeUserResponse(KnowledgeUserModel):
files: Optional[list[FileMetadataResponse | dict]] = None
pass
class KnowledgeForm(BaseModel):
name: str
description: str
data: Optional[dict] = None
access_control: Optional[dict] = None
access_grants: Optional[list[dict]] = None
class FileUserResponse(FileModelResponse):
user: Optional[UserResponse] = None
class KnowledgeListResponse(BaseModel):
items: list[KnowledgeUserModel]
total: int
class KnowledgeFileListResponse(BaseModel):
items: list[FileUserResponse]
total: int
class KnowledgeTable:
def _get_access_grants(
self, knowledge_id: str, db: Optional[Session] = None
) -> list[AccessGrantModel]:
return AccessGrants.get_grants_by_resource("knowledge", knowledge_id, db=db)
def _to_knowledge_model(
self,
knowledge: Knowledge,
access_grants: Optional[list[AccessGrantModel]] = None,
db: Optional[Session] = None,
) -> KnowledgeModel:
knowledge_data = KnowledgeModel.model_validate(knowledge).model_dump(
exclude={"access_grants"}
)
knowledge_data["access_grants"] = (
access_grants
if access_grants is not None
else self._get_access_grants(knowledge_data["id"], db=db)
)
return KnowledgeModel.model_validate(knowledge_data)
def insert_new_knowledge(
self, user_id: str, form_data: KnowledgeForm
self, user_id: str, form_data: KnowledgeForm, db: Optional[Session] = None
) -> Optional[KnowledgeModel]:
with get_db() as db:
with get_db_context(db) as db:
knowledge = KnowledgeModel(
**{
**form_data.model_dump(),
**form_data.model_dump(exclude={"access_grants"}),
"id": str(uuid.uuid4()),
"user_id": user_id,
"created_at": int(time.time()),
"updated_at": int(time.time()),
"access_grants": [],
}
)
try:
result = Knowledge(**knowledge.model_dump())
result = Knowledge(**knowledge.model_dump(exclude={"access_grants"}))
db.add(result)
db.commit()
db.refresh(result)
AccessGrants.set_access_grants(
"knowledge", result.id, form_data.access_grants, db=db
)
if result:
return KnowledgeModel.model_validate(result)
return self._to_knowledge_model(result, db=db)
else:
return None
except Exception:
return None
def get_knowledge_bases(self) -> list[KnowledgeUserModel]:
with get_db() as db:
def get_knowledge_bases(
self, skip: int = 0, limit: int = 30, db: Optional[Session] = None
) -> list[KnowledgeUserModel]:
with get_db_context(db) as db:
all_knowledge = (
db.query(Knowledge).order_by(Knowledge.updated_at.desc()).all()
)
user_ids = list(set(knowledge.user_id for knowledge in all_knowledge))
knowledge_ids = [knowledge.id for knowledge in all_knowledge]
users = Users.get_users_by_user_ids(user_ids) if user_ids else []
users = Users.get_users_by_user_ids(user_ids, db=db) if user_ids else []
users_dict = {user.id: user for user in users}
grants_map = AccessGrants.get_grants_by_resources(
"knowledge", knowledge_ids, db=db
)
knowledge_bases = []
for knowledge in all_knowledge:
@@ -144,68 +211,490 @@ class KnowledgeTable:
knowledge_bases.append(
KnowledgeUserModel.model_validate(
{
**KnowledgeModel.model_validate(knowledge).model_dump(),
**self._to_knowledge_model(
knowledge,
access_grants=grants_map.get(knowledge.id, []),
db=db,
).model_dump(),
"user": user.model_dump() if user else None,
}
)
)
return knowledge_bases
def check_access_by_user_id(self, id, user_id, permission="write") -> bool:
knowledge = self.get_knowledge_by_id(id)
def search_knowledge_bases(
self,
user_id: str,
filter: dict,
skip: int = 0,
limit: int = 30,
db: Optional[Session] = None,
) -> KnowledgeListResponse:
try:
with get_db_context(db) as db:
query = db.query(Knowledge, User).outerjoin(
User, User.id == Knowledge.user_id
)
if filter:
query_key = filter.get("query")
if query_key:
query = query.filter(
or_(
Knowledge.name.ilike(f"%{query_key}%"),
Knowledge.description.ilike(f"%{query_key}%"),
User.name.ilike(f"%{query_key}%"),
User.email.ilike(f"%{query_key}%"),
User.username.ilike(f"%{query_key}%"),
)
)
view_option = filter.get("view_option")
if view_option == "created":
query = query.filter(Knowledge.user_id == user_id)
elif view_option == "shared":
query = query.filter(Knowledge.user_id != user_id)
query = AccessGrants.has_permission_filter(
db=db,
query=query,
DocumentModel=Knowledge,
filter=filter,
resource_type="knowledge",
permission="read",
)
query = query.order_by(Knowledge.updated_at.desc(), Knowledge.id.asc())
total = query.count()
if skip:
query = query.offset(skip)
if limit:
query = query.limit(limit)
items = query.all()
knowledge_ids = [kb.id for kb, _ in items]
grants_map = AccessGrants.get_grants_by_resources(
"knowledge", knowledge_ids, db=db
)
knowledge_bases = []
for knowledge_base, user in items:
knowledge_bases.append(
KnowledgeUserModel.model_validate(
{
**self._to_knowledge_model(
knowledge_base,
access_grants=grants_map.get(knowledge_base.id, []),
db=db,
).model_dump(),
"user": (
UserModel.model_validate(user).model_dump()
if user
else None
),
}
)
)
return KnowledgeListResponse(items=knowledge_bases, total=total)
except Exception as e:
print(e)
return KnowledgeListResponse(items=[], total=0)
def search_knowledge_files(
self, filter: dict, skip: int = 0, limit: int = 30, db: Optional[Session] = None
) -> KnowledgeFileListResponse:
"""
Scalable version: search files across all knowledge bases the user has
READ access to, without loading all KBs or using large IN() lists.
"""
try:
with get_db_context(db) as db:
# Base query: join Knowledge → KnowledgeFile → File
query = (
db.query(File, User, Knowledge)
.join(KnowledgeFile, File.id == KnowledgeFile.file_id)
.join(Knowledge, KnowledgeFile.knowledge_id == Knowledge.id)
.outerjoin(User, User.id == KnowledgeFile.user_id)
)
# Apply access-control directly to the joined query
# This makes the database handle filtering, even with 10k+ KBs
query = AccessGrants.has_permission_filter(
db=db,
query=query,
DocumentModel=Knowledge,
filter=filter,
resource_type="knowledge",
permission="read",
)
# Apply filename search
if filter:
q = filter.get("query")
if q:
query = query.filter(File.filename.ilike(f"%{q}%"))
# Order by file changes
query = query.order_by(File.updated_at.desc(), File.id.asc())
# Count before pagination
total = query.count()
if skip:
query = query.offset(skip)
if limit:
query = query.limit(limit)
rows = query.all()
items = []
for file, user, knowledge in rows:
items.append(
FileUserResponse(
**FileModel.model_validate(file).model_dump(),
user=(
UserResponse(
**UserModel.model_validate(user).model_dump()
)
if user
else None
),
collection=self._to_knowledge_model(
knowledge, db=db
).model_dump(),
)
)
return KnowledgeFileListResponse(items=items, total=total)
except Exception as e:
print("search_knowledge_files error:", e)
return KnowledgeFileListResponse(items=[], total=0)
def check_access_by_user_id(
self, id, user_id, permission="write", db: Optional[Session] = None
) -> bool:
knowledge = self.get_knowledge_by_id(id, db=db)
if not knowledge:
return False
if knowledge.user_id == user_id:
return True
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user_id)}
return has_access(user_id, permission, knowledge.access_control, user_group_ids)
user_group_ids = {
group.id for group in Groups.get_groups_by_member_id(user_id, db=db)
}
return AccessGrants.has_access(
user_id=user_id,
resource_type="knowledge",
resource_id=knowledge.id,
permission=permission,
user_group_ids=user_group_ids,
db=db,
)
def get_knowledge_bases_by_user_id(
self, user_id: str, permission: str = "write"
self, user_id: str, permission: str = "write", db: Optional[Session] = None
) -> list[KnowledgeUserModel]:
knowledge_bases = self.get_knowledge_bases()
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user_id)}
knowledge_bases = self.get_knowledge_bases(db=db)
user_group_ids = {
group.id for group in Groups.get_groups_by_member_id(user_id, db=db)
}
return [
knowledge_base
for knowledge_base in knowledge_bases
if knowledge_base.user_id == user_id
or has_access(
user_id, permission, knowledge_base.access_control, user_group_ids
or AccessGrants.has_access(
user_id=user_id,
resource_type="knowledge",
resource_id=knowledge_base.id,
permission=permission,
user_group_ids=user_group_ids,
db=db,
)
]
def get_knowledge_by_id(self, id: str) -> Optional[KnowledgeModel]:
def get_knowledge_by_id(
self, id: str, db: Optional[Session] = None
) -> Optional[KnowledgeModel]:
try:
with get_db() as db:
with get_db_context(db) as db:
knowledge = db.query(Knowledge).filter_by(id=id).first()
return KnowledgeModel.model_validate(knowledge) if knowledge else None
return self._to_knowledge_model(knowledge, db=db) if knowledge else None
except Exception:
return None
def update_knowledge_by_id(
self, id: str, form_data: KnowledgeForm, overwrite: bool = False
def get_knowledge_by_id_and_user_id(
self, id: str, user_id: str, db: Optional[Session] = None
) -> Optional[KnowledgeModel]:
knowledge = self.get_knowledge_by_id(id, db=db)
if not knowledge:
return None
if knowledge.user_id == user_id:
return knowledge
user_group_ids = {
group.id for group in Groups.get_groups_by_member_id(user_id, db=db)
}
if AccessGrants.has_access(
user_id=user_id,
resource_type="knowledge",
resource_id=knowledge.id,
permission="write",
user_group_ids=user_group_ids,
db=db,
):
return knowledge
return None
def get_knowledges_by_file_id(
self, file_id: str, db: Optional[Session] = None
) -> list[KnowledgeModel]:
try:
with get_db_context(db) as db:
knowledges = (
db.query(Knowledge)
.join(KnowledgeFile, Knowledge.id == KnowledgeFile.knowledge_id)
.filter(KnowledgeFile.file_id == file_id)
.all()
)
knowledge_ids = [k.id for k in knowledges]
grants_map = AccessGrants.get_grants_by_resources(
"knowledge", knowledge_ids, db=db
)
return [
self._to_knowledge_model(
knowledge,
access_grants=grants_map.get(knowledge.id, []),
db=db,
)
for knowledge in knowledges
]
except Exception:
return []
def search_files_by_id(
self,
knowledge_id: str,
user_id: str,
filter: dict,
skip: int = 0,
limit: int = 30,
db: Optional[Session] = None,
) -> KnowledgeFileListResponse:
try:
with get_db_context(db) as db:
query = (
db.query(File, User)
.join(KnowledgeFile, File.id == KnowledgeFile.file_id)
.outerjoin(User, User.id == KnowledgeFile.user_id)
.filter(KnowledgeFile.knowledge_id == knowledge_id)
)
# Default sort: updated_at descending
primary_sort = File.updated_at.desc()
if filter:
query_key = filter.get("query")
if query_key:
query = query.filter(or_(File.filename.ilike(f"%{query_key}%")))
view_option = filter.get("view_option")
if view_option == "created":
query = query.filter(KnowledgeFile.user_id == user_id)
elif view_option == "shared":
query = query.filter(KnowledgeFile.user_id != user_id)
order_by = filter.get("order_by")
direction = filter.get("direction")
is_asc = direction == "asc"
if order_by == "name":
primary_sort = (
File.filename.asc() if is_asc else File.filename.desc()
)
elif order_by == "created_at":
primary_sort = (
File.created_at.asc() if is_asc else File.created_at.desc()
)
elif order_by == "updated_at":
primary_sort = (
File.updated_at.asc() if is_asc else File.updated_at.desc()
)
# Apply sort with secondary key for deterministic pagination
query = query.order_by(primary_sort, File.id.asc())
# Count BEFORE pagination
total = query.count()
if skip:
query = query.offset(skip)
if limit:
query = query.limit(limit)
items = query.all()
files = []
for file, user in items:
files.append(
FileUserResponse(
**FileModel.model_validate(file).model_dump(),
user=(
UserResponse(
**UserModel.model_validate(user).model_dump()
)
if user
else None
),
)
)
return KnowledgeFileListResponse(items=files, total=total)
except Exception as e:
print(e)
return KnowledgeFileListResponse(items=[], total=0)
def get_files_by_id(
self, knowledge_id: str, db: Optional[Session] = None
) -> list[FileModel]:
try:
with get_db_context(db) as db:
files = (
db.query(File)
.join(KnowledgeFile, File.id == KnowledgeFile.file_id)
.filter(KnowledgeFile.knowledge_id == knowledge_id)
.all()
)
return [FileModel.model_validate(file) for file in files]
except Exception:
return []
def get_file_metadatas_by_id(
self, knowledge_id: str, db: Optional[Session] = None
) -> list[FileMetadataResponse]:
try:
with get_db_context(db) as db:
files = self.get_files_by_id(knowledge_id, db=db)
return [FileMetadataResponse(**file.model_dump()) for file in files]
except Exception:
return []
def add_file_to_knowledge_by_id(
self,
knowledge_id: str,
file_id: str,
user_id: str,
db: Optional[Session] = None,
) -> Optional[KnowledgeFileModel]:
with get_db_context(db) as db:
knowledge_file = KnowledgeFileModel(
**{
"id": str(uuid.uuid4()),
"knowledge_id": knowledge_id,
"file_id": file_id,
"user_id": user_id,
"created_at": int(time.time()),
"updated_at": int(time.time()),
}
)
try:
result = KnowledgeFile(**knowledge_file.model_dump())
db.add(result)
db.commit()
db.refresh(result)
if result:
return KnowledgeFileModel.model_validate(result)
else:
return None
except Exception:
return None
def has_file(
self, knowledge_id: str, file_id: str, db: Optional[Session] = None
) -> bool:
"""Check whether a file belongs to a knowledge base."""
try:
with get_db_context(db) as db:
return (
db.query(KnowledgeFile)
.filter_by(knowledge_id=knowledge_id, file_id=file_id)
.first()
is not None
)
except Exception:
return False
def remove_file_from_knowledge_by_id(
self, knowledge_id: str, file_id: str, db: Optional[Session] = None
) -> bool:
try:
with get_db_context(db) as db:
db.query(KnowledgeFile).filter_by(
knowledge_id=knowledge_id, file_id=file_id
).delete()
db.commit()
return True
except Exception:
return False
def reset_knowledge_by_id(
self, id: str, db: Optional[Session] = None
) -> Optional[KnowledgeModel]:
try:
with get_db() as db:
knowledge = self.get_knowledge_by_id(id=id)
with get_db_context(db) as db:
# Delete all knowledge_file entries for this knowledge_id
db.query(KnowledgeFile).filter_by(knowledge_id=id).delete()
db.commit()
# Update the knowledge entry's updated_at timestamp
db.query(Knowledge).filter_by(id=id).update(
{
**form_data.model_dump(),
"updated_at": int(time.time()),
}
)
db.commit()
return self.get_knowledge_by_id(id=id)
return self.get_knowledge_by_id(id=id, db=db)
except Exception as e:
log.exception(e)
return None
def update_knowledge_by_id(
self,
id: str,
form_data: KnowledgeForm,
overwrite: bool = False,
db: Optional[Session] = None,
) -> Optional[KnowledgeModel]:
try:
with get_db_context(db) as db:
knowledge = self.get_knowledge_by_id(id=id, db=db)
db.query(Knowledge).filter_by(id=id).update(
{
**form_data.model_dump(exclude={"access_grants"}),
"updated_at": int(time.time()),
}
)
db.commit()
if form_data.access_grants is not None:
AccessGrants.set_access_grants(
"knowledge", id, form_data.access_grants, db=db
)
return self.get_knowledge_by_id(id=id, db=db)
except Exception as e:
log.exception(e)
return None
def update_knowledge_data_by_id(
self, id: str, data: dict
self, id: str, data: dict, db: Optional[Session] = None
) -> Optional[KnowledgeModel]:
try:
with get_db() as db:
knowledge = self.get_knowledge_by_id(id=id)
with get_db_context(db) as db:
knowledge = self.get_knowledge_by_id(id=id, db=db)
db.query(Knowledge).filter_by(id=id).update(
{
"data": data,
@@ -213,23 +702,27 @@ class KnowledgeTable:
}
)
db.commit()
return self.get_knowledge_by_id(id=id)
return self.get_knowledge_by_id(id=id, db=db)
except Exception as e:
log.exception(e)
return None
def delete_knowledge_by_id(self, id: str) -> bool:
def delete_knowledge_by_id(self, id: str, db: Optional[Session] = None) -> bool:
try:
with get_db() as db:
with get_db_context(db) as db:
AccessGrants.revoke_all_access("knowledge", id, db=db)
db.query(Knowledge).filter_by(id=id).delete()
db.commit()
return True
except Exception:
return False
def delete_all_knowledge(self) -> bool:
with get_db() as db:
def delete_all_knowledge(self, db: Optional[Session] = None) -> bool:
with get_db_context(db) as db:
try:
knowledge_ids = [row[0] for row in db.query(Knowledge.id).all()]
for knowledge_id in knowledge_ids:
AccessGrants.revoke_all_access("knowledge", knowledge_id, db=db)
db.query(Knowledge).delete()
db.commit()
+29 -17
View File
@@ -2,7 +2,8 @@ import time
import uuid
from typing import Optional
from open_webui.internal.db import Base, get_db
from sqlalchemy.orm import Session
from open_webui.internal.db import Base, get_db, get_db_context
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Column, String, Text
@@ -14,7 +15,7 @@ from sqlalchemy import BigInteger, Column, String, Text
class Memory(Base):
__tablename__ = "memory"
id = Column(String, primary_key=True)
id = Column(String, primary_key=True, unique=True)
user_id = Column(String)
content = Column(Text)
updated_at = Column(BigInteger)
@@ -41,8 +42,9 @@ class MemoriesTable:
self,
user_id: str,
content: str,
db: Optional[Session] = None,
) -> Optional[MemoryModel]:
with get_db() as db:
with get_db_context(db) as db:
id = str(uuid.uuid4())
memory = MemoryModel(
@@ -68,8 +70,9 @@ class MemoriesTable:
id: str,
user_id: str,
content: str,
db: Optional[Session] = None,
) -> Optional[MemoryModel]:
with get_db() as db:
with get_db_context(db) as db:
try:
memory = db.get(Memory, id)
if not memory or memory.user_id != user_id:
@@ -79,36 +82,41 @@ class MemoriesTable:
memory.updated_at = int(time.time())
db.commit()
return self.get_memory_by_id(id)
db.refresh(memory)
return MemoryModel.model_validate(memory)
except Exception:
return None
def get_memories(self) -> list[MemoryModel]:
with get_db() as db:
def get_memories(self, db: Optional[Session] = None) -> list[MemoryModel]:
with get_db_context(db) as db:
try:
memories = db.query(Memory).all()
return [MemoryModel.model_validate(memory) for memory in memories]
except Exception:
return None
def get_memories_by_user_id(self, user_id: str) -> list[MemoryModel]:
with get_db() as db:
def get_memories_by_user_id(
self, user_id: str, db: Optional[Session] = None
) -> list[MemoryModel]:
with get_db_context(db) as db:
try:
memories = db.query(Memory).filter_by(user_id=user_id).all()
return [MemoryModel.model_validate(memory) for memory in memories]
except Exception:
return None
def get_memory_by_id(self, id: str) -> Optional[MemoryModel]:
with get_db() as db:
def get_memory_by_id(
self, id: str, db: Optional[Session] = None
) -> Optional[MemoryModel]:
with get_db_context(db) as db:
try:
memory = db.get(Memory, id)
return MemoryModel.model_validate(memory)
except Exception:
return None
def delete_memory_by_id(self, id: str) -> bool:
with get_db() as db:
def delete_memory_by_id(self, id: str, db: Optional[Session] = None) -> bool:
with get_db_context(db) as db:
try:
db.query(Memory).filter_by(id=id).delete()
db.commit()
@@ -118,8 +126,10 @@ class MemoriesTable:
except Exception:
return False
def delete_memories_by_user_id(self, user_id: str) -> bool:
with get_db() as db:
def delete_memories_by_user_id(
self, user_id: str, db: Optional[Session] = None
) -> bool:
with get_db_context(db) as db:
try:
db.query(Memory).filter_by(user_id=user_id).delete()
db.commit()
@@ -128,8 +138,10 @@ class MemoriesTable:
except Exception:
return False
def delete_memory_by_id_and_user_id(self, id: str, user_id: str) -> bool:
with get_db() as db:
def delete_memory_by_id_and_user_id(
self, id: str, user_id: str, db: Optional[Session] = None
) -> bool:
with get_db_context(db) as db:
try:
memory = db.get(Memory, id)
if not memory or memory.user_id != user_id:
+406 -52
View File
@@ -3,11 +3,14 @@ import time
import uuid
from typing import Optional
from open_webui.internal.db import Base, get_db
from sqlalchemy.orm import Session
from open_webui.internal.db import Base, JSONField, get_db, get_db_context
from open_webui.models.tags import TagModel, Tag, Tags
from open_webui.models.users import Users, User, UserNameResponse
from open_webui.models.channels import Channels, ChannelMember
from pydantic import BaseModel, ConfigDict
from pydantic import BaseModel, ConfigDict, field_validator
from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON
from sqlalchemy import or_, func, select, and_, text
from sqlalchemy.sql import exists
@@ -19,7 +22,7 @@ from sqlalchemy.sql import exists
class MessageReaction(Base):
__tablename__ = "message_reaction"
id = Column(Text, primary_key=True)
id = Column(Text, primary_key=True, unique=True)
user_id = Column(Text)
message_id = Column(Text)
name = Column(Text)
@@ -38,13 +41,19 @@ class MessageReactionModel(BaseModel):
class Message(Base):
__tablename__ = "message"
id = Column(Text, primary_key=True)
id = Column(Text, primary_key=True, unique=True)
user_id = Column(Text)
channel_id = Column(Text, nullable=True)
reply_to_id = Column(Text, nullable=True)
parent_id = Column(Text, nullable=True)
# Pins
is_pinned = Column(Boolean, nullable=False, default=False)
pinned_at = Column(BigInteger, nullable=True)
pinned_by = Column(Text, nullable=True)
content = Column(Text)
data = Column(JSON, nullable=True)
meta = Column(JSON, nullable=True)
@@ -60,14 +69,20 @@ class MessageModel(BaseModel):
user_id: str
channel_id: Optional[str] = None
reply_to_id: Optional[str] = None
parent_id: Optional[str] = None
# Pins
is_pinned: bool = False
pinned_by: Optional[str] = None
pinned_at: Optional[int] = None # timestamp in epoch (time_ns)
content: str
data: Optional[dict] = None
meta: Optional[dict] = None
created_at: int # timestamp in epoch
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch (time_ns)
updated_at: int # timestamp in epoch (time_ns)
####################
@@ -76,7 +91,9 @@ class MessageModel(BaseModel):
class MessageForm(BaseModel):
temp_id: Optional[str] = None
content: str
reply_to_id: Optional[str] = None
parent_id: Optional[str] = None
data: Optional[dict] = None
meta: Optional[dict] = None
@@ -84,11 +101,36 @@ class MessageForm(BaseModel):
class Reactions(BaseModel):
name: str
user_ids: list[str]
users: list[dict]
count: int
class MessageResponse(MessageModel):
class MessageUserResponse(MessageModel):
user: Optional[UserNameResponse] = None
class MessageUserSlimResponse(MessageUserResponse):
data: bool | None = None
@field_validator("data", mode="before")
def convert_data_to_bool(cls, v):
# No data or not a dict → False
if not isinstance(v, dict):
return False
# True if ANY value in the dict is non-empty
return any(bool(val) for val in v.values())
class MessageReplyToResponse(MessageUserResponse):
reply_to_message: Optional[MessageUserSlimResponse] = None
class MessageWithReactionsResponse(MessageUserSlimResponse):
reactions: list[Reactions]
class MessageResponse(MessageReplyToResponse):
latest_reply_at: Optional[int]
reply_count: int
reactions: list[Reactions]
@@ -96,18 +138,28 @@ class MessageResponse(MessageModel):
class MessageTable:
def insert_new_message(
self, form_data: MessageForm, channel_id: str, user_id: str
self,
form_data: MessageForm,
channel_id: str,
user_id: str,
db: Optional[Session] = None,
) -> Optional[MessageModel]:
with get_db() as db:
id = str(uuid.uuid4())
with get_db_context(db) as db:
channel_member = Channels.join_channel(channel_id, user_id)
id = str(uuid.uuid4())
ts = int(time.time_ns())
message = MessageModel(
**{
"id": id,
"user_id": user_id,
"channel_id": channel_id,
"reply_to_id": form_data.reply_to_id,
"parent_id": form_data.parent_id,
"is_pinned": False,
"pinned_at": None,
"pinned_by": None,
"content": form_data.content,
"data": form_data.data,
"meta": form_data.meta,
@@ -115,52 +167,145 @@ class MessageTable:
"updated_at": ts,
}
)
result = Message(**message.model_dump())
db.add(result)
db.commit()
db.refresh(result)
return MessageModel.model_validate(result) if result else None
def get_message_by_id(self, id: str) -> Optional[MessageResponse]:
with get_db() as db:
def get_message_by_id(
self,
id: str,
include_thread_replies: Optional[bool] = True,
db: Optional[Session] = None,
) -> Optional[MessageResponse]:
with get_db_context(db) as db:
message = db.get(Message, id)
if not message:
return None
reactions = self.get_reactions_by_message_id(id)
replies = self.get_replies_by_message_id(id)
reply_to_message = (
self.get_message_by_id(
message.reply_to_id, include_thread_replies=False, db=db
)
if message.reply_to_id
else None
)
return MessageResponse(
**{
reactions = self.get_reactions_by_message_id(id, db=db)
thread_replies = []
if include_thread_replies:
thread_replies = self.get_thread_replies_by_message_id(id, db=db)
# Check if message was sent by webhook (webhook info in meta takes precedence)
webhook_info = message.meta.get("webhook") if message.meta else None
if webhook_info and webhook_info.get("id"):
# Look up webhook by ID to get current name
webhook = Channels.get_webhook_by_id(webhook_info.get("id"), db=db)
if webhook:
user_info = {
"id": webhook.id,
"name": webhook.name,
"role": "webhook",
}
else:
# Webhook was deleted, use placeholder
user_info = {
"id": webhook_info.get("id"),
"name": "Deleted Webhook",
"role": "webhook",
}
else:
user = Users.get_user_by_id(message.user_id, db=db)
user_info = user.model_dump() if user else None
return MessageResponse.model_validate(
{
**MessageModel.model_validate(message).model_dump(),
"latest_reply_at": replies[0].created_at if replies else None,
"reply_count": len(replies),
"user": user_info,
"reply_to_message": (
reply_to_message.model_dump() if reply_to_message else None
),
"latest_reply_at": (
thread_replies[0].created_at if thread_replies else None
),
"reply_count": len(thread_replies),
"reactions": reactions,
}
)
def get_replies_by_message_id(self, id: str) -> list[MessageModel]:
with get_db() as db:
def get_thread_replies_by_message_id(
self, id: str, db: Optional[Session] = None
) -> list[MessageReplyToResponse]:
with get_db_context(db) as db:
all_messages = (
db.query(Message)
.filter_by(parent_id=id)
.order_by(Message.created_at.desc())
.all()
)
return [MessageModel.model_validate(message) for message in all_messages]
def get_reply_user_ids_by_message_id(self, id: str) -> list[str]:
with get_db() as db:
messages = []
for message in all_messages:
reply_to_message = (
self.get_message_by_id(
message.reply_to_id, include_thread_replies=False, db=db
)
if message.reply_to_id
else None
)
webhook_info = message.meta.get("webhook") if message.meta else None
user_info = None
if webhook_info and webhook_info.get("id"):
webhook = Channels.get_webhook_by_id(webhook_info.get("id"), db=db)
if webhook:
user_info = {
"id": webhook.id,
"name": webhook.name,
"role": "webhook",
}
else:
user_info = {
"id": webhook_info.get("id"),
"name": "Deleted Webhook",
"role": "webhook",
}
messages.append(
MessageReplyToResponse.model_validate(
{
**MessageModel.model_validate(message).model_dump(),
"user": user_info,
"reply_to_message": (
reply_to_message.model_dump()
if reply_to_message
else None
),
}
)
)
return messages
def get_reply_user_ids_by_message_id(
self, id: str, db: Optional[Session] = None
) -> list[str]:
with get_db_context(db) as db:
return [
message.user_id
for message in db.query(Message).filter_by(parent_id=id).all()
]
def get_messages_by_channel_id(
self, channel_id: str, skip: int = 0, limit: int = 50
) -> list[MessageModel]:
with get_db() as db:
self,
channel_id: str,
skip: int = 0,
limit: int = 50,
db: Optional[Session] = None,
) -> list[MessageReplyToResponse]:
with get_db_context(db) as db:
all_messages = (
db.query(Message)
.filter_by(channel_id=channel_id, parent_id=None)
@@ -169,12 +314,58 @@ class MessageTable:
.limit(limit)
.all()
)
return [MessageModel.model_validate(message) for message in all_messages]
messages = []
for message in all_messages:
reply_to_message = (
self.get_message_by_id(
message.reply_to_id, include_thread_replies=False, db=db
)
if message.reply_to_id
else None
)
webhook_info = message.meta.get("webhook") if message.meta else None
user_info = None
if webhook_info and webhook_info.get("id"):
webhook = Channels.get_webhook_by_id(webhook_info.get("id"), db=db)
if webhook:
user_info = {
"id": webhook.id,
"name": webhook.name,
"role": "webhook",
}
else:
user_info = {
"id": webhook_info.get("id"),
"name": "Deleted Webhook",
"role": "webhook",
}
messages.append(
MessageReplyToResponse.model_validate(
{
**MessageModel.model_validate(message).model_dump(),
"user": user_info,
"reply_to_message": (
reply_to_message.model_dump()
if reply_to_message
else None
),
}
)
)
return messages
def get_messages_by_parent_id(
self, channel_id: str, parent_id: str, skip: int = 0, limit: int = 50
) -> list[MessageModel]:
with get_db() as db:
self,
channel_id: str,
parent_id: str,
skip: int = 0,
limit: int = 50,
db: Optional[Session] = None,
) -> list[MessageReplyToResponse]:
with get_db_context(db) as db:
message = db.get(Message, parent_id)
if not message:
@@ -193,25 +384,143 @@ class MessageTable:
if len(all_messages) < limit:
all_messages.append(message)
messages = []
for message in all_messages:
reply_to_message = (
self.get_message_by_id(
message.reply_to_id, include_thread_replies=False, db=db
)
if message.reply_to_id
else None
)
webhook_info = message.meta.get("webhook") if message.meta else None
user_info = None
if webhook_info and webhook_info.get("id"):
webhook = Channels.get_webhook_by_id(webhook_info.get("id"), db=db)
if webhook:
user_info = {
"id": webhook.id,
"name": webhook.name,
"role": "webhook",
}
else:
user_info = {
"id": webhook_info.get("id"),
"name": "Deleted Webhook",
"role": "webhook",
}
messages.append(
MessageReplyToResponse.model_validate(
{
**MessageModel.model_validate(message).model_dump(),
"user": user_info,
"reply_to_message": (
reply_to_message.model_dump()
if reply_to_message
else None
),
}
)
)
return messages
def get_last_message_by_channel_id(
self, channel_id: str, db: Optional[Session] = None
) -> Optional[MessageModel]:
with get_db_context(db) as db:
message = (
db.query(Message)
.filter_by(channel_id=channel_id)
.order_by(Message.created_at.desc())
.first()
)
return MessageModel.model_validate(message) if message else None
def get_pinned_messages_by_channel_id(
self,
channel_id: str,
skip: int = 0,
limit: int = 50,
db: Optional[Session] = None,
) -> list[MessageModel]:
with get_db_context(db) as db:
all_messages = (
db.query(Message)
.filter_by(channel_id=channel_id, is_pinned=True)
.order_by(Message.pinned_at.desc())
.offset(skip)
.limit(limit)
.all()
)
return [MessageModel.model_validate(message) for message in all_messages]
def update_message_by_id(
self, id: str, form_data: MessageForm
self, id: str, form_data: MessageForm, db: Optional[Session] = None
) -> Optional[MessageModel]:
with get_db() as db:
with get_db_context(db) as db:
message = db.get(Message, id)
message.content = form_data.content
message.data = form_data.data
message.meta = form_data.meta
message.data = {
**(message.data if message.data else {}),
**(form_data.data if form_data.data else {}),
}
message.meta = {
**(message.meta if message.meta else {}),
**(form_data.meta if form_data.meta else {}),
}
message.updated_at = int(time.time_ns())
db.commit()
db.refresh(message)
return MessageModel.model_validate(message) if message else None
def update_is_pinned_by_id(
self,
id: str,
is_pinned: bool,
pinned_by: Optional[str] = None,
db: Optional[Session] = None,
) -> Optional[MessageModel]:
with get_db_context(db) as db:
message = db.get(Message, id)
message.is_pinned = is_pinned
message.pinned_at = int(time.time_ns()) if is_pinned else None
message.pinned_by = pinned_by if is_pinned else None
db.commit()
db.refresh(message)
return MessageModel.model_validate(message) if message else None
def get_unread_message_count(
self,
channel_id: str,
user_id: str,
last_read_at: Optional[int] = None,
db: Optional[Session] = None,
) -> int:
with get_db_context(db) as db:
query = db.query(Message).filter(
Message.channel_id == channel_id,
Message.parent_id == None, # only count top-level messages
Message.created_at > (last_read_at if last_read_at else 0),
)
if user_id:
query = query.filter(Message.user_id != user_id)
return query.count()
def add_reaction_to_message(
self, id: str, user_id: str, name: str
self, id: str, user_id: str, name: str, db: Optional[Session] = None
) -> Optional[MessageReactionModel]:
with get_db() as db:
with get_db_context(db) as db:
# check for existing reaction
existing_reaction = (
db.query(MessageReaction)
.filter_by(message_id=id, user_id=user_id, name=name)
.first()
)
if existing_reaction:
return MessageReactionModel.model_validate(existing_reaction)
reaction_id = str(uuid.uuid4())
reaction = MessageReactionModel(
id=reaction_id,
@@ -226,47 +535,62 @@ class MessageTable:
db.refresh(result)
return MessageReactionModel.model_validate(result) if result else None
def get_reactions_by_message_id(self, id: str) -> list[Reactions]:
with get_db() as db:
all_reactions = db.query(MessageReaction).filter_by(message_id=id).all()
def get_reactions_by_message_id(
self, id: str, db: Optional[Session] = None
) -> list[Reactions]:
with get_db_context(db) as db:
# JOIN User so all user info is fetched in one query
results = (
db.query(MessageReaction, User)
.join(User, MessageReaction.user_id == User.id)
.filter(MessageReaction.message_id == id)
.all()
)
reactions = {}
for reaction in all_reactions:
for reaction, user in results:
if reaction.name not in reactions:
reactions[reaction.name] = {
"name": reaction.name,
"user_ids": [],
"users": [],
"count": 0,
}
reactions[reaction.name]["user_ids"].append(reaction.user_id)
reactions[reaction.name]["users"].append(
{
"id": user.id,
"name": user.name,
}
)
reactions[reaction.name]["count"] += 1
return [Reactions(**reaction) for reaction in reactions.values()]
def remove_reaction_by_id_and_user_id_and_name(
self, id: str, user_id: str, name: str
self, id: str, user_id: str, name: str, db: Optional[Session] = None
) -> bool:
with get_db() as db:
with get_db_context(db) as db:
db.query(MessageReaction).filter_by(
message_id=id, user_id=user_id, name=name
).delete()
db.commit()
return True
def delete_reactions_by_id(self, id: str) -> bool:
with get_db() as db:
def delete_reactions_by_id(self, id: str, db: Optional[Session] = None) -> bool:
with get_db_context(db) as db:
db.query(MessageReaction).filter_by(message_id=id).delete()
db.commit()
return True
def delete_replies_by_id(self, id: str) -> bool:
with get_db() as db:
def delete_replies_by_id(self, id: str, db: Optional[Session] = None) -> bool:
with get_db_context(db) as db:
db.query(Message).filter_by(parent_id=id).delete()
db.commit()
return True
def delete_message_by_id(self, id: str) -> bool:
with get_db() as db:
def delete_message_by_id(self, id: str, db: Optional[Session] = None) -> bool:
with get_db_context(db) as db:
db.query(Message).filter_by(id=id).delete()
# Delete all reactions to this message
@@ -275,5 +599,35 @@ class MessageTable:
db.commit()
return True
def search_messages_by_channel_ids(
self,
channel_ids: list[str],
query: str,
start_timestamp: Optional[int] = None,
end_timestamp: Optional[int] = None,
limit: int = 10,
db: Optional[Session] = None,
) -> list[MessageModel]:
"""Search messages in specified channels by content."""
with get_db_context(db) as db:
query_builder = db.query(Message).filter(
Message.channel_id.in_(channel_ids),
Message.content.ilike(f"%{query}%"),
)
if start_timestamp:
query_builder = query_builder.filter(
Message.created_at >= start_timestamp
)
if end_timestamp:
query_builder = query_builder.filter(
Message.created_at <= end_timestamp
)
messages = (
query_builder.order_by(Message.created_at.desc()).limit(limit).all()
)
return [MessageModel.model_validate(msg) for msg in messages]
Messages = MessageTable()
+297 -93
View File
@@ -2,25 +2,23 @@ import logging
import time
from typing import Optional
from open_webui.internal.db import Base, JSONField, get_db
from open_webui.env import SRC_LOG_LEVELS
from sqlalchemy.orm import Session
from open_webui.internal.db import Base, JSONField, get_db, get_db_context
from open_webui.models.groups import Groups
from open_webui.models.users import Users, UserResponse
from open_webui.models.users import User, UserModel, Users, UserResponse
from open_webui.models.access_grants import AccessGrantModel, AccessGrants
from pydantic import BaseModel, ConfigDict
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy import or_, and_, func
from sqlalchemy import String, cast, or_, and_, func
from sqlalchemy.dialects import postgresql, sqlite
from sqlalchemy import BigInteger, Column, Text, JSON, Boolean
from open_webui.utils.access_control import has_access
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy import BigInteger, Column, Text, Boolean
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
####################
@@ -53,7 +51,7 @@ class ModelMeta(BaseModel):
class Model(Base):
__tablename__ = "model"
id = Column(Text, primary_key=True)
id = Column(Text, primary_key=True, unique=True)
"""
The model's id as used in the API. If set to an existing model, it will override the model.
"""
@@ -79,23 +77,6 @@ class Model(Base):
Holds a JSON encoded blob of metadata, see `ModelMeta`.
"""
access_control = Column(JSON, nullable=True) # Controls data access levels.
# Defines access control rules for this entry.
# - `None`: Public access, available to all users with the "user" role.
# - `{}`: Private access, restricted exclusively to the owner.
# - Custom permissions: Specific access control for reading and writing;
# Can specify group or user-level restrictions:
# {
# "read": {
# "group_ids": ["group_id1", "group_id2"],
# "user_ids": ["user_id1", "user_id2"]
# },
# "write": {
# "group_ids": ["group_id1", "group_id2"],
# "user_ids": ["user_id1", "user_id2"]
# }
# }
is_active = Column(Boolean, default=True)
updated_at = Column(BigInteger)
@@ -111,7 +92,7 @@ class ModelModel(BaseModel):
params: ModelParams
meta: ModelMeta
access_control: Optional[dict] = None
access_grants: list[AccessGrantModel] = Field(default_factory=list)
is_active: bool
updated_at: int # timestamp in epoch
@@ -129,59 +110,106 @@ class ModelUserResponse(ModelModel):
user: Optional[UserResponse] = None
class ModelAccessResponse(ModelUserResponse):
write_access: Optional[bool] = False
class ModelResponse(ModelModel):
pass
class ModelListResponse(BaseModel):
items: list[ModelUserResponse]
total: int
class ModelAccessListResponse(BaseModel):
items: list[ModelAccessResponse]
total: int
class ModelForm(BaseModel):
id: str
base_model_id: Optional[str] = None
name: str
meta: ModelMeta
params: ModelParams
access_control: Optional[dict] = None
access_grants: Optional[list[dict]] = None
is_active: bool = True
class ModelsTable:
def insert_new_model(
self, form_data: ModelForm, user_id: str
) -> Optional[ModelModel]:
model = ModelModel(
**{
**form_data.model_dump(),
"user_id": user_id,
"created_at": int(time.time()),
"updated_at": int(time.time()),
}
def _get_access_grants(
self, model_id: str, db: Optional[Session] = None
) -> list[AccessGrantModel]:
return AccessGrants.get_grants_by_resource("model", model_id, db=db)
def _to_model_model(
self,
model: Model,
access_grants: Optional[list[AccessGrantModel]] = None,
db: Optional[Session] = None,
) -> ModelModel:
model_data = ModelModel.model_validate(model).model_dump(
exclude={"access_grants"}
)
model_data["access_grants"] = (
access_grants
if access_grants is not None
else self._get_access_grants(model_data["id"], db=db)
)
return ModelModel.model_validate(model_data)
def insert_new_model(
self, form_data: ModelForm, user_id: str, db: Optional[Session] = None
) -> Optional[ModelModel]:
try:
with get_db() as db:
result = Model(**model.model_dump())
with get_db_context(db) as db:
result = Model(
**{
**form_data.model_dump(exclude={"access_grants"}),
"user_id": user_id,
"created_at": int(time.time()),
"updated_at": int(time.time()),
}
)
db.add(result)
db.commit()
db.refresh(result)
AccessGrants.set_access_grants(
"model", result.id, form_data.access_grants, db=db
)
if result:
return ModelModel.model_validate(result)
return self._to_model_model(result, db=db)
else:
return None
except Exception as e:
log.exception(f"Failed to insert a new model: {e}")
return None
def get_all_models(self) -> list[ModelModel]:
with get_db() as db:
return [ModelModel.model_validate(model) for model in db.query(Model).all()]
def get_all_models(self, db: Optional[Session] = None) -> list[ModelModel]:
with get_db_context(db) as db:
all_models = db.query(Model).all()
model_ids = [model.id for model in all_models]
grants_map = AccessGrants.get_grants_by_resources("model", model_ids, db=db)
return [
self._to_model_model(
model, access_grants=grants_map.get(model.id, []), db=db
)
for model in all_models
]
def get_models(self) -> list[ModelUserResponse]:
with get_db() as db:
def get_models(self, db: Optional[Session] = None) -> list[ModelUserResponse]:
with get_db_context(db) as db:
all_models = db.query(Model).filter(Model.base_model_id != None).all()
user_ids = list(set(model.user_id for model in all_models))
model_ids = [model.id for model in all_models]
users = Users.get_users_by_user_ids(user_ids) if user_ids else []
users = Users.get_users_by_user_ids(user_ids, db=db) if user_ids else []
users_dict = {user.id: user for user in users}
grants_map = AccessGrants.get_grants_by_resources("model", model_ids, db=db)
models = []
for model in all_models:
@@ -189,78 +217,235 @@ class ModelsTable:
models.append(
ModelUserResponse.model_validate(
{
**ModelModel.model_validate(model).model_dump(),
**self._to_model_model(
model,
access_grants=grants_map.get(model.id, []),
db=db,
).model_dump(),
"user": user.model_dump() if user else None,
}
)
)
return models
def get_base_models(self) -> list[ModelModel]:
with get_db() as db:
def get_base_models(self, db: Optional[Session] = None) -> list[ModelModel]:
with get_db_context(db) as db:
all_models = db.query(Model).filter(Model.base_model_id == None).all()
model_ids = [model.id for model in all_models]
grants_map = AccessGrants.get_grants_by_resources("model", model_ids, db=db)
return [
ModelModel.model_validate(model)
for model in db.query(Model).filter(Model.base_model_id == None).all()
self._to_model_model(
model, access_grants=grants_map.get(model.id, []), db=db
)
for model in all_models
]
def get_models_by_user_id(
self, user_id: str, permission: str = "write"
self, user_id: str, permission: str = "write", db: Optional[Session] = None
) -> list[ModelUserResponse]:
models = self.get_models()
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user_id)}
models = self.get_models(db=db)
user_group_ids = {
group.id for group in Groups.get_groups_by_member_id(user_id, db=db)
}
return [
model
for model in models
if model.user_id == user_id
or has_access(user_id, permission, model.access_control, user_group_ids)
or AccessGrants.has_access(
user_id=user_id,
resource_type="model",
resource_id=model.id,
permission=permission,
user_group_ids=user_group_ids,
db=db,
)
]
def get_model_by_id(self, id: str) -> Optional[ModelModel]:
def _has_permission(self, db, query, filter: dict, permission: str = "read"):
return AccessGrants.has_permission_filter(
db=db,
query=query,
DocumentModel=Model,
filter=filter,
resource_type="model",
permission=permission,
)
def search_models(
self,
user_id: str,
filter: dict = {},
skip: int = 0,
limit: int = 30,
db: Optional[Session] = None,
) -> ModelListResponse:
with get_db_context(db) as db:
# Join GroupMember so we can order by group_id when requested
query = db.query(Model, User).outerjoin(User, User.id == Model.user_id)
query = query.filter(Model.base_model_id != None)
if filter:
query_key = filter.get("query")
if query_key:
query = query.filter(
or_(
Model.name.ilike(f"%{query_key}%"),
Model.base_model_id.ilike(f"%{query_key}%"),
User.name.ilike(f"%{query_key}%"),
User.email.ilike(f"%{query_key}%"),
User.username.ilike(f"%{query_key}%"),
)
)
view_option = filter.get("view_option")
if view_option == "created":
query = query.filter(Model.user_id == user_id)
elif view_option == "shared":
query = query.filter(Model.user_id != user_id)
# Apply access control filtering
query = self._has_permission(
db,
query,
filter,
permission="read",
)
tag = filter.get("tag")
if tag:
# TODO: This is a simple implementation and should be improved for performance
like_pattern = f'%"{tag.lower()}"%' # `"tag"` inside JSON array
meta_text = func.lower(cast(Model.meta, String))
query = query.filter(meta_text.like(like_pattern))
order_by = filter.get("order_by")
direction = filter.get("direction")
if order_by == "name":
if direction == "asc":
query = query.order_by(Model.name.asc())
else:
query = query.order_by(Model.name.desc())
elif order_by == "created_at":
if direction == "asc":
query = query.order_by(Model.created_at.asc())
else:
query = query.order_by(Model.created_at.desc())
elif order_by == "updated_at":
if direction == "asc":
query = query.order_by(Model.updated_at.asc())
else:
query = query.order_by(Model.updated_at.desc())
else:
query = query.order_by(Model.created_at.desc())
# Count BEFORE pagination
total = query.count()
if skip:
query = query.offset(skip)
if limit:
query = query.limit(limit)
items = query.all()
model_ids = [model.id for model, _ in items]
grants_map = AccessGrants.get_grants_by_resources("model", model_ids, db=db)
models = []
for model, user in items:
models.append(
ModelUserResponse(
**self._to_model_model(
model,
access_grants=grants_map.get(model.id, []),
db=db,
).model_dump(),
user=(
UserResponse(**UserModel.model_validate(user).model_dump())
if user
else None
),
)
)
return ModelListResponse(items=models, total=total)
def get_model_by_id(
self, id: str, db: Optional[Session] = None
) -> Optional[ModelModel]:
try:
with get_db() as db:
with get_db_context(db) as db:
model = db.get(Model, id)
return ModelModel.model_validate(model)
return self._to_model_model(model, db=db) if model else None
except Exception:
return None
def toggle_model_by_id(self, id: str) -> Optional[ModelModel]:
with get_db() as db:
try:
is_active = db.query(Model).filter_by(id=id).first().is_active
db.query(Model).filter_by(id=id).update(
{
"is_active": not is_active,
"updated_at": int(time.time()),
}
def get_models_by_ids(
self, ids: list[str], db: Optional[Session] = None
) -> list[ModelModel]:
try:
with get_db_context(db) as db:
models = db.query(Model).filter(Model.id.in_(ids)).all()
model_ids = [model.id for model in models]
grants_map = AccessGrants.get_grants_by_resources(
"model", model_ids, db=db
)
db.commit()
return [
self._to_model_model(
model,
access_grants=grants_map.get(model.id, []),
db=db,
)
for model in models
]
except Exception:
return []
return self.get_model_by_id(id)
def toggle_model_by_id(
self, id: str, db: Optional[Session] = None
) -> Optional[ModelModel]:
with get_db_context(db) as db:
try:
model = db.query(Model).filter_by(id=id).first()
if not model:
return None
model.is_active = not model.is_active
model.updated_at = int(time.time())
db.commit()
db.refresh(model)
return self._to_model_model(model, db=db)
except Exception:
return None
def update_model_by_id(self, id: str, model: ModelForm) -> Optional[ModelModel]:
def update_model_by_id(
self, id: str, model: ModelForm, db: Optional[Session] = None
) -> Optional[ModelModel]:
try:
with get_db() as db:
with get_db_context(db) as db:
# update only the fields that are present in the model
result = (
db.query(Model)
.filter_by(id=id)
.update(model.model_dump(exclude={"id"}))
)
db.commit()
data = model.model_dump(exclude={"id", "access_grants"})
result = db.query(Model).filter_by(id=id).update(data)
model = db.get(Model, id)
db.refresh(model)
return ModelModel.model_validate(model)
db.commit()
if model.access_grants is not None:
AccessGrants.set_access_grants(
"model", id, model.access_grants, db=db
)
return self.get_model_by_id(id, db=db)
except Exception as e:
log.exception(f"Failed to update the model by id {id}: {e}")
return None
def delete_model_by_id(self, id: str) -> bool:
def delete_model_by_id(self, id: str, db: Optional[Session] = None) -> bool:
try:
with get_db() as db:
with get_db_context(db) as db:
AccessGrants.revoke_all_access("model", id, db=db)
db.query(Model).filter_by(id=id).delete()
db.commit()
@@ -268,9 +453,12 @@ class ModelsTable:
except Exception:
return False
def delete_all_models(self) -> bool:
def delete_all_models(self, db: Optional[Session] = None) -> bool:
try:
with get_db() as db:
with get_db_context(db) as db:
model_ids = [row[0] for row in db.query(Model.id).all()]
for model_id in model_ids:
AccessGrants.revoke_all_access("model", model_id, db=db)
db.query(Model).delete()
db.commit()
@@ -278,9 +466,11 @@ class ModelsTable:
except Exception:
return False
def sync_models(self, user_id: str, models: list[ModelModel]) -> list[ModelModel]:
def sync_models(
self, user_id: str, models: list[ModelModel], db: Optional[Session] = None
) -> list[ModelModel]:
try:
with get_db() as db:
with get_db_context(db) as db:
# Get existing models
existing_models = db.query(Model).all()
existing_ids = {model.id for model in existing_models}
@@ -293,7 +483,7 @@ class ModelsTable:
if model.id in existing_ids:
db.query(Model).filter_by(id=model.id).update(
{
**model.model_dump(),
**model.model_dump(exclude={"access_grants"}),
"user_id": user_id,
"updated_at": int(time.time()),
}
@@ -301,22 +491,36 @@ class ModelsTable:
else:
new_model = Model(
**{
**model.model_dump(),
**model.model_dump(exclude={"access_grants"}),
"user_id": user_id,
"updated_at": int(time.time()),
}
)
db.add(new_model)
AccessGrants.set_access_grants(
"model", model.id, model.access_grants, db=db
)
# Remove models that are no longer present
for model in existing_models:
if model.id not in new_model_ids:
AccessGrants.revoke_all_access("model", model.id, db=db)
db.delete(model)
db.commit()
all_models = db.query(Model).all()
model_ids = [model.id for model in all_models]
grants_map = AccessGrants.get_grants_by_resources(
"model", model_ids, db=db
)
return [
ModelModel.model_validate(model) for model in db.query(Model).all()
self._to_model_model(
model,
access_grants=grants_map.get(model.id, []),
db=db,
)
for model in all_models
]
except Exception as e:
log.exception(f"Error syncing models for user {user_id}: {e}")
+238 -46
View File
@@ -2,17 +2,18 @@ import json
import time
import uuid
from typing import Optional
from functools import lru_cache
from open_webui.internal.db import Base, get_db
from sqlalchemy.orm import Session
from open_webui.internal.db import Base, get_db, get_db_context
from open_webui.models.groups import Groups
from open_webui.utils.access_control import has_access
from open_webui.models.users import Users, UserResponse
from open_webui.models.users import User, UserModel, Users, UserResponse
from open_webui.models.access_grants import AccessGrantModel, AccessGrants
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON
from sqlalchemy import or_, func, select, and_, text
from sqlalchemy.sql import exists
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy import BigInteger, Column, Text, JSON
from sqlalchemy import or_, func, cast
####################
# Note DB Schema
@@ -22,15 +23,13 @@ from sqlalchemy.sql import exists
class Note(Base):
__tablename__ = "note"
id = Column(Text, primary_key=True)
id = Column(Text, primary_key=True, unique=True)
user_id = Column(Text)
title = Column(Text)
data = Column(JSON, nullable=True)
meta = Column(JSON, nullable=True)
access_control = Column(JSON, nullable=True)
created_at = Column(BigInteger)
updated_at = Column(BigInteger)
@@ -45,7 +44,7 @@ class NoteModel(BaseModel):
data: Optional[dict] = None
meta: Optional[dict] = None
access_control: Optional[dict] = None
access_grants: list[AccessGrantModel] = Field(default_factory=list)
created_at: int # timestamp in epoch
updated_at: int # timestamp in epoch
@@ -60,69 +59,256 @@ class NoteForm(BaseModel):
title: str
data: Optional[dict] = None
meta: Optional[dict] = None
access_control: Optional[dict] = None
access_grants: Optional[list[dict]] = None
class NoteUpdateForm(BaseModel):
title: Optional[str] = None
data: Optional[dict] = None
meta: Optional[dict] = None
access_control: Optional[dict] = None
access_grants: Optional[list[dict]] = None
class NoteUserResponse(NoteModel):
user: Optional[UserResponse] = None
class NoteItemResponse(BaseModel):
id: str
title: str
data: Optional[dict]
updated_at: int
created_at: int
user: Optional[UserResponse] = None
class NoteListResponse(BaseModel):
items: list[NoteUserResponse]
total: int
class NoteTable:
def insert_new_note(
def _get_access_grants(
self, note_id: str, db: Optional[Session] = None
) -> list[AccessGrantModel]:
return AccessGrants.get_grants_by_resource("note", note_id, db=db)
def _to_note_model(
self,
form_data: NoteForm,
user_id: str,
note: Note,
access_grants: Optional[list[AccessGrantModel]] = None,
db: Optional[Session] = None,
) -> NoteModel:
note_data = NoteModel.model_validate(note).model_dump(exclude={"access_grants"})
note_data["access_grants"] = (
access_grants
if access_grants is not None
else self._get_access_grants(note_data["id"], db=db)
)
return NoteModel.model_validate(note_data)
def _has_permission(self, db, query, filter: dict, permission: str = "read"):
return AccessGrants.has_permission_filter(
db=db,
query=query,
DocumentModel=Note,
filter=filter,
resource_type="note",
permission=permission,
)
def insert_new_note(
self, user_id: str, form_data: NoteForm, db: Optional[Session] = None
) -> Optional[NoteModel]:
with get_db() as db:
with get_db_context(db) as db:
note = NoteModel(
**{
"id": str(uuid.uuid4()),
"user_id": user_id,
**form_data.model_dump(),
**form_data.model_dump(exclude={"access_grants"}),
"created_at": int(time.time_ns()),
"updated_at": int(time.time_ns()),
"access_grants": [],
}
)
new_note = Note(**note.model_dump())
new_note = Note(**note.model_dump(exclude={"access_grants"}))
db.add(new_note)
db.commit()
return note
AccessGrants.set_access_grants(
"note", note.id, form_data.access_grants, db=db
)
return self._to_note_model(new_note, db=db)
def get_notes(self) -> list[NoteModel]:
with get_db() as db:
notes = db.query(Note).order_by(Note.updated_at.desc()).all()
return [NoteModel.model_validate(note) for note in notes]
def get_notes(
self, skip: int = 0, limit: int = 50, db: Optional[Session] = None
) -> list[NoteModel]:
with get_db_context(db) as db:
query = db.query(Note).order_by(Note.updated_at.desc())
if skip is not None:
query = query.offset(skip)
if limit is not None:
query = query.limit(limit)
notes = query.all()
note_ids = [note.id for note in notes]
grants_map = AccessGrants.get_grants_by_resources("note", note_ids, db=db)
return [
self._to_note_model(
note, access_grants=grants_map.get(note.id, []), db=db
)
for note in notes
]
def search_notes(
self,
user_id: str,
filter: dict = {},
skip: int = 0,
limit: int = 30,
db: Optional[Session] = None,
) -> NoteListResponse:
with get_db_context(db) as db:
query = db.query(Note, User).outerjoin(User, User.id == Note.user_id)
if filter:
query_key = filter.get("query")
if query_key:
# Normalize search by removing hyphens and spaces (e.g., "todo" matches "to-do" and "to do")
normalized_query = query_key.replace("-", "").replace(" ", "")
query = query.filter(
or_(
func.replace(
func.replace(Note.title, "-", ""), " ", ""
).ilike(f"%{normalized_query}%"),
func.replace(
func.replace(
cast(Note.data["content"]["md"], Text), "-", ""
),
" ",
"",
).ilike(f"%{normalized_query}%"),
)
)
view_option = filter.get("view_option")
if view_option == "created":
query = query.filter(Note.user_id == user_id)
elif view_option == "shared":
query = query.filter(Note.user_id != user_id)
# Apply access control filtering
if "permission" in filter:
permission = filter["permission"]
else:
permission = "write"
query = self._has_permission(
db,
query,
filter,
permission=permission,
)
order_by = filter.get("order_by")
direction = filter.get("direction")
if order_by == "name":
if direction == "asc":
query = query.order_by(Note.title.asc())
else:
query = query.order_by(Note.title.desc())
elif order_by == "created_at":
if direction == "asc":
query = query.order_by(Note.created_at.asc())
else:
query = query.order_by(Note.created_at.desc())
elif order_by == "updated_at":
if direction == "asc":
query = query.order_by(Note.updated_at.asc())
else:
query = query.order_by(Note.updated_at.desc())
else:
query = query.order_by(Note.updated_at.desc())
else:
query = query.order_by(Note.updated_at.desc())
# Count BEFORE pagination
total = query.count()
if skip:
query = query.offset(skip)
if limit:
query = query.limit(limit)
items = query.all()
note_ids = [note.id for note, _ in items]
grants_map = AccessGrants.get_grants_by_resources("note", note_ids, db=db)
notes = []
for note, user in items:
notes.append(
NoteUserResponse(
**self._to_note_model(
note,
access_grants=grants_map.get(note.id, []),
db=db,
).model_dump(),
user=(
UserResponse(**UserModel.model_validate(user).model_dump())
if user
else None
),
)
)
return NoteListResponse(items=notes, total=total)
def get_notes_by_user_id(
self, user_id: str, permission: str = "write"
self,
user_id: str,
permission: str = "read",
skip: int = 0,
limit: int = 50,
db: Optional[Session] = None,
) -> list[NoteModel]:
notes = self.get_notes()
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user_id)}
return [
note
for note in notes
if note.user_id == user_id
or has_access(user_id, permission, note.access_control, user_group_ids)
]
with get_db_context(db) as db:
user_group_ids = [
group.id for group in Groups.get_groups_by_member_id(user_id, db=db)
]
def get_note_by_id(self, id: str) -> Optional[NoteModel]:
with get_db() as db:
query = db.query(Note).order_by(Note.updated_at.desc())
query = self._has_permission(
db, query, {"user_id": user_id, "group_ids": user_group_ids}, permission
)
if skip is not None:
query = query.offset(skip)
if limit is not None:
query = query.limit(limit)
notes = query.all()
note_ids = [note.id for note in notes]
grants_map = AccessGrants.get_grants_by_resources("note", note_ids, db=db)
return [
self._to_note_model(
note, access_grants=grants_map.get(note.id, []), db=db
)
for note in notes
]
def get_note_by_id(
self, id: str, db: Optional[Session] = None
) -> Optional[NoteModel]:
with get_db_context(db) as db:
note = db.query(Note).filter(Note.id == id).first()
return NoteModel.model_validate(note) if note else None
return self._to_note_model(note, db=db) if note else None
def update_note_by_id(
self, id: str, form_data: NoteUpdateForm
self, id: str, form_data: NoteUpdateForm, db: Optional[Session] = None
) -> Optional[NoteModel]:
with get_db() as db:
with get_db_context(db) as db:
note = db.query(Note).filter(Note.id == id).first()
if not note:
return None
@@ -136,19 +322,25 @@ class NoteTable:
if "meta" in form_data:
note.meta = {**note.meta, **form_data["meta"]}
if "access_control" in form_data:
note.access_control = form_data["access_control"]
if "access_grants" in form_data:
AccessGrants.set_access_grants(
"note", id, form_data["access_grants"], db=db
)
note.updated_at = int(time.time_ns())
db.commit()
return NoteModel.model_validate(note) if note else None
return self._to_note_model(note, db=db) if note else None
def delete_note_by_id(self, id: str):
with get_db() as db:
db.query(Note).filter(Note.id == id).delete()
db.commit()
return True
def delete_note_by_id(self, id: str, db: Optional[Session] = None) -> bool:
try:
with get_db_context(db) as db:
AccessGrants.revoke_all_access("note", id, db=db)
db.query(Note).filter(Note.id == id).delete()
db.commit()
return True
except Exception:
return False
Notes = NoteTable()
+76 -20
View File
@@ -8,14 +8,14 @@ import json
from cryptography.fernet import Fernet
from open_webui.internal.db import Base, get_db
from open_webui.env import SRC_LOG_LEVELS, OAUTH_SESSION_TOKEN_ENCRYPTION_KEY
from sqlalchemy.orm import Session
from open_webui.internal.db import Base, get_db, get_db_context
from open_webui.env import OAUTH_SESSION_TOKEN_ENCRYPTION_KEY
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Column, String, Text, Index
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
####################
# DB MODEL
@@ -25,7 +25,7 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
class OAuthSession(Base):
__tablename__ = "oauth_session"
id = Column(Text, primary_key=True)
id = Column(Text, primary_key=True, unique=True)
user_id = Column(Text, nullable=False)
provider = Column(Text, nullable=False)
token = Column(
@@ -102,7 +102,7 @@ class OAuthSessionTable:
decrypted = self.fernet.decrypt(token.encode()).decode()
return json.loads(decrypted)
except Exception as e:
log.error(f"Error decrypting tokens: {e}")
log.error(f"Error decrypting tokens: {type(e).__name__}: {e}")
raise
def create_session(
@@ -110,10 +110,11 @@ class OAuthSessionTable:
user_id: str,
provider: str,
token: dict,
db: Optional[Session] = None,
) -> Optional[OAuthSessionModel]:
"""Create a new OAuth session"""
try:
with get_db() as db:
with get_db_context(db) as db:
current_time = int(time.time())
id = str(uuid.uuid4())
@@ -134,6 +135,7 @@ class OAuthSessionTable:
db.refresh(result)
if result:
db.expunge(result) # Detach so dict swap is never flushed
result.token = token # Return decrypted token
return OAuthSessionModel.model_validate(result)
else:
@@ -142,12 +144,15 @@ class OAuthSessionTable:
log.error(f"Error creating OAuth session: {e}")
return None
def get_session_by_id(self, session_id: str) -> Optional[OAuthSessionModel]:
def get_session_by_id(
self, session_id: str, db: Optional[Session] = None
) -> Optional[OAuthSessionModel]:
"""Get OAuth session by ID"""
try:
with get_db() as db:
with get_db_context(db) as db:
session = db.query(OAuthSession).filter_by(id=session_id).first()
if session:
db.expunge(session)
session.token = self._decrypt_token(session.token)
return OAuthSessionModel.model_validate(session)
@@ -157,17 +162,18 @@ class OAuthSessionTable:
return None
def get_session_by_id_and_user_id(
self, session_id: str, user_id: str
self, session_id: str, user_id: str, db: Optional[Session] = None
) -> Optional[OAuthSessionModel]:
"""Get OAuth session by ID and user ID"""
try:
with get_db() as db:
with get_db_context(db) as db:
session = (
db.query(OAuthSession)
.filter_by(id=session_id, user_id=user_id)
.first()
)
if session:
db.expunge(session)
session.token = self._decrypt_token(session.token)
return OAuthSessionModel.model_validate(session)
@@ -176,16 +182,48 @@ class OAuthSessionTable:
log.error(f"Error getting OAuth session by ID: {e}")
return None
def get_sessions_by_user_id(self, user_id: str) -> List[OAuthSessionModel]:
def get_session_by_provider_and_user_id(
self, provider: str, user_id: str, db: Optional[Session] = None
) -> Optional[OAuthSessionModel]:
"""Get OAuth session by provider and user ID"""
try:
with get_db_context(db) as db:
session = (
db.query(OAuthSession)
.filter_by(provider=provider, user_id=user_id)
.order_by(OAuthSession.created_at.desc())
.first()
)
if session:
db.expunge(session)
session.token = self._decrypt_token(session.token)
return OAuthSessionModel.model_validate(session)
return None
except Exception as e:
log.error(f"Error getting OAuth session by provider and user ID: {e}")
return None
def get_sessions_by_user_id(
self, user_id: str, db: Optional[Session] = None
) -> List[OAuthSessionModel]:
"""Get all OAuth sessions for a user"""
try:
with get_db() as db:
with get_db_context(db) as db:
sessions = db.query(OAuthSession).filter_by(user_id=user_id).all()
results = []
for session in sessions:
session.token = self._decrypt_token(session.token)
results.append(OAuthSessionModel.model_validate(session))
try:
db.expunge(session)
session.token = self._decrypt_token(session.token)
results.append(OAuthSessionModel.model_validate(session))
except Exception as e:
log.warning(
f"Skipping OAuth session {session.id} due to decryption failure, deleting corrupted session: {type(e).__name__}: {e}"
)
db.query(OAuthSession).filter_by(id=session.id).delete()
db.commit()
return results
@@ -194,11 +232,11 @@ class OAuthSessionTable:
return []
def update_session_by_id(
self, session_id: str, token: dict
self, session_id: str, token: dict, db: Optional[Session] = None
) -> Optional[OAuthSessionModel]:
"""Update OAuth session tokens"""
try:
with get_db() as db:
with get_db_context(db) as db:
current_time = int(time.time())
db.query(OAuthSession).filter_by(id=session_id).update(
@@ -212,6 +250,7 @@ class OAuthSessionTable:
session = db.query(OAuthSession).filter_by(id=session_id).first()
if session:
db.expunge(session)
session.token = self._decrypt_token(session.token)
return OAuthSessionModel.model_validate(session)
@@ -220,10 +259,12 @@ class OAuthSessionTable:
log.error(f"Error updating OAuth session tokens: {e}")
return None
def delete_session_by_id(self, session_id: str) -> bool:
def delete_session_by_id(
self, session_id: str, db: Optional[Session] = None
) -> bool:
"""Delete an OAuth session"""
try:
with get_db() as db:
with get_db_context(db) as db:
result = db.query(OAuthSession).filter_by(id=session_id).delete()
db.commit()
return result > 0
@@ -231,10 +272,12 @@ class OAuthSessionTable:
log.error(f"Error deleting OAuth session: {e}")
return False
def delete_sessions_by_user_id(self, user_id: str) -> bool:
def delete_sessions_by_user_id(
self, user_id: str, db: Optional[Session] = None
) -> bool:
"""Delete all OAuth sessions for a user"""
try:
with get_db() as db:
with get_db_context(db) as db:
result = db.query(OAuthSession).filter_by(user_id=user_id).delete()
db.commit()
return True
@@ -242,5 +285,18 @@ class OAuthSessionTable:
log.error(f"Error deleting OAuth sessions by user ID: {e}")
return False
def delete_sessions_by_provider(
self, provider: str, db: Optional[Session] = None
) -> bool:
"""Delete all OAuth sessions for a provider"""
try:
with get_db_context(db) as db:
db.query(OAuthSession).filter_by(provider=provider).delete()
db.commit()
return True
except Exception as e:
log.error(f"Error deleting OAuth sessions by provider {provider}: {e}")
return False
OAuthSessions = OAuthSessionTable()
+234
View File
@@ -0,0 +1,234 @@
"""Prompt history model for version tracking."""
import time
import uuid
from typing import Optional
import json
import difflib
from sqlalchemy.orm import Session
from open_webui.internal.db import Base, get_db_context
from open_webui.models.users import Users, UserResponse
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Column, Text, JSON, Index
####################
# PromptHistory DB Schema
####################
class PromptHistory(Base):
__tablename__ = "prompt_history"
id = Column(Text, primary_key=True)
prompt_id = Column(Text, nullable=False, index=True)
parent_id = Column(Text, nullable=True) # Reference to parent commit
snapshot = Column(JSON, nullable=False)
user_id = Column(Text, nullable=False)
commit_message = Column(Text, nullable=True)
created_at = Column(BigInteger, nullable=False)
class PromptHistoryModel(BaseModel):
id: str
prompt_id: str
parent_id: Optional[str] = None
snapshot: dict
user_id: str
commit_message: Optional[str] = None
created_at: int
model_config = ConfigDict(from_attributes=True)
class PromptHistoryResponse(PromptHistoryModel):
"""Response model with user info."""
user: Optional[UserResponse] = None
class PromptHistoryTable:
def create_history_entry(
self,
prompt_id: str,
snapshot: dict,
user_id: str,
parent_id: Optional[str] = None,
commit_message: Optional[str] = None,
db: Optional[Session] = None,
) -> Optional[PromptHistoryModel]:
"""Create a new history entry (commit) for a prompt."""
with get_db_context(db) as db:
history = PromptHistory(
id=str(uuid.uuid4()),
prompt_id=prompt_id,
parent_id=parent_id,
snapshot=snapshot,
user_id=user_id,
commit_message=commit_message,
created_at=int(time.time()),
)
db.add(history)
db.commit()
db.refresh(history)
return PromptHistoryModel.model_validate(history)
def get_history_by_prompt_id(
self,
prompt_id: str,
limit: int = 50,
offset: int = 0,
db: Optional[Session] = None,
) -> list[PromptHistoryResponse]:
"""Get all history entries for a prompt, ordered by created_at desc."""
with get_db_context(db) as db:
entries = (
db.query(PromptHistory)
.filter(PromptHistory.prompt_id == prompt_id)
.order_by(PromptHistory.created_at.desc())
.offset(offset)
.limit(limit)
.all()
)
# Get user info for each entry
user_ids = list(set(e.user_id for e in entries))
users = Users.get_users_by_user_ids(user_ids, db=db) if user_ids else []
users_dict = {user.id: user for user in users}
return [
PromptHistoryResponse(
**PromptHistoryModel.model_validate(entry).model_dump(),
user=(
users_dict.get(entry.user_id).model_dump()
if users_dict.get(entry.user_id)
else None
),
)
for entry in entries
]
def get_history_entry_by_id(
self,
history_id: str,
db: Optional[Session] = None,
) -> Optional[PromptHistoryModel]:
"""Get a specific history entry by ID."""
with get_db_context(db) as db:
entry = (
db.query(PromptHistory).filter(PromptHistory.id == history_id).first()
)
if entry:
return PromptHistoryModel.model_validate(entry)
return None
def get_latest_history_entry(
self,
prompt_id: str,
db: Optional[Session] = None,
) -> Optional[PromptHistoryModel]:
"""Get the most recent history entry for a prompt."""
with get_db_context(db) as db:
entry = (
db.query(PromptHistory)
.filter(PromptHistory.prompt_id == prompt_id)
.order_by(PromptHistory.created_at.desc())
.first()
)
if entry:
return PromptHistoryModel.model_validate(entry)
return None
def get_history_count(
self,
prompt_id: str,
db: Optional[Session] = None,
) -> int:
"""Get the number of history entries for a prompt."""
with get_db_context(db) as db:
return (
db.query(PromptHistory)
.filter(PromptHistory.prompt_id == prompt_id)
.count()
)
def compute_diff(
self,
from_id: str,
to_id: str,
db: Optional[Session] = None,
) -> Optional[dict]:
"""Compute diff between two history entries."""
with get_db_context(db) as db:
from_entry = (
db.query(PromptHistory).filter(PromptHistory.id == from_id).first()
)
to_entry = db.query(PromptHistory).filter(PromptHistory.id == to_id).first()
if not from_entry or not to_entry:
return None
from_snapshot = from_entry.snapshot
to_snapshot = to_entry.snapshot
# Compute diff for content field
from_content = from_snapshot.get("content", "")
to_content = to_snapshot.get("content", "")
diff_lines = list(
difflib.unified_diff(
from_content.splitlines(keepends=True),
to_content.splitlines(keepends=True),
fromfile=f"v{from_id[:8]}",
tofile=f"v{to_id[:8]}",
lineterm="",
)
)
return {
"from_id": from_id,
"to_id": to_id,
"from_snapshot": from_snapshot,
"to_snapshot": to_snapshot,
"content_diff": diff_lines,
"name_changed": from_snapshot.get("name") != to_snapshot.get("name"),
}
def delete_history_by_prompt_id(
self,
prompt_id: str,
db: Optional[Session] = None,
) -> bool:
"""Delete all history entries for a prompt."""
with get_db_context(db) as db:
db.query(PromptHistory).filter(
PromptHistory.prompt_id == prompt_id
).delete()
db.commit()
return True
def delete_history_entry(
self,
history_id: str,
db: Optional[Session] = None,
) -> bool:
"""Delete a history entry and reparent its children to grandparent."""
with get_db_context(db) as db:
entry = db.query(PromptHistory).filter_by(id=history_id).first()
if not entry:
return False
# Find children that reference this entry as parent
children = db.query(PromptHistory).filter_by(parent_id=history_id).all()
# Reparent children to grandparent
for child in children:
child.parent_id = entry.parent_id
db.delete(entry)
db.commit()
return True
PromptHistories = PromptHistoryTable()
+548 -61
View File
@@ -1,14 +1,17 @@
import time
import uuid
from typing import Optional
from open_webui.internal.db import Base, get_db
from sqlalchemy.orm import Session
from open_webui.internal.db import Base, JSONField, get_db, get_db_context
from open_webui.models.groups import Groups
from open_webui.models.users import Users, UserResponse
from open_webui.models.prompt_history import PromptHistories
from open_webui.models.access_grants import AccessGrantModel, AccessGrants
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Column, String, Text, JSON
from open_webui.utils.access_control import has_access
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON, or_, func, cast
####################
# Prompts DB Schema
@@ -18,38 +21,35 @@ from open_webui.utils.access_control import has_access
class Prompt(Base):
__tablename__ = "prompt"
command = Column(String, primary_key=True)
id = Column(Text, primary_key=True)
command = Column(String, unique=True, index=True)
user_id = Column(String)
title = Column(Text)
name = Column(Text)
content = Column(Text)
timestamp = Column(BigInteger)
access_control = Column(JSON, nullable=True) # Controls data access levels.
# Defines access control rules for this entry.
# - `None`: Public access, available to all users with the "user" role.
# - `{}`: Private access, restricted exclusively to the owner.
# - Custom permissions: Specific access control for reading and writing;
# Can specify group or user-level restrictions:
# {
# "read": {
# "group_ids": ["group_id1", "group_id2"],
# "user_ids": ["user_id1", "user_id2"]
# },
# "write": {
# "group_ids": ["group_id1", "group_id2"],
# "user_ids": ["user_id1", "user_id2"]
# }
# }
data = Column(JSON, nullable=True)
meta = Column(JSON, nullable=True)
tags = Column(JSON, nullable=True)
is_active = Column(Boolean, default=True)
version_id = Column(Text, nullable=True) # Points to active history entry
created_at = Column(BigInteger, nullable=True)
updated_at = Column(BigInteger, nullable=True)
class PromptModel(BaseModel):
id: Optional[str] = None
command: str
user_id: str
title: str
name: str
content: str
timestamp: int # timestamp in epoch
data: Optional[dict] = None
meta: Optional[dict] = None
tags: Optional[list[str]] = None
is_active: Optional[bool] = True
version_id: Optional[str] = None
created_at: Optional[int] = None
updated_at: Optional[int] = None
access_grants: list[AccessGrantModel] = Field(default_factory=list)
access_control: Optional[dict] = None
model_config = ConfigDict(from_attributes=True)
@@ -62,54 +62,164 @@ class PromptUserResponse(PromptModel):
user: Optional[UserResponse] = None
class PromptAccessResponse(PromptUserResponse):
write_access: Optional[bool] = False
class PromptListResponse(BaseModel):
items: list[PromptUserResponse]
total: int
class PromptAccessListResponse(BaseModel):
items: list[PromptAccessResponse]
total: int
class PromptForm(BaseModel):
command: str
title: str
name: str # Changed from title
content: str
access_control: Optional[dict] = None
data: Optional[dict] = None
meta: Optional[dict] = None
tags: Optional[list[str]] = None
access_grants: Optional[list[dict]] = None
version_id: Optional[str] = None # Active version
commit_message: Optional[str] = None # For history tracking
is_production: Optional[bool] = True # Whether to set new version as production
class PromptsTable:
def _get_access_grants(
self, prompt_id: str, db: Optional[Session] = None
) -> list[AccessGrantModel]:
return AccessGrants.get_grants_by_resource("prompt", prompt_id, db=db)
def _to_prompt_model(
self,
prompt: Prompt,
access_grants: Optional[list[AccessGrantModel]] = None,
db: Optional[Session] = None,
) -> PromptModel:
prompt_data = PromptModel.model_validate(prompt).model_dump(
exclude={"access_grants"}
)
prompt_data["access_grants"] = (
access_grants
if access_grants is not None
else self._get_access_grants(prompt_data["id"], db=db)
)
return PromptModel.model_validate(prompt_data)
def insert_new_prompt(
self, user_id: str, form_data: PromptForm
self, user_id: str, form_data: PromptForm, db: Optional[Session] = None
) -> Optional[PromptModel]:
now = int(time.time())
prompt_id = str(uuid.uuid4())
prompt = PromptModel(
**{
"user_id": user_id,
**form_data.model_dump(),
"timestamp": int(time.time()),
}
id=prompt_id,
user_id=user_id,
command=form_data.command,
name=form_data.name,
content=form_data.content,
data=form_data.data or {},
meta=form_data.meta or {},
tags=form_data.tags or [],
access_grants=[],
is_active=True,
created_at=now,
updated_at=now,
)
try:
with get_db() as db:
result = Prompt(**prompt.model_dump())
with get_db_context(db) as db:
result = Prompt(**prompt.model_dump(exclude={"access_grants"}))
db.add(result)
db.commit()
db.refresh(result)
AccessGrants.set_access_grants(
"prompt", prompt_id, form_data.access_grants, db=db
)
if result:
return PromptModel.model_validate(result)
current_access_grants = self._get_access_grants(prompt_id, db=db)
snapshot = {
"name": form_data.name,
"content": form_data.content,
"command": form_data.command,
"data": form_data.data or {},
"meta": form_data.meta or {},
"tags": form_data.tags or [],
"access_grants": [
grant.model_dump() for grant in current_access_grants
],
}
history_entry = PromptHistories.create_history_entry(
prompt_id=prompt_id,
snapshot=snapshot,
user_id=user_id,
parent_id=None, # Initial commit has no parent
commit_message=form_data.commit_message or "Initial version",
db=db,
)
# Set the initial version as the production version
if history_entry:
result.version_id = history_entry.id
db.commit()
db.refresh(result)
return self._to_prompt_model(result, db=db)
else:
return None
except Exception:
return None
def get_prompt_by_command(self, command: str) -> Optional[PromptModel]:
def get_prompt_by_id(
self, prompt_id: str, db: Optional[Session] = None
) -> Optional[PromptModel]:
"""Get prompt by UUID."""
try:
with get_db() as db:
prompt = db.query(Prompt).filter_by(command=command).first()
return PromptModel.model_validate(prompt)
with get_db_context(db) as db:
prompt = db.query(Prompt).filter_by(id=prompt_id).first()
if prompt:
return self._to_prompt_model(prompt, db=db)
return None
except Exception:
return None
def get_prompts(self) -> list[PromptUserResponse]:
with get_db() as db:
all_prompts = db.query(Prompt).order_by(Prompt.timestamp.desc()).all()
def get_prompt_by_command(
self, command: str, db: Optional[Session] = None
) -> Optional[PromptModel]:
try:
with get_db_context(db) as db:
prompt = db.query(Prompt).filter_by(command=command).first()
if prompt:
return self._to_prompt_model(prompt, db=db)
return None
except Exception:
return None
def get_prompts(self, db: Optional[Session] = None) -> list[PromptUserResponse]:
with get_db_context(db) as db:
all_prompts = (
db.query(Prompt)
.filter(Prompt.is_active == True)
.order_by(Prompt.updated_at.desc())
.all()
)
user_ids = list(set(prompt.user_id for prompt in all_prompts))
prompt_ids = [prompt.id for prompt in all_prompts]
users = Users.get_users_by_user_ids(user_ids) if user_ids else []
users = Users.get_users_by_user_ids(user_ids, db=db) if user_ids else []
users_dict = {user.id: user for user in users}
grants_map = AccessGrants.get_grants_by_resources(
"prompt", prompt_ids, db=db
)
prompts = []
for prompt in all_prompts:
@@ -117,7 +227,11 @@ class PromptsTable:
prompts.append(
PromptUserResponse.model_validate(
{
**PromptModel.model_validate(prompt).model_dump(),
**self._to_prompt_model(
prompt,
access_grants=grants_map.get(prompt.id, []),
db=db,
).model_dump(),
"user": user.model_dump() if user else None,
}
)
@@ -126,42 +240,415 @@ class PromptsTable:
return prompts
def get_prompts_by_user_id(
self, user_id: str, permission: str = "write"
self, user_id: str, permission: str = "write", db: Optional[Session] = None
) -> list[PromptUserResponse]:
prompts = self.get_prompts()
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user_id)}
prompts = self.get_prompts(db=db)
user_group_ids = {
group.id for group in Groups.get_groups_by_member_id(user_id, db=db)
}
return [
prompt
for prompt in prompts
if prompt.user_id == user_id
or has_access(user_id, permission, prompt.access_control, user_group_ids)
or AccessGrants.has_access(
user_id=user_id,
resource_type="prompt",
resource_id=prompt.id,
permission=permission,
user_group_ids=user_group_ids,
db=db,
)
]
def search_prompts(
self,
user_id: str,
filter: dict = {},
skip: int = 0,
limit: int = 30,
db: Optional[Session] = None,
) -> PromptListResponse:
with get_db_context(db) as db:
from open_webui.models.users import User, UserModel
# Join with User table for user filtering and sorting
query = db.query(Prompt, User).outerjoin(User, User.id == Prompt.user_id)
if filter:
query_key = filter.get("query")
if query_key:
query = query.filter(
or_(
Prompt.name.ilike(f"%{query_key}%"),
Prompt.command.ilike(f"%{query_key}%"),
Prompt.content.ilike(f"%{query_key}%"),
User.name.ilike(f"%{query_key}%"),
User.email.ilike(f"%{query_key}%"),
)
)
view_option = filter.get("view_option")
if view_option == "created":
query = query.filter(Prompt.user_id == user_id)
elif view_option == "shared":
query = query.filter(Prompt.user_id != user_id)
# Apply access grant filtering
query = AccessGrants.has_permission_filter(
db=db,
query=query,
DocumentModel=Prompt,
filter=filter,
resource_type="prompt",
permission="read",
)
tag = filter.get("tag")
if tag:
# Search for tag in JSON array field
like_pattern = f'%"{tag.lower()}"%'
tags_text = func.lower(cast(Prompt.tags, String))
query = query.filter(tags_text.like(like_pattern))
order_by = filter.get("order_by")
direction = filter.get("direction")
if order_by == "name":
if direction == "asc":
query = query.order_by(Prompt.name.asc())
else:
query = query.order_by(Prompt.name.desc())
elif order_by == "created_at":
if direction == "asc":
query = query.order_by(Prompt.created_at.asc())
else:
query = query.order_by(Prompt.created_at.desc())
elif order_by == "updated_at":
if direction == "asc":
query = query.order_by(Prompt.updated_at.asc())
else:
query = query.order_by(Prompt.updated_at.desc())
else:
query = query.order_by(Prompt.updated_at.desc())
else:
query = query.order_by(Prompt.updated_at.desc())
# Count BEFORE pagination
total = query.count()
if skip:
query = query.offset(skip)
if limit:
query = query.limit(limit)
items = query.all()
prompt_ids = [prompt.id for prompt, _ in items]
grants_map = AccessGrants.get_grants_by_resources(
"prompt", prompt_ids, db=db
)
prompts = []
for prompt, user in items:
prompts.append(
PromptUserResponse(
**self._to_prompt_model(
prompt,
access_grants=grants_map.get(prompt.id, []),
db=db,
).model_dump(),
user=(
UserResponse(**UserModel.model_validate(user).model_dump())
if user
else None
),
)
)
return PromptListResponse(items=prompts, total=total)
def update_prompt_by_command(
self, command: str, form_data: PromptForm
self,
command: str,
form_data: PromptForm,
user_id: str,
db: Optional[Session] = None,
) -> Optional[PromptModel]:
try:
with get_db() as db:
with get_db_context(db) as db:
prompt = db.query(Prompt).filter_by(command=command).first()
prompt.title = form_data.title
if not prompt:
return None
latest_history = PromptHistories.get_latest_history_entry(
prompt.id, db=db
)
parent_id = latest_history.id if latest_history else None
current_access_grants = self._get_access_grants(prompt.id, db=db)
# Check if content changed to decide on history creation
content_changed = (
prompt.name != form_data.name
or prompt.content != form_data.content
or form_data.access_grants is not None
)
# Update prompt fields
prompt.name = form_data.name
prompt.content = form_data.content
prompt.access_control = form_data.access_control
prompt.timestamp = int(time.time())
prompt.data = form_data.data or prompt.data
prompt.meta = form_data.meta or prompt.meta
prompt.updated_at = int(time.time())
if form_data.access_grants is not None:
AccessGrants.set_access_grants(
"prompt", prompt.id, form_data.access_grants, db=db
)
current_access_grants = self._get_access_grants(prompt.id, db=db)
db.commit()
return PromptModel.model_validate(prompt)
# Create history entry only if content changed
if content_changed:
snapshot = {
"name": form_data.name,
"content": form_data.content,
"command": command,
"data": form_data.data or {},
"meta": form_data.meta or {},
"access_grants": [
grant.model_dump() for grant in current_access_grants
],
}
history_entry = PromptHistories.create_history_entry(
prompt_id=prompt.id,
snapshot=snapshot,
user_id=user_id,
parent_id=parent_id,
commit_message=form_data.commit_message,
db=db,
)
# Set as production if flag is True (default)
if form_data.is_production and history_entry:
prompt.version_id = history_entry.id
db.commit()
return self._to_prompt_model(prompt, db=db)
except Exception:
return None
def delete_prompt_by_command(self, command: str) -> bool:
def update_prompt_by_id(
self,
prompt_id: str,
form_data: PromptForm,
user_id: str,
db: Optional[Session] = None,
) -> Optional[PromptModel]:
try:
with get_db() as db:
db.query(Prompt).filter_by(command=command).delete()
with get_db_context(db) as db:
prompt = db.query(Prompt).filter_by(id=prompt_id).first()
if not prompt:
return None
latest_history = PromptHistories.get_latest_history_entry(
prompt.id, db=db
)
parent_id = latest_history.id if latest_history else None
current_access_grants = self._get_access_grants(prompt.id, db=db)
# Check if content changed to decide on history creation
content_changed = (
prompt.name != form_data.name
or prompt.command != form_data.command
or prompt.content != form_data.content
or form_data.access_grants is not None
or (form_data.tags is not None and prompt.tags != form_data.tags)
)
# Update prompt fields
prompt.name = form_data.name
prompt.command = form_data.command
prompt.content = form_data.content
prompt.data = form_data.data or prompt.data
prompt.meta = form_data.meta or prompt.meta
if form_data.tags is not None:
prompt.tags = form_data.tags
if form_data.access_grants is not None:
AccessGrants.set_access_grants(
"prompt", prompt.id, form_data.access_grants, db=db
)
current_access_grants = self._get_access_grants(prompt.id, db=db)
prompt.updated_at = int(time.time())
db.commit()
return True
# Create history entry only if content changed
if content_changed:
snapshot = {
"name": form_data.name,
"content": form_data.content,
"command": prompt.command,
"data": form_data.data or {},
"meta": form_data.meta or {},
"tags": prompt.tags or [],
"access_grants": [
grant.model_dump() for grant in current_access_grants
],
}
history_entry = PromptHistories.create_history_entry(
prompt_id=prompt.id,
snapshot=snapshot,
user_id=user_id,
parent_id=parent_id,
commit_message=form_data.commit_message,
db=db,
)
# Set as production if flag is True (default)
if form_data.is_production and history_entry:
prompt.version_id = history_entry.id
db.commit()
return self._to_prompt_model(prompt, db=db)
except Exception:
return None
def update_prompt_metadata(
self,
prompt_id: str,
name: str,
command: str,
tags: Optional[list[str]] = None,
db: Optional[Session] = None,
) -> Optional[PromptModel]:
"""Update only name, command, and tags (no history created)."""
try:
with get_db_context(db) as db:
prompt = db.query(Prompt).filter_by(id=prompt_id).first()
if not prompt:
return None
prompt.name = name
prompt.command = command
if tags is not None:
prompt.tags = tags
prompt.updated_at = int(time.time())
db.commit()
return self._to_prompt_model(prompt, db=db)
except Exception:
return None
def update_prompt_version(
self,
prompt_id: str,
version_id: str,
db: Optional[Session] = None,
) -> Optional[PromptModel]:
"""Set the active version of a prompt and restore content from that version's snapshot."""
try:
with get_db_context(db) as db:
prompt = db.query(Prompt).filter_by(id=prompt_id).first()
if not prompt:
return None
history_entry = PromptHistories.get_history_entry_by_id(
version_id, db=db
)
if not history_entry:
return None
# Restore prompt content from the snapshot
snapshot = history_entry.snapshot
if snapshot:
prompt.name = snapshot.get("name", prompt.name)
prompt.content = snapshot.get("content", prompt.content)
prompt.data = snapshot.get("data", prompt.data)
prompt.meta = snapshot.get("meta", prompt.meta)
prompt.tags = snapshot.get("tags", prompt.tags)
# Note: command and access_grants are not restored from snapshot
prompt.version_id = version_id
prompt.updated_at = int(time.time())
db.commit()
return self._to_prompt_model(prompt, db=db)
except Exception:
return None
def toggle_prompt_active(
self, prompt_id: str, db: Optional[Session] = None
) -> Optional[PromptModel]:
"""Toggle the is_active flag on a prompt."""
try:
with get_db_context(db) as db:
prompt = db.query(Prompt).filter_by(id=prompt_id).first()
if prompt:
prompt.is_active = not prompt.is_active
prompt.updated_at = int(time.time())
db.commit()
db.refresh(prompt)
return self._to_prompt_model(prompt, db=db)
return None
except Exception:
return None
def delete_prompt_by_command(
self, command: str, db: Optional[Session] = None
) -> bool:
"""Permanently delete a prompt and its history."""
try:
with get_db_context(db) as db:
prompt = db.query(Prompt).filter_by(command=command).first()
if prompt:
PromptHistories.delete_history_by_prompt_id(prompt.id, db=db)
AccessGrants.revoke_all_access("prompt", prompt.id, db=db)
db.delete(prompt)
db.commit()
return True
return False
except Exception:
return False
def delete_prompt_by_id(self, prompt_id: str, db: Optional[Session] = None) -> bool:
"""Permanently delete a prompt and its history."""
try:
with get_db_context(db) as db:
prompt = db.query(Prompt).filter_by(id=prompt_id).first()
if prompt:
PromptHistories.delete_history_by_prompt_id(prompt.id, db=db)
AccessGrants.revoke_all_access("prompt", prompt.id, db=db)
db.delete(prompt)
db.commit()
return True
return False
except Exception:
return False
def get_tags(self, db: Optional[Session] = None) -> list[str]:
try:
with get_db_context(db) as db:
prompts = db.query(Prompt).filter_by(is_active=True).all()
tags = set()
for prompt in prompts:
if prompt.tags:
for tag in prompt.tags:
if tag:
tags.add(tag)
return sorted(list(tags))
except Exception:
return []
Prompts = PromptsTable()
+363
View File
@@ -0,0 +1,363 @@
import logging
import time
from typing import Optional
from sqlalchemy.orm import Session
from open_webui.internal.db import Base, get_db, get_db_context
from open_webui.models.users import Users, UserResponse
from open_webui.models.groups import Groups
from open_webui.models.access_grants import AccessGrantModel, AccessGrants
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy import JSON, BigInteger, Boolean, Column, String, Text, or_
log = logging.getLogger(__name__)
####################
# Skills DB Schema
####################
class Skill(Base):
__tablename__ = "skill"
id = Column(String, primary_key=True, unique=True)
user_id = Column(String)
name = Column(Text, unique=True)
description = Column(Text, nullable=True)
content = Column(Text)
meta = Column(JSON)
is_active = Column(Boolean, default=True)
updated_at = Column(BigInteger)
created_at = Column(BigInteger)
class SkillMeta(BaseModel):
tags: Optional[list[str]] = []
class SkillModel(BaseModel):
id: str
user_id: str
name: str
description: Optional[str] = None
content: str
meta: SkillMeta
is_active: bool = True
access_grants: list[AccessGrantModel] = Field(default_factory=list)
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
model_config = ConfigDict(from_attributes=True)
####################
# Forms
####################
class SkillUserModel(SkillModel):
user: Optional[UserResponse] = None
class SkillResponse(BaseModel):
id: str
user_id: str
name: str
description: Optional[str] = None
meta: SkillMeta
is_active: bool = True
access_grants: list[AccessGrantModel] = Field(default_factory=list)
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
class SkillUserResponse(SkillResponse):
user: Optional[UserResponse] = None
model_config = ConfigDict(extra="allow")
class SkillAccessResponse(SkillUserResponse):
write_access: Optional[bool] = False
class SkillForm(BaseModel):
id: str
name: str
description: Optional[str] = None
content: str
meta: SkillMeta = SkillMeta()
is_active: bool = True
access_grants: Optional[list[dict]] = None
class SkillListResponse(BaseModel):
items: list[SkillUserResponse] = []
total: int = 0
class SkillAccessListResponse(BaseModel):
items: list[SkillAccessResponse] = []
total: int = 0
class SkillsTable:
def _get_access_grants(
self, skill_id: str, db: Optional[Session] = None
) -> list[AccessGrantModel]:
return AccessGrants.get_grants_by_resource("skill", skill_id, db=db)
def _to_skill_model(
self,
skill: Skill,
access_grants: Optional[list[AccessGrantModel]] = None,
db: Optional[Session] = None,
) -> SkillModel:
skill_data = SkillModel.model_validate(skill).model_dump(
exclude={"access_grants"}
)
skill_data["access_grants"] = (
access_grants
if access_grants is not None
else self._get_access_grants(skill_data["id"], db=db)
)
return SkillModel.model_validate(skill_data)
def insert_new_skill(
self,
user_id: str,
form_data: SkillForm,
db: Optional[Session] = None,
) -> Optional[SkillModel]:
with get_db_context(db) as db:
try:
result = Skill(
**{
**form_data.model_dump(exclude={"access_grants"}),
"user_id": user_id,
"updated_at": int(time.time()),
"created_at": int(time.time()),
}
)
db.add(result)
db.commit()
db.refresh(result)
AccessGrants.set_access_grants(
"skill", result.id, form_data.access_grants, db=db
)
if result:
return self._to_skill_model(result, db=db)
else:
return None
except Exception as e:
log.exception(f"Error creating a new skill: {e}")
return None
def get_skill_by_id(
self, id: str, db: Optional[Session] = None
) -> Optional[SkillModel]:
try:
with get_db_context(db) as db:
skill = db.get(Skill, id)
return self._to_skill_model(skill, db=db) if skill else None
except Exception:
return None
def get_skill_by_name(
self, name: str, db: Optional[Session] = None
) -> Optional[SkillModel]:
try:
with get_db_context(db) as db:
skill = db.query(Skill).filter_by(name=name).first()
return self._to_skill_model(skill, db=db) if skill else None
except Exception:
return None
def get_skills(self, db: Optional[Session] = None) -> list[SkillUserModel]:
with get_db_context(db) as db:
all_skills = db.query(Skill).order_by(Skill.updated_at.desc()).all()
user_ids = list(set(skill.user_id for skill in all_skills))
skill_ids = [skill.id for skill in all_skills]
users = Users.get_users_by_user_ids(user_ids, db=db) if user_ids else []
users_dict = {user.id: user for user in users}
grants_map = AccessGrants.get_grants_by_resources("skill", skill_ids, db=db)
skills = []
for skill in all_skills:
user = users_dict.get(skill.user_id)
skills.append(
SkillUserModel.model_validate(
{
**self._to_skill_model(
skill,
access_grants=grants_map.get(skill.id, []),
db=db,
).model_dump(),
"user": user.model_dump() if user else None,
}
)
)
return skills
def get_skills_by_user_id(
self, user_id: str, permission: str = "write", db: Optional[Session] = None
) -> list[SkillUserModel]:
skills = self.get_skills(db=db)
user_group_ids = {
group.id for group in Groups.get_groups_by_member_id(user_id, db=db)
}
return [
skill
for skill in skills
if skill.user_id == user_id
or AccessGrants.has_access(
user_id=user_id,
resource_type="skill",
resource_id=skill.id,
permission=permission,
user_group_ids=user_group_ids,
db=db,
)
]
def search_skills(
self,
user_id: str,
filter: dict = {},
skip: int = 0,
limit: int = 30,
db: Optional[Session] = None,
) -> SkillListResponse:
try:
with get_db_context(db) as db:
from open_webui.models.users import User, UserModel
# Join with User table for user filtering
query = db.query(Skill, User).outerjoin(User, User.id == Skill.user_id)
if filter:
query_key = filter.get("query")
if query_key:
query = query.filter(
or_(
Skill.name.ilike(f"%{query_key}%"),
Skill.description.ilike(f"%{query_key}%"),
Skill.id.ilike(f"%{query_key}%"),
User.name.ilike(f"%{query_key}%"),
User.email.ilike(f"%{query_key}%"),
)
)
view_option = filter.get("view_option")
if view_option == "created":
query = query.filter(Skill.user_id == user_id)
elif view_option == "shared":
query = query.filter(Skill.user_id != user_id)
# Apply access grant filtering
query = AccessGrants.has_permission_filter(
db=db,
query=query,
DocumentModel=Skill,
filter=filter,
resource_type="skill",
permission="read",
)
query = query.order_by(Skill.updated_at.desc())
# Count BEFORE pagination
total = query.count()
if skip:
query = query.offset(skip)
if limit:
query = query.limit(limit)
items = query.all()
skill_ids = [skill.id for skill, _ in items]
grants_map = AccessGrants.get_grants_by_resources(
"skill", skill_ids, db=db
)
skills = []
for skill, user in items:
skills.append(
SkillUserResponse(
**self._to_skill_model(
skill,
access_grants=grants_map.get(skill.id, []),
db=db,
).model_dump(),
user=(
UserResponse(
**UserModel.model_validate(user).model_dump()
)
if user
else None
),
)
)
return SkillListResponse(items=skills, total=total)
except Exception as e:
log.exception(f"Error searching skills: {e}")
return SkillListResponse(items=[], total=0)
def update_skill_by_id(
self, id: str, updated: dict, db: Optional[Session] = None
) -> Optional[SkillModel]:
try:
with get_db_context(db) as db:
access_grants = updated.pop("access_grants", None)
db.query(Skill).filter_by(id=id).update(
{**updated, "updated_at": int(time.time())}
)
db.commit()
if access_grants is not None:
AccessGrants.set_access_grants("skill", id, access_grants, db=db)
skill = db.query(Skill).get(id)
db.refresh(skill)
return self._to_skill_model(skill, db=db)
except Exception:
return None
def toggle_skill_by_id(
self, id: str, db: Optional[Session] = None
) -> Optional[SkillModel]:
with get_db_context(db) as db:
try:
skill = db.query(Skill).filter_by(id=id).first()
if not skill:
return None
skill.is_active = not skill.is_active
skill.updated_at = int(time.time())
db.commit()
db.refresh(skill)
return self._to_skill_model(skill, db=db)
except Exception:
return None
def delete_skill_by_id(self, id: str, db: Optional[Session] = None) -> bool:
try:
with get_db_context(db) as db:
AccessGrants.revoke_all_access("skill", id, db=db)
db.query(Skill).filter_by(id=id).delete()
db.commit()
return True
except Exception:
return False
Skills = SkillsTable()
+58 -13
View File
@@ -3,15 +3,14 @@ import time
import uuid
from typing import Optional
from open_webui.internal.db import Base, get_db
from sqlalchemy.orm import Session
from open_webui.internal.db import Base, JSONField, get_db, get_db_context
from open_webui.env import SRC_LOG_LEVELS
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Column, String, JSON, PrimaryKeyConstraint, Index
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
####################
@@ -52,8 +51,10 @@ class TagChatIdForm(BaseModel):
class TagTable:
def insert_new_tag(self, name: str, user_id: str) -> Optional[TagModel]:
with get_db() as db:
def insert_new_tag(
self, name: str, user_id: str, db: Optional[Session] = None
) -> Optional[TagModel]:
with get_db_context(db) as db:
id = name.replace(" ", "_").lower()
tag = TagModel(**{"id": id, "user_id": user_id, "name": name})
try:
@@ -70,27 +71,29 @@ class TagTable:
return None
def get_tag_by_name_and_user_id(
self, name: str, user_id: str
self, name: str, user_id: str, db: Optional[Session] = None
) -> Optional[TagModel]:
try:
id = name.replace(" ", "_").lower()
with get_db() as db:
with get_db_context(db) as db:
tag = db.query(Tag).filter_by(id=id, user_id=user_id).first()
return TagModel.model_validate(tag)
except Exception:
return None
def get_tags_by_user_id(self, user_id: str) -> list[TagModel]:
with get_db() as db:
def get_tags_by_user_id(
self, user_id: str, db: Optional[Session] = None
) -> list[TagModel]:
with get_db_context(db) as db:
return [
TagModel.model_validate(tag)
for tag in (db.query(Tag).filter_by(user_id=user_id).all())
]
def get_tags_by_ids_and_user_id(
self, ids: list[str], user_id: str
self, ids: list[str], user_id: str, db: Optional[Session] = None
) -> list[TagModel]:
with get_db() as db:
with get_db_context(db) as db:
return [
TagModel.model_validate(tag)
for tag in (
@@ -98,9 +101,11 @@ class TagTable:
)
]
def delete_tag_by_name_and_user_id(self, name: str, user_id: str) -> bool:
def delete_tag_by_name_and_user_id(
self, name: str, user_id: str, db: Optional[Session] = None
) -> bool:
try:
with get_db() as db:
with get_db_context(db) as db:
id = name.replace(" ", "_").lower()
res = db.query(Tag).filter_by(id=id, user_id=user_id).delete()
log.debug(f"res: {res}")
@@ -110,5 +115,45 @@ class TagTable:
log.error(f"delete_tag: {e}")
return False
def delete_tags_by_ids_and_user_id(
self, ids: list[str], user_id: str, db: Optional[Session] = None
) -> bool:
"""Delete all tags whose id is in *ids* for the given user, in one query."""
if not ids:
return True
try:
with get_db_context(db) as db:
db.query(Tag).filter(Tag.id.in_(ids), Tag.user_id == user_id).delete(
synchronize_session=False
)
db.commit()
return True
except Exception as e:
log.error(f"delete_tags_by_ids: {e}")
return False
def ensure_tags_exist(
self, names: list[str], user_id: str, db: Optional[Session] = None
) -> None:
"""Create tag rows for any *names* that don't already exist for *user_id*."""
if not names:
return
ids = [n.replace(" ", "_").lower() for n in names]
with get_db_context(db) as db:
existing = {
t.id
for t in db.query(Tag.id)
.filter(Tag.id.in_(ids), Tag.user_id == user_id)
.all()
}
new_tags = [
Tag(id=tag_id, name=name, user_id=user_id)
for tag_id, name in zip(ids, names)
if tag_id not in existing
]
if new_tags:
db.add_all(new_tags)
db.commit()
Tags = TagTable()
+117 -71
View File
@@ -2,19 +2,16 @@ import logging
import time
from typing import Optional
from open_webui.internal.db import Base, JSONField, get_db
from sqlalchemy.orm import Session, defer
from open_webui.internal.db import Base, JSONField, get_db, get_db_context
from open_webui.models.users import Users, UserResponse
from open_webui.models.groups import Groups
from open_webui.models.access_grants import AccessGrantModel, AccessGrants
from open_webui.env import SRC_LOG_LEVELS
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Column, String, Text, JSON
from open_webui.utils.access_control import has_access
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy import BigInteger, Column, String, Text
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
####################
# Tools DB Schema
@@ -24,7 +21,7 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
class Tool(Base):
__tablename__ = "tool"
id = Column(String, primary_key=True)
id = Column(String, primary_key=True, unique=True)
user_id = Column(String)
name = Column(Text)
content = Column(Text)
@@ -32,23 +29,6 @@ class Tool(Base):
meta = Column(JSONField)
valves = Column(JSONField)
access_control = Column(JSON, nullable=True) # Controls data access levels.
# Defines access control rules for this entry.
# - `None`: Public access, available to all users with the "user" role.
# - `{}`: Private access, restricted exclusively to the owner.
# - Custom permissions: Specific access control for reading and writing;
# Can specify group or user-level restrictions:
# {
# "read": {
# "group_ids": ["group_id1", "group_id2"],
# "user_ids": ["user_id1", "user_id2"]
# },
# "write": {
# "group_ids": ["group_id1", "group_id2"],
# "user_ids": ["user_id1", "user_id2"]
# }
# }
updated_at = Column(BigInteger)
created_at = Column(BigInteger)
@@ -65,7 +45,7 @@ class ToolModel(BaseModel):
content: str
specs: list[dict]
meta: ToolMeta
access_control: Optional[dict] = None
access_grants: list[AccessGrantModel] = Field(default_factory=list)
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
@@ -87,7 +67,7 @@ class ToolResponse(BaseModel):
user_id: str
name: str
meta: ToolMeta
access_control: Optional[dict] = None
access_grants: list[AccessGrantModel] = Field(default_factory=list)
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
@@ -95,13 +75,19 @@ class ToolResponse(BaseModel):
class ToolUserResponse(ToolResponse):
user: Optional[UserResponse] = None
model_config = ConfigDict(extra="allow")
class ToolAccessResponse(ToolUserResponse):
write_access: Optional[bool] = False
class ToolForm(BaseModel):
id: str
name: str
content: str
meta: ToolMeta
access_control: Optional[dict] = None
access_grants: Optional[list[dict]] = None
class ToolValves(BaseModel):
@@ -109,49 +95,82 @@ class ToolValves(BaseModel):
class ToolsTable:
def insert_new_tool(
self, user_id: str, form_data: ToolForm, specs: list[dict]
) -> Optional[ToolModel]:
with get_db() as db:
tool = ToolModel(
**{
**form_data.model_dump(),
"specs": specs,
"user_id": user_id,
"updated_at": int(time.time()),
"created_at": int(time.time()),
}
)
def _get_access_grants(
self, tool_id: str, db: Optional[Session] = None
) -> list[AccessGrantModel]:
return AccessGrants.get_grants_by_resource("tool", tool_id, db=db)
def _to_tool_model(
self,
tool: Tool,
access_grants: Optional[list[AccessGrantModel]] = None,
db: Optional[Session] = None,
) -> ToolModel:
tool_data = ToolModel.model_validate(tool).model_dump(exclude={"access_grants"})
tool_data["access_grants"] = (
access_grants
if access_grants is not None
else self._get_access_grants(tool_data["id"], db=db)
)
return ToolModel.model_validate(tool_data)
def insert_new_tool(
self,
user_id: str,
form_data: ToolForm,
specs: list[dict],
db: Optional[Session] = None,
) -> Optional[ToolModel]:
with get_db_context(db) as db:
try:
result = Tool(**tool.model_dump())
result = Tool(
**{
**form_data.model_dump(exclude={"access_grants"}),
"specs": specs,
"user_id": user_id,
"updated_at": int(time.time()),
"created_at": int(time.time()),
}
)
db.add(result)
db.commit()
db.refresh(result)
AccessGrants.set_access_grants(
"tool", result.id, form_data.access_grants, db=db
)
if result:
return ToolModel.model_validate(result)
return self._to_tool_model(result, db=db)
else:
return None
except Exception as e:
log.exception(f"Error creating a new tool: {e}")
return None
def get_tool_by_id(self, id: str) -> Optional[ToolModel]:
def get_tool_by_id(
self, id: str, db: Optional[Session] = None
) -> Optional[ToolModel]:
try:
with get_db() as db:
with get_db_context(db) as db:
tool = db.get(Tool, id)
return ToolModel.model_validate(tool)
return self._to_tool_model(tool, db=db) if tool else None
except Exception:
return None
def get_tools(self) -> list[ToolUserModel]:
with get_db() as db:
all_tools = db.query(Tool).order_by(Tool.updated_at.desc()).all()
def get_tools(
self, defer_content: bool = False, db: Optional[Session] = None
) -> list[ToolUserModel]:
with get_db_context(db) as db:
query = db.query(Tool).order_by(Tool.updated_at.desc())
if defer_content:
query = query.options(defer(Tool.content), defer(Tool.specs))
all_tools = query.all()
user_ids = list(set(tool.user_id for tool in all_tools))
tool_ids = [tool.id for tool in all_tools]
users = Users.get_users_by_user_ids(user_ids) if user_ids else []
users = Users.get_users_by_user_ids(user_ids, db=db) if user_ids else []
users_dict = {user.id: user for user in users}
grants_map = AccessGrants.get_grants_by_resources("tool", tool_ids, db=db)
tools = []
for tool in all_tools:
@@ -159,7 +178,11 @@ class ToolsTable:
tools.append(
ToolUserModel.model_validate(
{
**ToolModel.model_validate(tool).model_dump(),
**self._to_tool_model(
tool,
access_grants=grants_map.get(tool.id, []),
db=db,
).model_dump(),
"user": user.model_dump() if user else None,
}
)
@@ -167,43 +190,60 @@ class ToolsTable:
return tools
def get_tools_by_user_id(
self, user_id: str, permission: str = "write"
self,
user_id: str,
permission: str = "write",
defer_content: bool = False,
db: Optional[Session] = None,
) -> list[ToolUserModel]:
tools = self.get_tools()
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user_id)}
tools = self.get_tools(defer_content=defer_content, db=db)
user_group_ids = {
group.id for group in Groups.get_groups_by_member_id(user_id, db=db)
}
return [
tool
for tool in tools
if tool.user_id == user_id
or has_access(user_id, permission, tool.access_control, user_group_ids)
or AccessGrants.has_access(
user_id=user_id,
resource_type="tool",
resource_id=tool.id,
permission=permission,
user_group_ids=user_group_ids,
db=db,
)
]
def get_tool_valves_by_id(self, id: str) -> Optional[dict]:
def get_tool_valves_by_id(
self, id: str, db: Optional[Session] = None
) -> Optional[dict]:
try:
with get_db() as db:
with get_db_context(db) as db:
tool = db.get(Tool, id)
return tool.valves if tool.valves else {}
except Exception as e:
log.exception(f"Error getting tool valves by id {id}")
return None
def update_tool_valves_by_id(self, id: str, valves: dict) -> Optional[ToolValves]:
def update_tool_valves_by_id(
self, id: str, valves: dict, db: Optional[Session] = None
) -> Optional[ToolValves]:
try:
with get_db() as db:
with get_db_context(db) as db:
db.query(Tool).filter_by(id=id).update(
{"valves": valves, "updated_at": int(time.time())}
)
db.commit()
return self.get_tool_by_id(id)
return self.get_tool_by_id(id, db=db)
except Exception:
return None
def get_user_valves_by_id_and_user_id(
self, id: str, user_id: str
self, id: str, user_id: str, db: Optional[Session] = None
) -> Optional[dict]:
try:
user = Users.get_user_by_id(user_id)
user = Users.get_user_by_id(user_id, db=db)
user_settings = user.settings.model_dump() if user.settings else {}
# Check if user has "tools" and "valves" settings
@@ -220,10 +260,10 @@ class ToolsTable:
return None
def update_user_valves_by_id_and_user_id(
self, id: str, user_id: str, valves: dict
self, id: str, user_id: str, valves: dict, db: Optional[Session] = None
) -> Optional[dict]:
try:
user = Users.get_user_by_id(user_id)
user = Users.get_user_by_id(user_id, db=db)
user_settings = user.settings.model_dump() if user.settings else {}
# Check if user has "tools" and "valves" settings
@@ -235,7 +275,7 @@ class ToolsTable:
user_settings["tools"]["valves"][id] = valves
# Update the user settings in the database
Users.update_user_by_id(user_id, {"settings": user_settings})
Users.update_user_by_id(user_id, {"settings": user_settings}, db=db)
return user_settings["tools"]["valves"][id]
except Exception as e:
@@ -244,23 +284,29 @@ class ToolsTable:
)
return None
def update_tool_by_id(self, id: str, updated: dict) -> Optional[ToolModel]:
def update_tool_by_id(
self, id: str, updated: dict, db: Optional[Session] = None
) -> Optional[ToolModel]:
try:
with get_db() as db:
with get_db_context(db) as db:
access_grants = updated.pop("access_grants", None)
db.query(Tool).filter_by(id=id).update(
{**updated, "updated_at": int(time.time())}
)
db.commit()
if access_grants is not None:
AccessGrants.set_access_grants("tool", id, access_grants, db=db)
tool = db.query(Tool).get(id)
db.refresh(tool)
return ToolModel.model_validate(tool)
return self._to_tool_model(tool, db=db)
except Exception:
return None
def delete_tool_by_id(self, id: str) -> bool:
def delete_tool_by_id(self, id: str, db: Optional[Session] = None) -> bool:
try:
with get_db() as db:
with get_db_context(db) as db:
AccessGrants.revoke_all_access("tool", id, db=db)
db.query(Tool).filter_by(id=id).delete()
db.commit()
+548 -140
View File
@@ -1,18 +1,35 @@
import time
from typing import Optional
from open_webui.internal.db import Base, JSONField, get_db
from sqlalchemy.orm import Session, defer
from open_webui.internal.db import Base, JSONField, get_db, get_db_context
from open_webui.env import DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL
from open_webui.models.chats import Chats
from open_webui.models.groups import Groups
from open_webui.models.groups import Groups, GroupMember
from open_webui.models.channels import ChannelMember
from open_webui.utils.misc import throttle
from open_webui.utils.validate import validate_profile_image_url
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Column, String, Text, Date
from sqlalchemy import or_
from pydantic import BaseModel, ConfigDict, field_validator, model_validator
from sqlalchemy import (
BigInteger,
JSON,
Column,
String,
Boolean,
Text,
Date,
exists,
select,
cast,
)
from sqlalchemy import or_, case, func
from sqlalchemy.dialects.postgresql import JSONB
import datetime
@@ -21,59 +38,73 @@ import datetime
####################
class User(Base):
__tablename__ = "user"
id = Column(String, primary_key=True)
name = Column(String)
email = Column(String)
username = Column(String(50), nullable=True)
role = Column(String)
profile_image_url = Column(Text)
bio = Column(Text, nullable=True)
gender = Column(Text, nullable=True)
date_of_birth = Column(Date, nullable=True)
info = Column(JSONField, nullable=True)
settings = Column(JSONField, nullable=True)
api_key = Column(String, nullable=True, unique=True)
oauth_sub = Column(Text, unique=True)
last_active_at = Column(BigInteger)
updated_at = Column(BigInteger)
created_at = Column(BigInteger)
class UserSettings(BaseModel):
ui: Optional[dict] = {}
model_config = ConfigDict(extra="allow")
pass
class User(Base):
__tablename__ = "user"
id = Column(String, primary_key=True, unique=True)
email = Column(String)
username = Column(String(50), nullable=True)
role = Column(String)
name = Column(String)
profile_image_url = Column(Text)
profile_banner_image_url = Column(Text, nullable=True)
bio = Column(Text, nullable=True)
gender = Column(Text, nullable=True)
date_of_birth = Column(Date, nullable=True)
timezone = Column(String, nullable=True)
presence_state = Column(String, nullable=True)
status_emoji = Column(String, nullable=True)
status_message = Column(Text, nullable=True)
status_expires_at = Column(BigInteger, nullable=True)
info = Column(JSON, nullable=True)
settings = Column(JSON, nullable=True)
oauth = Column(JSON, nullable=True)
scim = Column(JSON, nullable=True)
last_active_at = Column(BigInteger)
updated_at = Column(BigInteger)
created_at = Column(BigInteger)
class UserModel(BaseModel):
id: str
name: str
email: str
username: Optional[str] = None
role: str = "pending"
profile_image_url: str
name: str
profile_image_url: Optional[str] = None
profile_banner_image_url: Optional[str] = None
bio: Optional[str] = None
gender: Optional[str] = None
date_of_birth: Optional[datetime.date] = None
timezone: Optional[str] = None
presence_state: Optional[str] = None
status_emoji: Optional[str] = None
status_message: Optional[str] = None
status_expires_at: Optional[int] = None
info: Optional[dict] = None
settings: Optional[UserSettings] = None
api_key: Optional[str] = None
oauth_sub: Optional[str] = None
oauth: Optional[dict] = None
scim: Optional[dict] = None
last_active_at: int # timestamp in epoch
updated_at: int # timestamp in epoch
@@ -81,6 +112,44 @@ class UserModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
@model_validator(mode="after")
def set_profile_image_url(self):
if not self.profile_image_url:
self.profile_image_url = f"/api/v1/users/{self.id}/profile/image"
return self
class UserStatusModel(UserModel):
is_active: bool = False
model_config = ConfigDict(from_attributes=True)
class ApiKey(Base):
__tablename__ = "api_key"
id = Column(Text, primary_key=True, unique=True)
user_id = Column(Text, nullable=False)
key = Column(Text, unique=True, nullable=False)
data = Column(JSON, nullable=True)
expires_at = Column(BigInteger, nullable=True)
last_used_at = Column(BigInteger, nullable=True)
created_at = Column(BigInteger, nullable=False)
updated_at = Column(BigInteger, nullable=False)
class ApiKeyModel(BaseModel):
id: str
user_id: str
key: str
data: Optional[dict] = None
expires_at: Optional[int] = None
last_used_at: Optional[int] = None
created_at: int # timestamp in epoch
updated_at: int # timestamp in epoch
model_config = ConfigDict(from_attributes=True)
####################
# Forms
@@ -94,17 +163,55 @@ class UpdateProfileForm(BaseModel):
gender: Optional[str] = None
date_of_birth: Optional[datetime.date] = None
@field_validator("profile_image_url")
@classmethod
def check_profile_image_url(cls, v: str) -> str:
return validate_profile_image_url(v)
class UserGroupIdsModel(UserModel):
group_ids: list[str] = []
class UserModelResponse(UserModel):
model_config = ConfigDict(extra="allow")
class UserListResponse(BaseModel):
users: list[UserModel]
users: list[UserModelResponse]
total: int
class UserInfoResponse(BaseModel):
class UserGroupIdsListResponse(BaseModel):
users: list[UserGroupIdsModel]
total: int
class UserStatus(BaseModel):
status_emoji: Optional[str] = None
status_message: Optional[str] = None
status_expires_at: Optional[int] = None
class UserInfoResponse(UserStatus):
id: str
name: str
email: str
role: str
bio: Optional[str] = None
groups: Optional[list] = []
is_active: bool = False
class UserIdNameResponse(BaseModel):
id: str
name: str
class UserIdNameStatusResponse(UserStatus):
id: str
name: str
is_active: Optional[bool] = None
class UserInfoListResponse(BaseModel):
@@ -112,18 +219,23 @@ class UserInfoListResponse(BaseModel):
total: int
class UserResponse(BaseModel):
id: str
name: str
email: str
role: str
profile_image_url: str
class UserIdNameListResponse(BaseModel):
users: list[UserIdNameResponse]
total: int
class UserNameResponse(BaseModel):
id: str
name: str
role: str
class UserResponse(UserNameResponse):
email: str
class UserProfileImageResponse(UserNameResponse):
email: str
profile_image_url: str
@@ -139,6 +251,11 @@ class UserUpdateForm(BaseModel):
profile_image_url: str
password: Optional[str] = None
@field_validator("profile_image_url")
@classmethod
def check_profile_image_url(cls, v: str) -> str:
return validate_profile_image_url(v)
class UsersTable:
def insert_new_user(
@@ -148,20 +265,23 @@ class UsersTable:
email: str,
profile_image_url: str = "/user.png",
role: str = "pending",
oauth_sub: Optional[str] = None,
username: Optional[str] = None,
oauth: Optional[dict] = None,
db: Optional[Session] = None,
) -> Optional[UserModel]:
with get_db() as db:
with get_db_context(db) as db:
user = UserModel(
**{
"id": id,
"name": name,
"email": email,
"name": name,
"role": role,
"profile_image_url": profile_image_url,
"last_active_at": int(time.time()),
"created_at": int(time.time()),
"updated_at": int(time.time()),
"oauth_sub": oauth_sub,
"username": username,
"oauth": oauth,
}
)
result = User(**user.model_dump())
@@ -173,35 +293,86 @@ class UsersTable:
else:
return None
def get_user_by_id(self, id: str) -> Optional[UserModel]:
def get_user_by_id(
self, id: str, db: Optional[Session] = None
) -> Optional[UserModel]:
try:
with get_db() as db:
with get_db_context(db) as db:
user = db.query(User).filter_by(id=id).first()
return UserModel.model_validate(user)
except Exception:
return None
def get_user_by_api_key(self, api_key: str) -> Optional[UserModel]:
def get_user_by_api_key(
self, api_key: str, db: Optional[Session] = None
) -> Optional[UserModel]:
try:
with get_db() as db:
user = db.query(User).filter_by(api_key=api_key).first()
return UserModel.model_validate(user)
with get_db_context(db) as db:
user = (
db.query(User)
.join(ApiKey, User.id == ApiKey.user_id)
.filter(ApiKey.key == api_key)
.first()
)
return UserModel.model_validate(user) if user else None
except Exception:
return None
def get_user_by_email(self, email: str) -> Optional[UserModel]:
def get_user_by_email(
self, email: str, db: Optional[Session] = None
) -> Optional[UserModel]:
try:
with get_db() as db:
user = db.query(User).filter_by(email=email).first()
return UserModel.model_validate(user)
with get_db_context(db) as db:
user = (
db.query(User)
.filter(func.lower(User.email) == email.lower())
.first()
)
return UserModel.model_validate(user) if user else None
except Exception:
return None
def get_user_by_oauth_sub(self, sub: str) -> Optional[UserModel]:
def get_user_by_oauth_sub(
self, provider: str, sub: str, db: Optional[Session] = None
) -> Optional[UserModel]:
try:
with get_db() as db:
user = db.query(User).filter_by(oauth_sub=sub).first()
return UserModel.model_validate(user)
with get_db_context(db) as db: # type: Session
dialect_name = db.bind.dialect.name
query = db.query(User)
if dialect_name == "sqlite":
query = query.filter(User.oauth.contains({provider: {"sub": sub}}))
elif dialect_name == "postgresql":
query = query.filter(
User.oauth[provider].cast(JSONB)["sub"].astext == sub
)
user = query.first()
return UserModel.model_validate(user) if user else None
except Exception as e:
# You may want to log the exception here
return None
def get_user_by_scim_external_id(
self, provider: str, external_id: str, db: Optional[Session] = None
) -> Optional[UserModel]:
try:
with get_db_context(db) as db: # type: Session
dialect_name = db.bind.dialect.name
query = db.query(User)
if dialect_name == "sqlite":
query = query.filter(
User.scim.contains({provider: {"external_id": external_id}})
)
elif dialect_name == "postgresql":
query = query.filter(
User.scim[provider].cast(JSONB)["external_id"].astext
== external_id
)
user = query.first()
return UserModel.model_validate(user) if user else None
except Exception:
return None
@@ -210,9 +381,11 @@ class UsersTable:
filter: Optional[dict] = None,
skip: Optional[int] = None,
limit: Optional[int] = None,
) -> UserListResponse:
with get_db() as db:
query = db.query(User)
db: Optional[Session] = None,
) -> dict:
with get_db_context(db) as db:
# Join GroupMember so we can order by group_id when requested
query = db.query(User).options(defer(User.profile_image_url))
if filter:
query_key = filter.get("query")
@@ -224,14 +397,76 @@ class UsersTable:
)
)
channel_id = filter.get("channel_id")
if channel_id:
query = query.filter(
exists(
select(ChannelMember.id).where(
ChannelMember.user_id == User.id,
ChannelMember.channel_id == channel_id,
)
)
)
user_ids = filter.get("user_ids")
group_ids = filter.get("group_ids")
if isinstance(user_ids, list) and isinstance(group_ids, list):
# If both are empty lists, return no users
if not user_ids and not group_ids:
return {"users": [], "total": 0}
if user_ids:
query = query.filter(User.id.in_(user_ids))
if group_ids:
query = query.filter(
exists(
select(GroupMember.id).where(
GroupMember.user_id == User.id,
GroupMember.group_id.in_(group_ids),
)
)
)
roles = filter.get("roles")
if roles:
include_roles = [role for role in roles if not role.startswith("!")]
exclude_roles = [role[1:] for role in roles if role.startswith("!")]
if include_roles:
query = query.filter(User.role.in_(include_roles))
if exclude_roles:
query = query.filter(~User.role.in_(exclude_roles))
order_by = filter.get("order_by")
direction = filter.get("direction")
if order_by == "name":
if order_by and order_by.startswith("group_id:"):
group_id = order_by.split(":", 1)[1]
# Subquery that checks if the user belongs to the group
membership_exists = exists(
select(GroupMember.id).where(
GroupMember.user_id == User.id,
GroupMember.group_id == group_id,
)
)
# CASE: user in group → 1, user not in group → 0
group_sort = case((membership_exists, 1), else_=0)
if direction == "asc":
query = query.order_by(group_sort.asc(), User.name.asc())
else:
query = query.order_by(group_sort.desc(), User.name.asc())
elif order_by == "name":
if direction == "asc":
query = query.order_by(User.name.asc())
else:
query = query.order_by(User.name.desc())
elif order_by == "email":
if direction == "asc":
query = query.order_by(User.email.asc())
@@ -264,41 +499,67 @@ class UsersTable:
else:
query = query.order_by(User.created_at.desc())
if skip:
# Count BEFORE pagination
total = query.count()
# correct pagination logic
if skip is not None:
query = query.offset(skip)
if limit:
if limit is not None:
query = query.limit(limit)
users = query.all()
return {
"users": [UserModel.model_validate(user) for user in users],
"total": db.query(User).count(),
"total": total,
}
def get_users_by_user_ids(self, user_ids: list[str]) -> list[UserModel]:
with get_db() as db:
users = db.query(User).filter(User.id.in_(user_ids)).all()
def get_users_by_group_id(
self, group_id: str, db: Optional[Session] = None
) -> list[UserModel]:
with get_db_context(db) as db:
users = (
db.query(User)
.options(defer(User.profile_image_url))
.join(GroupMember, User.id == GroupMember.user_id)
.filter(GroupMember.group_id == group_id)
.all()
)
return [UserModel.model_validate(user) for user in users]
def get_num_users(self) -> Optional[int]:
with get_db() as db:
def get_users_by_user_ids(
self, user_ids: list[str], db: Optional[Session] = None
) -> list[UserStatusModel]:
with get_db_context(db) as db:
users = (
db.query(User)
.options(defer(User.profile_image_url))
.filter(User.id.in_(user_ids))
.all()
)
return [UserModel.model_validate(user) for user in users]
def get_num_users(self, db: Optional[Session] = None) -> Optional[int]:
with get_db_context(db) as db:
return db.query(User).count()
def has_users(self) -> bool:
with get_db() as db:
def has_users(self, db: Optional[Session] = None) -> bool:
with get_db_context(db) as db:
return db.query(db.query(User).exists()).scalar()
def get_first_user(self) -> UserModel:
def get_first_user(self, db: Optional[Session] = None) -> UserModel:
try:
with get_db() as db:
with get_db_context(db) as db:
user = db.query(User).order_by(User.created_at).first()
return UserModel.model_validate(user)
except Exception:
return None
def get_user_webhook_url_by_id(self, id: str) -> Optional[str]:
def get_user_webhook_url_by_id(
self, id: str, db: Optional[Session] = None
) -> Optional[str]:
try:
with get_db() as db:
with get_db_context(db) as db:
user = db.query(User).filter_by(id=id).first()
if user.settings is None:
@@ -312,75 +573,168 @@ class UsersTable:
except Exception:
return None
def update_user_role_by_id(self, id: str, role: str) -> Optional[UserModel]:
def get_num_users_active_today(self, db: Optional[Session] = None) -> Optional[int]:
with get_db_context(db) as db:
current_timestamp = int(datetime.datetime.now().timestamp())
today_midnight_timestamp = current_timestamp - (current_timestamp % 86400)
query = db.query(User).filter(
User.last_active_at > today_midnight_timestamp
)
return query.count()
def update_user_role_by_id(
self, id: str, role: str, db: Optional[Session] = None
) -> Optional[UserModel]:
try:
with get_db() as db:
db.query(User).filter_by(id=id).update({"role": role})
db.commit()
with get_db_context(db) as db:
user = db.query(User).filter_by(id=id).first()
if not user:
return None
user.role = role
db.commit()
db.refresh(user)
return UserModel.model_validate(user)
except Exception:
return None
def update_user_status_by_id(
self, id: str, form_data: UserStatus, db: Optional[Session] = None
) -> Optional[UserModel]:
try:
with get_db_context(db) as db:
user = db.query(User).filter_by(id=id).first()
if not user:
return None
for key, value in form_data.model_dump(exclude_none=True).items():
setattr(user, key, value)
db.commit()
db.refresh(user)
return UserModel.model_validate(user)
except Exception:
return None
def update_user_profile_image_url_by_id(
self, id: str, profile_image_url: str
self, id: str, profile_image_url: str, db: Optional[Session] = None
) -> Optional[UserModel]:
try:
with get_db() as db:
db.query(User).filter_by(id=id).update(
{"profile_image_url": profile_image_url}
)
db.commit()
with get_db_context(db) as db:
user = db.query(User).filter_by(id=id).first()
if not user:
return None
user.profile_image_url = profile_image_url
db.commit()
db.refresh(user)
return UserModel.model_validate(user)
except Exception:
return None
@throttle(DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL)
def update_user_last_active_by_id(self, id: str) -> Optional[UserModel]:
try:
with get_db() as db:
db.query(User).filter_by(id=id).update(
{"last_active_at": int(time.time())}
)
db.commit()
user = db.query(User).filter_by(id=id).first()
return UserModel.model_validate(user)
except Exception:
return None
def update_user_oauth_sub_by_id(
self, id: str, oauth_sub: str
def update_last_active_by_id(
self, id: str, db: Optional[Session] = None
) -> Optional[UserModel]:
try:
with get_db() as db:
db.query(User).filter_by(id=id).update({"oauth_sub": oauth_sub})
db.commit()
with get_db_context(db) as db:
user = db.query(User).filter_by(id=id).first()
if not user:
return None
user.last_active_at = int(time.time())
db.commit()
db.refresh(user)
return UserModel.model_validate(user)
except Exception:
return None
def update_user_by_id(self, id: str, updated: dict) -> Optional[UserModel]:
def update_user_oauth_by_id(
self, id: str, provider: str, sub: str, db: Optional[Session] = None
) -> Optional[UserModel]:
"""
Update or insert an OAuth provider/sub pair into the user's oauth JSON field.
Example resulting structure:
{
"google": { "sub": "123" },
"github": { "sub": "abc" }
}
"""
try:
with get_db() as db:
db.query(User).filter_by(id=id).update(updated)
with get_db_context(db) as db:
user = db.query(User).filter_by(id=id).first()
if not user:
return None
# Load existing oauth JSON or create empty
oauth = user.oauth or {}
# Update or insert provider entry
oauth[provider] = {"sub": sub}
# Persist updated JSON
db.query(User).filter_by(id=id).update({"oauth": oauth})
db.commit()
user = db.query(User).filter_by(id=id).first()
return UserModel.model_validate(user)
# return UserModel(**user.dict())
except Exception:
return None
def update_user_scim_by_id(
self,
id: str,
provider: str,
external_id: str,
db: Optional[Session] = None,
) -> Optional[UserModel]:
"""
Update or insert a SCIM provider/external_id pair into the user's scim JSON field.
Example resulting structure:
{
"microsoft": { "external_id": "abc" },
"okta": { "external_id": "def" }
}
"""
try:
with get_db_context(db) as db:
user = db.query(User).filter_by(id=id).first()
if not user:
return None
scim = user.scim or {}
scim[provider] = {"external_id": external_id}
db.query(User).filter_by(id=id).update({"scim": scim})
db.commit()
return UserModel.model_validate(user)
except Exception:
return None
def update_user_by_id(
self, id: str, updated: dict, db: Optional[Session] = None
) -> Optional[UserModel]:
try:
with get_db_context(db) as db:
user = db.query(User).filter_by(id=id).first()
if not user:
return None
for key, value in updated.items():
setattr(user, key, value)
db.commit()
db.refresh(user)
return UserModel.model_validate(user)
except Exception as e:
print(e)
return None
def update_user_settings_by_id(self, id: str, updated: dict) -> Optional[UserModel]:
def update_user_settings_by_id(
self, id: str, updated: dict, db: Optional[Session] = None
) -> Optional[UserModel]:
try:
with get_db() as db:
user_settings = db.query(User).filter_by(id=id).first().settings
with get_db_context(db) as db:
user = db.query(User).filter_by(id=id).first()
if not user:
return None
user_settings = user.settings
if user_settings is None:
user_settings = {}
@@ -395,15 +749,15 @@ class UsersTable:
except Exception:
return None
def delete_user_by_id(self, id: str) -> bool:
def delete_user_by_id(self, id: str, db: Optional[Session] = None) -> bool:
try:
# Remove User from Groups
Groups.remove_user_from_all_groups(id)
# Delete User Chats
result = Chats.delete_chats_by_user_id(id)
result = Chats.delete_chats_by_user_id(id, db=db)
if result:
with get_db() as db:
with get_db_context(db) as db:
# Delete User
db.query(User).filter_by(id=id).delete()
db.commit()
@@ -414,35 +768,89 @@ class UsersTable:
except Exception:
return False
def update_user_api_key_by_id(self, id: str, api_key: str) -> bool:
def get_user_api_key_by_id(
self, id: str, db: Optional[Session] = None
) -> Optional[str]:
try:
with get_db() as db:
result = db.query(User).filter_by(id=id).update({"api_key": api_key})
db.commit()
return True if result == 1 else False
except Exception:
return False
def get_user_api_key_by_id(self, id: str) -> Optional[str]:
try:
with get_db() as db:
user = db.query(User).filter_by(id=id).first()
return user.api_key
with get_db_context(db) as db:
api_key = db.query(ApiKey).filter_by(user_id=id).first()
return api_key.key if api_key else None
except Exception:
return None
def get_valid_user_ids(self, user_ids: list[str]) -> list[str]:
with get_db() as db:
def update_user_api_key_by_id(
self, id: str, api_key: str, db: Optional[Session] = None
) -> bool:
try:
with get_db_context(db) as db:
db.query(ApiKey).filter_by(user_id=id).delete()
db.commit()
now = int(time.time())
new_api_key = ApiKey(
id=f"key_{id}",
user_id=id,
key=api_key,
created_at=now,
updated_at=now,
)
db.add(new_api_key)
db.commit()
return True
except Exception:
return False
def delete_user_api_key_by_id(self, id: str, db: Optional[Session] = None) -> bool:
try:
with get_db_context(db) as db:
db.query(ApiKey).filter_by(user_id=id).delete()
db.commit()
return True
except Exception:
return False
def get_valid_user_ids(
self, user_ids: list[str], db: Optional[Session] = None
) -> list[str]:
with get_db_context(db) as db:
users = db.query(User).filter(User.id.in_(user_ids)).all()
return [user.id for user in users]
def get_super_admin_user(self) -> Optional[UserModel]:
with get_db() as db:
def get_super_admin_user(self, db: Optional[Session] = None) -> Optional[UserModel]:
with get_db_context(db) as db:
user = db.query(User).filter_by(role="admin").first()
if user:
return UserModel.model_validate(user)
else:
return None
def get_active_user_count(self, db: Optional[Session] = None) -> int:
with get_db_context(db) as db:
# Consider user active if last_active_at within the last 3 minutes
three_minutes_ago = int(time.time()) - 180
count = (
db.query(User).filter(User.last_active_at >= three_minutes_ago).count()
)
return count
@staticmethod
def is_active(user: UserModel) -> bool:
"""Compute active status from an already-loaded UserModel (no DB hit)."""
if user.last_active_at:
three_minutes_ago = int(time.time()) - 180
return user.last_active_at >= three_minutes_ago
return False
def is_user_active(self, user_id: str, db: Optional[Session] = None) -> bool:
with get_db_context(db) as db:
user = db.query(User).filter_by(id=user_id).first()
if user and user.last_active_at:
# Consider user active if last_active_at within the last 3 minutes
three_minutes_ago = int(time.time()) - 180
return user.last_active_at >= three_minutes_ago
return False
Users = UsersTable()
@@ -5,10 +5,9 @@ from urllib.parse import quote
from langchain_core.document_loaders import BaseLoader
from langchain_core.documents import Document
from open_webui.env import SRC_LOG_LEVELS
from open_webui.utils.headers import include_user_info_headers
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class ExternalDocumentLoader(BaseLoader):
@@ -18,6 +17,7 @@ class ExternalDocumentLoader(BaseLoader):
url: str,
api_key: str,
mime_type=None,
user=None,
**kwargs,
) -> None:
self.url = url
@@ -26,6 +26,8 @@ class ExternalDocumentLoader(BaseLoader):
self.file_path = file_path
self.mime_type = mime_type
self.user = user
def load(self) -> List[Document]:
with open(self.file_path, "rb") as f:
data = f.read()
@@ -42,6 +44,9 @@ class ExternalDocumentLoader(BaseLoader):
except:
pass
if self.user is not None:
headers = include_user_info_headers(headers, self.user)
url = self.url
if url.endswith("/"):
url = url[:-1]
@@ -4,10 +4,8 @@ from typing import Iterator, List, Union
from langchain_core.document_loaders import BaseLoader
from langchain_core.documents import Document
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class ExternalWebLoader(BaseLoader):
+53 -80
View File
@@ -27,13 +27,13 @@ from open_webui.retrieval.loaders.external_document import ExternalDocumentLoade
from open_webui.retrieval.loaders.mistral import MistralLoader
from open_webui.retrieval.loaders.datalab_marker import DatalabMarkerLoader
from open_webui.retrieval.loaders.mineru import MinerULoader
from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL
from open_webui.env import GLOBAL_LOG_LEVEL, REQUESTS_VERIFY
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
known_source_ext = [
"go",
@@ -114,7 +114,7 @@ class TikaLoader:
endpoint += "/"
endpoint += "tika/text"
r = requests.put(endpoint, data=data, headers=headers)
r = requests.put(endpoint, data=data, headers=headers, verify=REQUESTS_VERIFY)
if r.ok:
raw_metadata = r.json()
@@ -131,8 +131,9 @@ class TikaLoader:
class DoclingLoader:
def __init__(self, url, file_path=None, mime_type=None, params=None):
def __init__(self, url, api_key=None, file_path=None, mime_type=None, params=None):
self.url = url.rstrip("/")
self.api_key = api_key
self.file_path = file_path
self.mime_type = mime_type
@@ -140,68 +141,25 @@ class DoclingLoader:
def load(self) -> list[Document]:
with open(self.file_path, "rb") as f:
files = {
"files": (
self.file_path,
f,
self.mime_type or "application/octet-stream",
)
}
headers = {}
if self.api_key:
headers["X-Api-Key"] = f"{self.api_key}"
params = {"image_export_mode": "placeholder"}
if self.params:
if self.params.get("do_picture_description"):
params["do_picture_description"] = self.params.get(
"do_picture_description"
r = requests.post(
f"{self.url}/v1/convert/file",
files={
"files": (
self.file_path,
f,
self.mime_type or "application/octet-stream",
)
picture_description_mode = self.params.get(
"picture_description_mode", ""
).lower()
if picture_description_mode == "local" and self.params.get(
"picture_description_local", {}
):
params["picture_description_local"] = json.dumps(
self.params.get("picture_description_local", {})
)
elif picture_description_mode == "api" and self.params.get(
"picture_description_api", {}
):
params["picture_description_api"] = json.dumps(
self.params.get("picture_description_api", {})
)
params["do_ocr"] = self.params.get("do_ocr")
params["force_ocr"] = self.params.get("force_ocr")
if (
self.params.get("do_ocr")
and self.params.get("ocr_engine")
and self.params.get("ocr_lang")
):
params["ocr_engine"] = self.params.get("ocr_engine")
params["ocr_lang"] = [
lang.strip()
for lang in self.params.get("ocr_lang").split(",")
if lang.strip()
]
if self.params.get("pdf_backend"):
params["pdf_backend"] = self.params.get("pdf_backend")
if self.params.get("table_mode"):
params["table_mode"] = self.params.get("table_mode")
if self.params.get("pipeline"):
params["pipeline"] = self.params.get("pipeline")
endpoint = f"{self.url}/v1/convert/file"
r = requests.post(endpoint, files=files, data=params)
},
data={
"image_export_mode": "placeholder",
**self.params,
},
headers=headers,
)
if r.ok:
result = r.json()
document_data = result.get("document", {})
@@ -210,7 +168,6 @@ class DoclingLoader:
metadata = {"Content-Type": self.mime_type} if self.mime_type else {}
log.debug("Docling extracted text: %s", text)
return [Document(page_content=text, metadata=metadata)]
else:
error_msg = f"Error calling Docling API: {r.reason}"
@@ -227,6 +184,7 @@ class DoclingLoader:
class Loader:
def __init__(self, engine: str = "", **kwargs):
self.engine = engine
self.user = kwargs.get("user", None)
self.kwargs = kwargs
def load(
@@ -263,6 +221,7 @@ class Loader:
url=self.kwargs.get("EXTERNAL_DOCUMENT_LOADER_URL"),
api_key=self.kwargs.get("EXTERNAL_DOCUMENT_LOADER_API_KEY"),
mime_type=file_content_type,
user=self.user,
)
elif self.engine == "tika" and self.kwargs.get("TIKA_SERVER_URL"):
if self._is_text_file(file_ext, file_content_type):
@@ -271,7 +230,6 @@ class Loader:
loader = TikaLoader(
url=self.kwargs.get("TIKA_SERVER_URL"),
file_path=file_path,
mime_type=file_content_type,
extract_images=self.kwargs.get("PDF_EXTRACT_IMAGES"),
)
elif (
@@ -338,6 +296,7 @@ class Loader:
loader = DoclingLoader(
url=self.kwargs.get("DOCLING_SERVER_URL"),
api_key=self.kwargs.get("DOCLING_API_KEY", None),
file_path=file_path,
mime_type=file_content_type,
params=params,
@@ -346,11 +305,9 @@ class Loader:
self.engine == "document_intelligence"
and self.kwargs.get("DOCUMENT_INTELLIGENCE_ENDPOINT") != ""
and (
file_ext in ["pdf", "xls", "xlsx", "docx", "ppt", "pptx"]
file_ext in ["pdf", "docx", "ppt", "pptx"]
or file_content_type
in [
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-powerpoint",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
@@ -362,13 +319,34 @@ class Loader:
file_path=file_path,
api_endpoint=self.kwargs.get("DOCUMENT_INTELLIGENCE_ENDPOINT"),
api_key=self.kwargs.get("DOCUMENT_INTELLIGENCE_KEY"),
api_model=self.kwargs.get("DOCUMENT_INTELLIGENCE_MODEL"),
)
else:
loader = AzureAIDocumentIntelligenceLoader(
file_path=file_path,
api_endpoint=self.kwargs.get("DOCUMENT_INTELLIGENCE_ENDPOINT"),
azure_credential=DefaultAzureCredential(),
api_model=self.kwargs.get("DOCUMENT_INTELLIGENCE_MODEL"),
)
elif self.engine == "mineru" and file_ext in [
"pdf"
]: # MinerU currently only supports PDF
mineru_timeout = self.kwargs.get("MINERU_API_TIMEOUT", 300)
if mineru_timeout:
try:
mineru_timeout = int(mineru_timeout)
except ValueError:
mineru_timeout = 300
loader = MinerULoader(
file_path=file_path,
api_mode=self.kwargs.get("MINERU_API_MODE", "local"),
api_url=self.kwargs.get("MINERU_API_URL", "http://localhost:8000"),
api_key=self.kwargs.get("MINERU_API_KEY", ""),
params=self.kwargs.get("MINERU_PARAMS", {}),
timeout=mineru_timeout,
)
elif (
self.engine == "mistral_ocr"
and self.kwargs.get("MISTRAL_OCR_API_KEY") != ""
@@ -376,21 +354,16 @@ class Loader:
in ["pdf"] # Mistral OCR currently only supports PDF and images
):
loader = MistralLoader(
api_key=self.kwargs.get("MISTRAL_OCR_API_KEY"), file_path=file_path
)
elif (
self.engine == "external"
and self.kwargs.get("MISTRAL_OCR_API_KEY") != ""
and file_ext
in ["pdf"] # Mistral OCR currently only supports PDF and images
):
loader = MistralLoader(
api_key=self.kwargs.get("MISTRAL_OCR_API_KEY"), file_path=file_path
base_url=self.kwargs.get("MISTRAL_OCR_API_BASE_URL"),
api_key=self.kwargs.get("MISTRAL_OCR_API_KEY"),
file_path=file_path,
)
else:
if file_ext == "pdf":
loader = PyPDFLoader(
file_path, extract_images=self.kwargs.get("PDF_EXTRACT_IMAGES")
file_path,
extract_images=self.kwargs.get("PDF_EXTRACT_IMAGES"),
mode=self.kwargs.get("PDF_LOADER_MODE", "page"),
)
elif file_ext == "csv":
loader = CSVLoader(file_path, autodetect_encoding=True)
@@ -0,0 +1,524 @@
import os
import time
import requests
import logging
import tempfile
import zipfile
from typing import List, Optional
from langchain_core.documents import Document
from fastapi import HTTPException, status
log = logging.getLogger(__name__)
class MinerULoader:
"""
MinerU document parser loader supporting both Cloud API and Local API modes.
Cloud API: Uses MinerU managed service with async task-based processing
Local API: Uses self-hosted MinerU API with synchronous processing
"""
def __init__(
self,
file_path: str,
api_mode: str = "local",
api_url: str = "http://localhost:8000",
api_key: str = "",
params: dict = None,
timeout: Optional[int] = 300,
):
self.file_path = file_path
self.api_mode = api_mode.lower()
self.api_url = api_url.rstrip("/")
self.api_key = api_key
self.timeout = timeout
# Parse params dict with defaults
self.params = params or {}
self.enable_ocr = params.get("enable_ocr", False)
self.enable_formula = params.get("enable_formula", True)
self.enable_table = params.get("enable_table", True)
self.language = params.get("language", "en")
self.model_version = params.get("model_version", "pipeline")
self.page_ranges = self.params.pop("page_ranges", "")
# Validate API mode
if self.api_mode not in ["local", "cloud"]:
raise ValueError(
f"Invalid API mode: {self.api_mode}. Must be 'local' or 'cloud'"
)
# Validate Cloud API requirements
if self.api_mode == "cloud" and not self.api_key:
raise ValueError("API key is required for Cloud API mode")
def load(self) -> List[Document]:
"""
Main entry point for loading and parsing the document.
Routes to Cloud or Local API based on api_mode.
"""
try:
if self.api_mode == "cloud":
return self._load_cloud_api()
else:
return self._load_local_api()
except Exception as e:
log.error(f"Error loading document with MinerU: {e}")
raise
def _load_local_api(self) -> List[Document]:
"""
Load document using Local API (synchronous).
Posts file to /file_parse endpoint and gets immediate response.
"""
log.info(f"Using MinerU Local API at {self.api_url}")
filename = os.path.basename(self.file_path)
# Build form data for Local API
form_data = {
**self.params,
"return_md": "true",
}
# Page ranges (Local API uses start_page_id and end_page_id)
if self.page_ranges:
# For simplicity, if page_ranges is specified, log a warning
# Full page range parsing would require parsing the string
log.warning(
f"Page ranges '{self.page_ranges}' specified but Local API uses different format. "
"Consider using start_page_id/end_page_id parameters if needed."
)
try:
with open(self.file_path, "rb") as f:
files = {"files": (filename, f, "application/octet-stream")}
log.info(f"Sending file to MinerU Local API: {filename}")
log.debug(f"Local API parameters: {form_data}")
response = requests.post(
f"{self.api_url}/file_parse",
data=form_data,
files=files,
timeout=self.timeout,
)
response.raise_for_status()
except FileNotFoundError:
raise HTTPException(
status.HTTP_404_NOT_FOUND, detail=f"File not found: {self.file_path}"
)
except requests.Timeout:
raise HTTPException(
status.HTTP_504_GATEWAY_TIMEOUT,
detail="MinerU Local API request timed out",
)
except requests.HTTPError as e:
error_detail = f"MinerU Local API request failed: {e}"
if e.response is not None:
try:
error_data = e.response.json()
error_detail += f" - {error_data}"
except:
error_detail += f" - {e.response.text}"
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=error_detail)
except Exception as e:
raise HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error calling MinerU Local API: {str(e)}",
)
# Parse response
try:
result = response.json()
except ValueError as e:
raise HTTPException(
status.HTTP_502_BAD_GATEWAY,
detail=f"Invalid JSON response from MinerU Local API: {e}",
)
# Extract markdown content from response
if "results" not in result:
raise HTTPException(
status.HTTP_502_BAD_GATEWAY,
detail="MinerU Local API response missing 'results' field",
)
results = result["results"]
if not results:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail="MinerU returned empty results",
)
# Get the first (and typically only) result
file_result = list(results.values())[0]
markdown_content = file_result.get("md_content", "")
if not markdown_content:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail="MinerU returned empty markdown content",
)
log.info(f"Successfully parsed document with MinerU Local API: {filename}")
# Create metadata
metadata = {
"source": filename,
"api_mode": "local",
"backend": result.get("backend", "unknown"),
"version": result.get("version", "unknown"),
}
return [Document(page_content=markdown_content, metadata=metadata)]
def _load_cloud_api(self) -> List[Document]:
"""
Load document using Cloud API (asynchronous).
Uses batch upload endpoint to avoid need for public file URLs.
"""
log.info(f"Using MinerU Cloud API at {self.api_url}")
filename = os.path.basename(self.file_path)
# Step 1: Request presigned upload URL
batch_id, upload_url = self._request_upload_url(filename)
# Step 2: Upload file to presigned URL
self._upload_to_presigned_url(upload_url)
# Step 3: Poll for results
result = self._poll_batch_status(batch_id, filename)
# Step 4: Download and extract markdown from ZIP
markdown_content = self._download_and_extract_zip(
result["full_zip_url"], filename
)
log.info(f"Successfully parsed document with MinerU Cloud API: {filename}")
# Create metadata
metadata = {
"source": filename,
"api_mode": "cloud",
"batch_id": batch_id,
}
return [Document(page_content=markdown_content, metadata=metadata)]
def _request_upload_url(self, filename: str) -> tuple:
"""
Request presigned upload URL from Cloud API.
Returns (batch_id, upload_url).
"""
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
}
# Build request body
request_body = {
**self.params,
"files": [
{
"name": filename,
"is_ocr": self.enable_ocr,
}
],
}
# Add page ranges if specified
if self.page_ranges:
request_body["files"][0]["page_ranges"] = self.page_ranges
log.info(f"Requesting upload URL for: {filename}")
log.debug(f"Cloud API request body: {request_body}")
try:
response = requests.post(
f"{self.api_url}/file-urls/batch",
headers=headers,
json=request_body,
timeout=30,
)
response.raise_for_status()
except requests.HTTPError as e:
error_detail = f"Failed to request upload URL: {e}"
if e.response is not None:
try:
error_data = e.response.json()
error_detail += f" - {error_data.get('msg', error_data)}"
except:
error_detail += f" - {e.response.text}"
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=error_detail)
except Exception as e:
raise HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error requesting upload URL: {str(e)}",
)
try:
result = response.json()
except ValueError as e:
raise HTTPException(
status.HTTP_502_BAD_GATEWAY,
detail=f"Invalid JSON response: {e}",
)
# Check for API error response
if result.get("code") != 0:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail=f"MinerU Cloud API error: {result.get('msg', 'Unknown error')}",
)
data = result.get("data", {})
batch_id = data.get("batch_id")
file_urls = data.get("file_urls", [])
if not batch_id or not file_urls:
raise HTTPException(
status.HTTP_502_BAD_GATEWAY,
detail="MinerU Cloud API response missing batch_id or file_urls",
)
upload_url = file_urls[0]
log.info(f"Received upload URL for batch: {batch_id}")
return batch_id, upload_url
def _upload_to_presigned_url(self, upload_url: str) -> None:
"""
Upload file to presigned URL (no authentication needed).
"""
log.info(f"Uploading file to presigned URL")
try:
with open(self.file_path, "rb") as f:
response = requests.put(
upload_url,
data=f,
timeout=self.timeout,
)
response.raise_for_status()
except FileNotFoundError:
raise HTTPException(
status.HTTP_404_NOT_FOUND, detail=f"File not found: {self.file_path}"
)
except requests.Timeout:
raise HTTPException(
status.HTTP_504_GATEWAY_TIMEOUT,
detail="File upload to presigned URL timed out",
)
except requests.HTTPError as e:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail=f"Failed to upload file to presigned URL: {e}",
)
except Exception as e:
raise HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error uploading file: {str(e)}",
)
log.info("File uploaded successfully")
def _poll_batch_status(self, batch_id: str, filename: str) -> dict:
"""
Poll batch status until completion.
Returns the result dict for the file.
"""
headers = {
"Authorization": f"Bearer {self.api_key}",
}
max_iterations = 300 # 10 minutes max (2 seconds per iteration)
poll_interval = 2 # seconds
log.info(f"Polling batch status: {batch_id}")
for iteration in range(max_iterations):
try:
response = requests.get(
f"{self.api_url}/extract-results/batch/{batch_id}",
headers=headers,
timeout=30,
)
response.raise_for_status()
except requests.HTTPError as e:
error_detail = f"Failed to poll batch status: {e}"
if e.response is not None:
try:
error_data = e.response.json()
error_detail += f" - {error_data.get('msg', error_data)}"
except:
error_detail += f" - {e.response.text}"
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=error_detail)
except Exception as e:
raise HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error polling batch status: {str(e)}",
)
try:
result = response.json()
except ValueError as e:
raise HTTPException(
status.HTTP_502_BAD_GATEWAY,
detail=f"Invalid JSON response while polling: {e}",
)
# Check for API error response
if result.get("code") != 0:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail=f"MinerU Cloud API error: {result.get('msg', 'Unknown error')}",
)
data = result.get("data", {})
extract_result = data.get("extract_result", [])
# Find our file in the batch results
file_result = None
for item in extract_result:
if item.get("file_name") == filename:
file_result = item
break
if not file_result:
raise HTTPException(
status.HTTP_502_BAD_GATEWAY,
detail=f"File {filename} not found in batch results",
)
state = file_result.get("state")
if state == "done":
log.info(f"Processing complete for {filename}")
return file_result
elif state == "failed":
error_msg = file_result.get("err_msg", "Unknown error")
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail=f"MinerU processing failed: {error_msg}",
)
elif state in ["waiting-file", "pending", "running", "converting"]:
# Still processing
if iteration % 10 == 0: # Log every 20 seconds
log.info(
f"Processing status: {state} (iteration {iteration + 1}/{max_iterations})"
)
time.sleep(poll_interval)
else:
log.warning(f"Unknown state: {state}")
time.sleep(poll_interval)
# Timeout
raise HTTPException(
status.HTTP_504_GATEWAY_TIMEOUT,
detail="MinerU processing timed out after 10 minutes",
)
def _download_and_extract_zip(self, zip_url: str, filename: str) -> str:
"""
Download ZIP file from CDN and extract markdown content.
Returns the markdown content as a string.
"""
log.info(f"Downloading results from: {zip_url}")
try:
response = requests.get(zip_url, timeout=60)
response.raise_for_status()
except requests.HTTPError as e:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail=f"Failed to download results ZIP: {e}",
)
except Exception as e:
raise HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error downloading results: {str(e)}",
)
# Save ZIP to temporary file and extract
try:
with tempfile.NamedTemporaryFile(delete=False, suffix=".zip") as tmp_zip:
tmp_zip.write(response.content)
tmp_zip_path = tmp_zip.name
with tempfile.TemporaryDirectory() as tmp_dir:
# Extract ZIP
with zipfile.ZipFile(tmp_zip_path, "r") as zip_ref:
zip_ref.extractall(tmp_dir)
# Find markdown file - search recursively for any .md file
markdown_content = None
found_md_path = None
# First, list all files in the ZIP for debugging
all_files = []
for root, dirs, files in os.walk(tmp_dir):
for file in files:
full_path = os.path.join(root, file)
all_files.append(full_path)
# Look for any .md file
if file.endswith(".md"):
found_md_path = full_path
log.info(f"Found markdown file at: {full_path}")
try:
with open(full_path, "r", encoding="utf-8") as f:
markdown_content = f.read()
if (
markdown_content
): # Use the first non-empty markdown file
break
except Exception as e:
log.warning(f"Failed to read {full_path}: {e}")
if markdown_content:
break
if markdown_content is None:
log.error(f"Available files in ZIP: {all_files}")
# Try to provide more helpful error message
md_files = [f for f in all_files if f.endswith(".md")]
if md_files:
error_msg = (
f"Found .md files but couldn't read them: {md_files}"
)
else:
error_msg = (
f"No .md files found in ZIP. Available files: {all_files}"
)
raise HTTPException(
status.HTTP_502_BAD_GATEWAY,
detail=error_msg,
)
# Clean up temporary ZIP file
os.unlink(tmp_zip_path)
except zipfile.BadZipFile as e:
raise HTTPException(
status.HTTP_502_BAD_GATEWAY,
detail=f"Invalid ZIP file received: {e}",
)
except Exception as e:
raise HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error extracting ZIP: {str(e)}",
)
if not markdown_content:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail="Extracted markdown content is empty",
)
log.info(
f"Successfully extracted markdown content ({len(markdown_content)} characters)"
)
return markdown_content
+13 -12
View File
@@ -9,11 +9,10 @@ from typing import List, Dict, Any
from contextlib import asynccontextmanager
from langchain_core.documents import Document
from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL
from open_webui.env import GLOBAL_LOG_LEVEL
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class MistralLoader:
@@ -30,10 +29,9 @@ class MistralLoader:
- Enhanced error handling with retryable error classification
"""
BASE_API_URL = "https://api.mistral.ai/v1"
def __init__(
self,
base_url: str,
api_key: str,
file_path: str,
timeout: int = 300, # 5 minutes default
@@ -55,6 +53,9 @@ class MistralLoader:
if not os.path.exists(file_path):
raise FileNotFoundError(f"File not found at {file_path}")
self.base_url = (
base_url.rstrip("/") if base_url else "https://api.mistral.ai/v1"
)
self.api_key = api_key
self.file_path = file_path
self.timeout = timeout
@@ -240,7 +241,7 @@ class MistralLoader:
in a context manager to minimize memory usage duration.
"""
log.info("Uploading file to Mistral API")
url = f"{self.BASE_API_URL}/files"
url = f"{self.base_url}/files"
def upload_request():
# MEMORY OPTIMIZATION: Use context manager to minimize file handle lifetime
@@ -275,7 +276,7 @@ class MistralLoader:
async def _upload_file_async(self, session: aiohttp.ClientSession) -> str:
"""Async file upload with streaming for better memory efficiency."""
url = f"{self.BASE_API_URL}/files"
url = f"{self.base_url}/files"
async def upload_request():
# Create multipart writer for streaming upload
@@ -321,7 +322,7 @@ class MistralLoader:
def _get_signed_url(self, file_id: str) -> str:
"""Retrieves a temporary signed URL for the uploaded file (sync version)."""
log.info(f"Getting signed URL for file ID: {file_id}")
url = f"{self.BASE_API_URL}/files/{file_id}/url"
url = f"{self.base_url}/files/{file_id}/url"
params = {"expiry": 1}
signed_url_headers = {**self.headers, "Accept": "application/json"}
@@ -346,7 +347,7 @@ class MistralLoader:
self, session: aiohttp.ClientSession, file_id: str
) -> str:
"""Async signed URL retrieval."""
url = f"{self.BASE_API_URL}/files/{file_id}/url"
url = f"{self.base_url}/files/{file_id}/url"
params = {"expiry": 1}
headers = {**self.headers, "Accept": "application/json"}
@@ -373,7 +374,7 @@ class MistralLoader:
def _process_ocr(self, signed_url: str) -> Dict[str, Any]:
"""Sends the signed URL to the OCR endpoint for processing (sync version)."""
log.info("Processing OCR via Mistral API")
url = f"{self.BASE_API_URL}/ocr"
url = f"{self.base_url}/ocr"
ocr_headers = {
**self.headers,
"Content-Type": "application/json",
@@ -407,7 +408,7 @@ class MistralLoader:
self, session: aiohttp.ClientSession, signed_url: str
) -> Dict[str, Any]:
"""Async OCR processing with timing metrics."""
url = f"{self.BASE_API_URL}/ocr"
url = f"{self.base_url}/ocr"
headers = {
**self.headers,
@@ -446,7 +447,7 @@ class MistralLoader:
def _delete_file(self, file_id: str) -> None:
"""Deletes the file from Mistral storage (sync version)."""
log.info(f"Deleting uploaded file ID: {file_id}")
url = f"{self.BASE_API_URL}/files/{file_id}"
url = f"{self.base_url}/files/{file_id}"
try:
response = requests.delete(
@@ -467,7 +468,7 @@ class MistralLoader:
async def delete_request():
self._debug_log(f"Deleting file ID: {file_id}")
async with session.delete(
url=f"{self.BASE_API_URL}/files/{file_id}",
url=f"{self.base_url}/files/{file_id}",
headers=self.headers,
timeout=aiohttp.ClientTimeout(
total=self.cleanup_timeout
@@ -4,10 +4,8 @@ from typing import Iterator, List, Literal, Union
from langchain_core.document_loaders import BaseLoader
from langchain_core.documents import Document
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class TavilyLoader(BaseLoader):
@@ -4,10 +4,8 @@ from xml.etree.ElementTree import ParseError
from typing import Any, Dict, Generator, List, Optional, Sequence, Union
from urllib.parse import parse_qs, urlparse
from langchain_core.documents import Document
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
ALLOWED_SCHEMES = {"http", "https"}
ALLOWED_NETLOCS = {
@@ -83,6 +81,7 @@ class YoutubeLoader:
TranscriptsDisabled,
YouTubeTranscriptApi,
)
from youtube_transcript_api.proxies import GenericProxyConfig
except ImportError:
raise ImportError(
'Could not import "youtube_transcript_api" Python package. '
@@ -90,10 +89,9 @@ class YoutubeLoader:
)
if self.proxy_url:
youtube_proxies = {
"http": self.proxy_url,
"https": self.proxy_url,
}
youtube_proxies = GenericProxyConfig(
http_url=self.proxy_url, https_url=self.proxy_url
)
log.debug(f"Using proxy URL: {self.proxy_url[:14]}...")
else:
youtube_proxies = None
@@ -157,3 +155,10 @@ class YoutubeLoader:
f"No transcript found for any of the specified languages: {languages_tried}. Verify if the video has transcripts, add more languages if needed."
)
raise NoTranscriptFound(self.video_id, self.language, list(transcript_list))
async def aload(self) -> Generator[Document, None, None]:
"""Asynchronously load YouTube transcripts into `Document` objects."""
import asyncio
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, self.load)
@@ -5,12 +5,10 @@ import numpy as np
from colbert.infra import ColBERTConfig
from colbert.modeling.checkpoint import Checkpoint
from open_webui.env import SRC_LOG_LEVELS
from open_webui.retrieval.models.base_reranker import BaseReranker
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class ColBERT(BaseReranker):
+15 -17
View File
@@ -4,12 +4,11 @@ from typing import Optional, List, Tuple
from urllib.parse import quote
from open_webui.env import ENABLE_FORWARD_USER_INFO_HEADERS, SRC_LOG_LEVELS
from open_webui.env import ENABLE_FORWARD_USER_INFO_HEADERS, REQUESTS_VERIFY
from open_webui.retrieval.models.base_reranker import BaseReranker
from open_webui.utils.headers import include_user_info_headers
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class ExternalReranker(BaseReranker):
@@ -18,10 +17,12 @@ class ExternalReranker(BaseReranker):
api_key: str,
url: str = "http://localhost:8080/v1/rerank",
model: str = "reranker",
timeout: Optional[int] = None,
):
self.api_key = api_key
self.url = url
self.model = model
self.timeout = timeout
def predict(
self, sentences: List[Tuple[str, str]], user=None
@@ -40,23 +41,20 @@ class ExternalReranker(BaseReranker):
log.info(f"ExternalReranker:predict:model {self.model}")
log.info(f"ExternalReranker:predict:query {query}")
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}",
}
if ENABLE_FORWARD_USER_INFO_HEADERS and user:
headers = include_user_info_headers(headers, user)
r = requests.post(
f"{self.url}",
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}",
**(
{
"X-OpenWebUI-User-Name": quote(user.name, safe=" "),
"X-OpenWebUI-User-Id": user.id,
"X-OpenWebUI-User-Email": user.email,
"X-OpenWebUI-User-Role": user.role,
}
if ENABLE_FORWARD_USER_INFO_HEADERS and user
else {}
),
},
headers=headers,
json=payload,
timeout=self.timeout,
verify=REQUESTS_VERIFY,
)
r.raise_for_status()
File diff suppressed because it is too large Load Diff
@@ -11,7 +11,7 @@ from open_webui.retrieval.vector.main import (
SearchResult,
GetResult,
)
from open_webui.retrieval.vector.utils import stringify_metadata
from open_webui.retrieval.vector.utils import process_metadata
from open_webui.config import (
CHROMA_DATA_PATH,
@@ -24,10 +24,8 @@ from open_webui.config import (
CHROMA_CLIENT_AUTH_PROVIDER,
CHROMA_CLIENT_AUTH_CREDENTIALS,
)
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class ChromaClient(VectorDBBase):
@@ -71,7 +69,11 @@ class ChromaClient(VectorDBBase):
return self.client.delete_collection(name=collection_name)
def search(
self, collection_name: str, vectors: list[list[float | int]], limit: int
self,
collection_name: str,
vectors: list[list[float | int]],
filter: Optional[dict] = None,
limit: int = 10,
) -> Optional[SearchResult]:
# Search for the nearest neighbor items based on the vectors and return 'limit' number of results.
try:
@@ -80,6 +82,7 @@ class ChromaClient(VectorDBBase):
result = collection.query(
query_embeddings=vectors,
n_results=limit,
where=filter,
)
# chromadb has cosine distance, 2 (worst) -> 0 (best). Re-odering to 0 -> 1
@@ -146,7 +149,7 @@ class ChromaClient(VectorDBBase):
ids = [item["id"] for item in items]
documents = [item["text"] for item in items]
embeddings = [item["vector"] for item in items]
metadatas = [stringify_metadata(item["metadata"]) for item in items]
metadatas = [process_metadata(item["metadata"]) for item in items]
for batch in create_batches(
api=self.client,
@@ -166,7 +169,7 @@ class ChromaClient(VectorDBBase):
ids = [item["id"] for item in items]
documents = [item["text"] for item in items]
embeddings = [item["vector"] for item in items]
metadatas = [stringify_metadata(item["metadata"]) for item in items]
metadatas = [process_metadata(item["metadata"]) for item in items]
collection.upsert(
ids=ids, documents=documents, embeddings=embeddings, metadatas=metadatas
@@ -3,7 +3,7 @@ from typing import Optional
import ssl
from elasticsearch.helpers import bulk, scan
from open_webui.retrieval.vector.utils import stringify_metadata
from open_webui.retrieval.vector.utils import process_metadata
from open_webui.retrieval.vector.main import (
VectorDBBase,
VectorItem,
@@ -153,7 +153,11 @@ class ElasticsearchClient(VectorDBBase):
# Status: works
def search(
self, collection_name: str, vectors: list[list[float]], limit: int
self,
collection_name: str,
vectors: list[list[float]],
filter: Optional[dict] = None,
limit: int = 10,
) -> Optional[SearchResult]:
query = {
"size": limit,
@@ -245,7 +249,7 @@ class ElasticsearchClient(VectorDBBase):
"collection": collection_name,
"vector": item["vector"],
"text": item["text"],
"metadata": stringify_metadata(item["metadata"]),
"metadata": process_metadata(item["metadata"]),
},
}
for item in batch
@@ -266,7 +270,7 @@ class ElasticsearchClient(VectorDBBase):
"collection": collection_name,
"vector": item["vector"],
"text": item["text"],
"metadata": stringify_metadata(item["metadata"]),
"metadata": process_metadata(item["metadata"]),
},
"doc_as_upsert": True,
}
@@ -6,7 +6,7 @@ import json
import logging
from typing import Optional
from open_webui.retrieval.vector.utils import stringify_metadata
from open_webui.retrieval.vector.utils import process_metadata
from open_webui.retrieval.vector.main import (
VectorDBBase,
VectorItem,
@@ -22,11 +22,11 @@ from open_webui.config import (
MILVUS_HNSW_M,
MILVUS_HNSW_EFCONSTRUCTION,
MILVUS_IVF_FLAT_NLIST,
MILVUS_DISKANN_MAX_DEGREE,
MILVUS_DISKANN_SEARCH_LIST_SIZE,
)
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class MilvusClient(VectorDBBase):
@@ -131,12 +131,18 @@ class MilvusClient(VectorDBBase):
elif index_type == "IVF_FLAT":
index_creation_params = {"nlist": MILVUS_IVF_FLAT_NLIST}
log.info(f"IVF_FLAT params: {index_creation_params}")
elif index_type == "DISKANN":
index_creation_params = {
"max_degree": MILVUS_DISKANN_MAX_DEGREE,
"search_list_size": MILVUS_DISKANN_SEARCH_LIST_SIZE,
}
log.info(f"DISKANN params: {index_creation_params}")
elif index_type in ["FLAT", "AUTOINDEX"]:
log.info(f"Using {index_type} index with no specific build-time params.")
else:
log.warning(
f"Unsupported MILVUS_INDEX_TYPE: '{index_type}'. "
f"Supported types: HNSW, IVF_FLAT, FLAT, AUTOINDEX. "
f"Supported types: HNSW, IVF_FLAT, DISKANN, FLAT, AUTOINDEX. "
f"Milvus will use its default for the collection if this type is not directly supported for index creation."
)
# For unsupported types, pass the type directly to Milvus; it might handle it or use a default.
@@ -173,7 +179,11 @@ class MilvusClient(VectorDBBase):
)
def search(
self, collection_name: str, vectors: list[list[float | int]], limit: int
self,
collection_name: str,
vectors: list[list[float | int]],
filter: Optional[dict] = None,
limit: int = 10,
) -> Optional[SearchResult]:
# Search for the nearest neighbor items based on the vectors and return 'limit' number of results.
collection_name = collection_name.replace("-", "_")
@@ -189,26 +199,27 @@ class MilvusClient(VectorDBBase):
)
return self._result_to_search_result(result)
def query(self, collection_name: str, filter: dict, limit: Optional[int] = None):
def query(self, collection_name: str, filter: dict, limit: int = -1):
connections.connect(uri=MILVUS_URI, token=MILVUS_TOKEN, db_name=MILVUS_DB)
# Construct the filter string for querying
collection_name = collection_name.replace("-", "_")
if not self.has_collection(collection_name):
log.warning(
f"Query attempted on non-existent collection: {self.collection_prefix}_{collection_name}"
)
return None
filter_string = " && ".join(
[
f'metadata["{key}"] == {json.dumps(value)}'
for key, value in filter.items()
]
)
filter_expressions = []
for key, value in filter.items():
if isinstance(value, str):
filter_expressions.append(f'metadata["{key}"] == "{value}"')
else:
filter_expressions.append(f'metadata["{key}"] == {value}')
filter_string = " && ".join(filter_expressions)
collection = Collection(f"{self.collection_prefix}_{collection_name}")
collection.load()
all_results = []
try:
log.info(
@@ -216,24 +227,25 @@ class MilvusClient(VectorDBBase):
)
iterator = collection.query_iterator(
filter=filter_string,
expr=filter_string,
output_fields=[
"id",
"data",
"metadata",
],
limit=limit, # Pass the limit directly; None means no limit.
limit=limit if limit > 0 else -1,
)
all_results = []
while True:
result = iterator.next()
if not result:
batch = iterator.next()
if not batch:
iterator.close()
break
all_results += result
all_results.extend(batch)
log.info(f"Total results from query: {len(all_results)}")
return self._result_to_get_result([all_results])
log.debug(f"Total results from query: {len(all_results)}")
return self._result_to_get_result([all_results] if all_results else [[]])
except Exception as e:
log.exception(
@@ -249,7 +261,7 @@ class MilvusClient(VectorDBBase):
)
# Using query with a trivial filter to get all items.
# This will use the paginated query logic.
return self.query(collection_name=collection_name, filter={}, limit=None)
return self.query(collection_name=collection_name, filter={}, limit=-1)
def insert(self, collection_name: str, items: list[VectorItem]):
# Insert the items into the collection, if the collection does not exist, it will be created.
@@ -281,7 +293,7 @@ class MilvusClient(VectorDBBase):
"id": item["id"],
"vector": item["vector"],
"data": {"text": item["text"]},
"metadata": stringify_metadata(item["metadata"]),
"metadata": process_metadata(item["metadata"]),
}
for item in items
],
@@ -317,7 +329,7 @@ class MilvusClient(VectorDBBase):
"id": item["id"],
"vector": item["vector"],
"data": {"text": item["text"]},
"metadata": stringify_metadata(item["metadata"]),
"metadata": process_metadata(item["metadata"]),
}
for item in items
],
@@ -0,0 +1,291 @@
import logging
from typing import Optional, Tuple, List, Dict, Any
from open_webui.config import (
MILVUS_URI,
MILVUS_TOKEN,
MILVUS_DB,
MILVUS_COLLECTION_PREFIX,
MILVUS_INDEX_TYPE,
MILVUS_METRIC_TYPE,
MILVUS_HNSW_M,
MILVUS_HNSW_EFCONSTRUCTION,
MILVUS_IVF_FLAT_NLIST,
)
from open_webui.retrieval.vector.main import (
GetResult,
SearchResult,
VectorDBBase,
VectorItem,
)
from pymilvus import (
connections,
utility,
Collection,
CollectionSchema,
FieldSchema,
DataType,
)
log = logging.getLogger(__name__)
RESOURCE_ID_FIELD = "resource_id"
class MilvusClient(VectorDBBase):
def __init__(self):
# Milvus collection names can only contain numbers, letters, and underscores.
self.collection_prefix = MILVUS_COLLECTION_PREFIX.replace("-", "_")
connections.connect(
alias="default",
uri=MILVUS_URI,
token=MILVUS_TOKEN,
db_name=MILVUS_DB,
)
# Main collection types for multi-tenancy
self.MEMORY_COLLECTION = f"{self.collection_prefix}_memories"
self.KNOWLEDGE_COLLECTION = f"{self.collection_prefix}_knowledge"
self.FILE_COLLECTION = f"{self.collection_prefix}_files"
self.WEB_SEARCH_COLLECTION = f"{self.collection_prefix}_web_search"
self.HASH_BASED_COLLECTION = f"{self.collection_prefix}_hash_based"
self.shared_collections = [
self.MEMORY_COLLECTION,
self.KNOWLEDGE_COLLECTION,
self.FILE_COLLECTION,
self.WEB_SEARCH_COLLECTION,
self.HASH_BASED_COLLECTION,
]
def _get_collection_and_resource_id(self, collection_name: str) -> Tuple[str, str]:
"""
Maps the traditional collection name to multi-tenant collection and resource ID.
WARNING: This mapping relies on current Open WebUI naming conventions for
collection names. If Open WebUI changes how it generates collection names
(e.g., "user-memory-" prefix, "file-" prefix, web search patterns, or hash
formats), this mapping will break and route data to incorrect collections.
POTENTIALLY CAUSING HUGE DATA CORRUPTION, DATA CONSISTENCY ISSUES AND INCORRECT
DATA MAPPING INSIDE THE DATABASE.
"""
resource_id = collection_name
if collection_name.startswith("user-memory-"):
return self.MEMORY_COLLECTION, resource_id
elif collection_name.startswith("file-"):
return self.FILE_COLLECTION, resource_id
elif collection_name.startswith("web-search-"):
return self.WEB_SEARCH_COLLECTION, resource_id
elif len(collection_name) == 63 and all(
c in "0123456789abcdef" for c in collection_name
):
return self.HASH_BASED_COLLECTION, resource_id
else:
return self.KNOWLEDGE_COLLECTION, resource_id
def _create_shared_collection(self, mt_collection_name: str, dimension: int):
fields = [
FieldSchema(
name="id",
dtype=DataType.VARCHAR,
is_primary=True,
auto_id=False,
max_length=36,
),
FieldSchema(name="vector", dtype=DataType.FLOAT_VECTOR, dim=dimension),
FieldSchema(name="text", dtype=DataType.VARCHAR, max_length=65535),
FieldSchema(name="metadata", dtype=DataType.JSON),
FieldSchema(name=RESOURCE_ID_FIELD, dtype=DataType.VARCHAR, max_length=255),
]
schema = CollectionSchema(fields, "Shared collection for multi-tenancy")
collection = Collection(mt_collection_name, schema)
index_params = {
"metric_type": MILVUS_METRIC_TYPE,
"index_type": MILVUS_INDEX_TYPE,
"params": {},
}
if MILVUS_INDEX_TYPE == "HNSW":
index_params["params"] = {
"M": MILVUS_HNSW_M,
"efConstruction": MILVUS_HNSW_EFCONSTRUCTION,
}
elif MILVUS_INDEX_TYPE == "IVF_FLAT":
index_params["params"] = {"nlist": MILVUS_IVF_FLAT_NLIST}
collection.create_index("vector", index_params)
collection.create_index(RESOURCE_ID_FIELD)
log.info(f"Created shared collection: {mt_collection_name}")
return collection
def _ensure_collection(self, mt_collection_name: str, dimension: int):
if not utility.has_collection(mt_collection_name):
self._create_shared_collection(mt_collection_name, dimension)
def has_collection(self, collection_name: str) -> bool:
mt_collection, resource_id = self._get_collection_and_resource_id(
collection_name
)
if not utility.has_collection(mt_collection):
return False
collection = Collection(mt_collection)
collection.load()
res = collection.query(expr=f"{RESOURCE_ID_FIELD} == '{resource_id}'", limit=1)
return len(res) > 0
def upsert(self, collection_name: str, items: List[VectorItem]):
if not items:
return
mt_collection, resource_id = self._get_collection_and_resource_id(
collection_name
)
dimension = len(items[0]["vector"])
self._ensure_collection(mt_collection, dimension)
collection = Collection(mt_collection)
entities = [
{
"id": item["id"],
"vector": item["vector"],
"text": item["text"],
"metadata": item["metadata"],
RESOURCE_ID_FIELD: resource_id,
}
for item in items
]
collection.insert(entities)
def search(
self,
collection_name: str,
vectors: List[List[float]],
filter: Optional[Dict] = None,
limit: int = 10,
) -> Optional[SearchResult]:
if not vectors:
return None
mt_collection, resource_id = self._get_collection_and_resource_id(
collection_name
)
if not utility.has_collection(mt_collection):
return None
collection = Collection(mt_collection)
collection.load()
search_params = {"metric_type": MILVUS_METRIC_TYPE, "params": {}}
results = collection.search(
data=vectors,
anns_field="vector",
param=search_params,
limit=limit,
expr=f"{RESOURCE_ID_FIELD} == '{resource_id}'",
output_fields=["id", "text", "metadata"],
)
ids, documents, metadatas, distances = [], [], [], []
for hits in results:
batch_ids, batch_docs, batch_metadatas, batch_dists = [], [], [], []
for hit in hits:
batch_ids.append(hit.entity.get("id"))
batch_docs.append(hit.entity.get("text"))
batch_metadatas.append(hit.entity.get("metadata"))
batch_dists.append(hit.distance)
ids.append(batch_ids)
documents.append(batch_docs)
metadatas.append(batch_metadatas)
distances.append(batch_dists)
return SearchResult(
ids=ids, documents=documents, metadatas=metadatas, distances=distances
)
def delete(
self,
collection_name: str,
ids: Optional[List[str]] = None,
filter: Optional[Dict[str, Any]] = None,
):
mt_collection, resource_id = self._get_collection_and_resource_id(
collection_name
)
if not utility.has_collection(mt_collection):
return
collection = Collection(mt_collection)
# Build expression
expr = [f"{RESOURCE_ID_FIELD} == '{resource_id}'"]
if ids:
# Milvus expects a string list for 'in' operator
id_list_str = ", ".join([f"'{id_val}'" for id_val in ids])
expr.append(f"id in [{id_list_str}]")
if filter:
for key, value in filter.items():
expr.append(f"metadata['{key}'] == '{value}'")
collection.delete(" and ".join(expr))
def reset(self):
for collection_name in self.shared_collections:
if utility.has_collection(collection_name):
utility.drop_collection(collection_name)
def delete_collection(self, collection_name: str):
mt_collection, resource_id = self._get_collection_and_resource_id(
collection_name
)
if not utility.has_collection(mt_collection):
return
collection = Collection(mt_collection)
collection.delete(f"{RESOURCE_ID_FIELD} == '{resource_id}'")
def query(
self, collection_name: str, filter: Dict[str, Any], limit: Optional[int] = None
) -> Optional[GetResult]:
mt_collection, resource_id = self._get_collection_and_resource_id(
collection_name
)
if not utility.has_collection(mt_collection):
return None
collection = Collection(mt_collection)
collection.load()
expr = [f"{RESOURCE_ID_FIELD} == '{resource_id}'"]
if filter:
for key, value in filter.items():
if isinstance(value, str):
expr.append(f"metadata['{key}'] == '{value}'")
else:
expr.append(f"metadata['{key}'] == {value}")
iterator = collection.query_iterator(
expr=" and ".join(expr),
output_fields=["id", "text", "metadata"],
limit=limit if limit else -1,
)
all_results = []
while True:
batch = iterator.next()
if not batch:
iterator.close()
break
all_results.extend(batch)
ids = [res["id"] for res in all_results]
documents = [res["text"] for res in all_results]
metadatas = [res["metadata"] for res in all_results]
return GetResult(ids=[ids], documents=[documents], metadatas=[metadatas])
def get(self, collection_name: str) -> Optional[GetResult]:
return self.query(collection_name, filter={}, limit=None)
def insert(self, collection_name: str, items: List[VectorItem]):
return self.upsert(collection_name, items)
@@ -0,0 +1,427 @@
from typing import Optional, List, Dict, Any
import logging
import re
import json
from sqlalchemy import (
func,
literal,
cast,
column,
create_engine,
Column,
Integer,
MetaData,
LargeBinary,
select,
text,
Text,
Table,
values,
)
from sqlalchemy.sql import true
from sqlalchemy.pool import NullPool, QueuePool
from sqlalchemy.orm import declarative_base, scoped_session, sessionmaker
from sqlalchemy.dialects.postgresql import JSONB, array
from pgvector.sqlalchemy import Vector
from sqlalchemy.ext.mutable import MutableDict
from sqlalchemy.exc import NoSuchTableError
from sqlalchemy.dialects.postgresql.psycopg2 import PGDialect_psycopg2
from sqlalchemy.dialects import registry
class OpenGaussDialect(PGDialect_psycopg2):
name = "opengauss"
def _get_server_version_info(self, connection):
try:
version = connection.exec_driver_sql("SELECT version()").scalar()
if not version:
return (9, 0, 0)
match = re.search(
r"openGauss\s+(\d+)\.(\d+)\.(\d+)(?:-\w+)?", version, re.IGNORECASE
)
if match:
return (int(match.group(1)), int(match.group(2)), int(match.group(3)))
return super()._get_server_version_info(connection)
except Exception:
return (9, 0, 0)
# Register dialect
registry.register("opengauss", __name__, "OpenGaussDialect")
from open_webui.retrieval.vector.utils import process_metadata
from open_webui.retrieval.vector.main import (
VectorDBBase,
VectorItem,
SearchResult,
GetResult,
)
from open_webui.config import (
OPENGAUSS_DB_URL,
OPENGAUSS_INITIALIZE_MAX_VECTOR_LENGTH,
OPENGAUSS_POOL_SIZE,
OPENGAUSS_POOL_MAX_OVERFLOW,
OPENGAUSS_POOL_TIMEOUT,
OPENGAUSS_POOL_RECYCLE,
)
from open_webui.env import SRC_LOG_LEVELS
VECTOR_LENGTH = OPENGAUSS_INITIALIZE_MAX_VECTOR_LENGTH
Base = declarative_base()
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class DocumentChunk(Base):
__tablename__ = "document_chunk"
id = Column(Text, primary_key=True)
vector = Column(Vector(dim=VECTOR_LENGTH), nullable=True)
collection_name = Column(Text, nullable=False)
text = Column(Text, nullable=True)
vmetadata = Column(MutableDict.as_mutable(JSONB), nullable=True)
class OpenGaussClient(VectorDBBase):
def __init__(self) -> None:
if not OPENGAUSS_DB_URL:
from open_webui.internal.db import ScopedSession
self.session = ScopedSession
else:
engine_kwargs = {"pool_pre_ping": True, "dialect": OpenGaussDialect()}
if isinstance(OPENGAUSS_POOL_SIZE, int) and OPENGAUSS_POOL_SIZE > 0:
engine_kwargs.update(
{
"pool_size": OPENGAUSS_POOL_SIZE,
"max_overflow": OPENGAUSS_POOL_MAX_OVERFLOW,
"pool_timeout": OPENGAUSS_POOL_TIMEOUT,
"pool_recycle": OPENGAUSS_POOL_RECYCLE,
"poolclass": QueuePool,
}
)
else:
engine_kwargs["poolclass"] = NullPool
engine = create_engine(OPENGAUSS_DB_URL, **engine_kwargs)
SessionLocal = sessionmaker(
autocommit=False, autoflush=False, bind=engine, expire_on_commit=False
)
self.session = scoped_session(SessionLocal)
try:
connection = self.session.connection()
Base.metadata.create_all(bind=connection)
self.session.execute(
text(
"CREATE INDEX IF NOT EXISTS idx_document_chunk_vector "
"ON document_chunk USING ivfflat (vector vector_cosine_ops) WITH (lists = 100);"
)
)
self.session.execute(
text(
"CREATE INDEX IF NOT EXISTS idx_document_chunk_collection_name "
"ON document_chunk (collection_name);"
)
)
self.session.commit()
log.info("OpenGauss vector database initialization completed.")
except Exception as e:
self.session.rollback()
log.exception(f"OpenGauss Initialization failed.: {e}")
raise
def check_vector_length(self) -> None:
metadata = MetaData()
try:
document_chunk_table = Table(
"document_chunk", metadata, autoload_with=self.session.bind
)
except NoSuchTableError:
return
if "vector" in document_chunk_table.columns:
vector_column = document_chunk_table.columns["vector"]
vector_type = vector_column.type
if isinstance(vector_type, Vector):
db_vector_length = vector_type.dim
if db_vector_length != VECTOR_LENGTH:
raise Exception(
f"Vector dimension mismatch: configured {VECTOR_LENGTH} vs. {db_vector_length} in the database."
)
else:
raise Exception("The 'vector' column type is not Vector.")
else:
raise Exception(
"The 'vector' column does not exist in the 'document_chunk' table."
)
def adjust_vector_length(self, vector: List[float]) -> List[float]:
current_length = len(vector)
if current_length < VECTOR_LENGTH:
vector += [0.0] * (VECTOR_LENGTH - current_length)
elif current_length > VECTOR_LENGTH:
vector = vector[:VECTOR_LENGTH]
return vector
def insert(self, collection_name: str, items: List[VectorItem]) -> None:
try:
new_items = []
for item in items:
vector = self.adjust_vector_length(item["vector"])
new_chunk = DocumentChunk(
id=item["id"],
vector=vector,
collection_name=collection_name,
text=item["text"],
vmetadata=process_metadata(item["metadata"]),
)
new_items.append(new_chunk)
self.session.bulk_save_objects(new_items)
self.session.commit()
log.info(
f"Inserting {len(new_items)} items into collection '{collection_name}'."
)
except Exception as e:
self.session.rollback()
log.exception(f"Failed to insert data: {e}")
raise
def upsert(self, collection_name: str, items: List[VectorItem]) -> None:
try:
for item in items:
vector = self.adjust_vector_length(item["vector"])
existing = (
self.session.query(DocumentChunk)
.filter(DocumentChunk.id == item["id"])
.first()
)
if existing:
existing.vector = vector
existing.text = item["text"]
existing.vmetadata = process_metadata(item["metadata"])
existing.collection_name = collection_name
else:
new_chunk = DocumentChunk(
id=item["id"],
vector=vector,
collection_name=collection_name,
text=item["text"],
vmetadata=process_metadata(item["metadata"]),
)
self.session.add(new_chunk)
self.session.commit()
log.info(
f"Inserting/updating {len(items)} items in collection '{collection_name}'."
)
except Exception as e:
self.session.rollback()
log.exception(f"Failed to insert or update data.: {e}")
raise
def search(
self,
collection_name: str,
vectors: List[List[float]],
filter: Optional[Dict[str, Any]] = None,
limit: int = 10,
) -> Optional[SearchResult]:
try:
if not vectors:
return None
vectors = [self.adjust_vector_length(vector) for vector in vectors]
num_queries = len(vectors)
def vector_expr(vector):
return cast(array(vector), Vector(VECTOR_LENGTH))
qid_col = column("qid", Integer)
q_vector_col = column("q_vector", Vector(VECTOR_LENGTH))
query_vectors = (
values(qid_col, q_vector_col)
.data(
[(idx, vector_expr(vector)) for idx, vector in enumerate(vectors)]
)
.alias("query_vectors")
)
result_fields = [
DocumentChunk.id,
DocumentChunk.text,
DocumentChunk.vmetadata,
(DocumentChunk.vector.cosine_distance(query_vectors.c.q_vector)).label(
"distance"
),
]
subq = (
select(*result_fields)
.where(DocumentChunk.collection_name == collection_name)
.order_by(
DocumentChunk.vector.cosine_distance(query_vectors.c.q_vector)
)
)
if limit is not None:
subq = subq.limit(limit)
subq = subq.lateral("result")
stmt = (
select(
query_vectors.c.qid,
subq.c.id,
subq.c.text,
subq.c.vmetadata,
subq.c.distance,
)
.select_from(query_vectors)
.join(subq, true())
.order_by(query_vectors.c.qid, subq.c.distance)
)
result_proxy = self.session.execute(stmt)
results = result_proxy.all()
ids = [[] for _ in range(num_queries)]
distances = [[] for _ in range(num_queries)]
documents = [[] for _ in range(num_queries)]
metadatas = [[] for _ in range(num_queries)]
for row in results:
qid = int(row.qid)
ids[qid].append(row.id)
distances[qid].append((2.0 - row.distance) / 2.0)
documents[qid].append(row.text)
metadatas[qid].append(row.vmetadata)
self.session.rollback()
return SearchResult(
ids=ids, distances=distances, documents=documents, metadatas=metadatas
)
except Exception as e:
self.session.rollback()
log.exception(f"Vector search failed: {e}")
return None
def query(
self, collection_name: str, filter: Dict[str, Any], limit: Optional[int] = None
) -> Optional[GetResult]:
try:
query = self.session.query(DocumentChunk).filter(
DocumentChunk.collection_name == collection_name
)
for key, value in filter.items():
query = query.filter(DocumentChunk.vmetadata[key].astext == str(value))
if limit is not None:
query = query.limit(limit)
results = query.all()
if not results:
return None
ids = [[result.id for result in results]]
documents = [[result.text for result in results]]
metadatas = [[result.vmetadata for result in results]]
self.session.rollback()
return GetResult(ids=ids, documents=documents, metadatas=metadatas)
except Exception as e:
self.session.rollback()
log.exception(f"Conditional query failed: {e}")
return None
def get(
self, collection_name: str, limit: Optional[int] = None
) -> Optional[GetResult]:
try:
query = self.session.query(DocumentChunk).filter(
DocumentChunk.collection_name == collection_name
)
if limit is not None:
query = query.limit(limit)
results = query.all()
if not results:
return None
ids = [[result.id for result in results]]
documents = [[result.text for result in results]]
metadatas = [[result.vmetadata for result in results]]
self.session.rollback()
return GetResult(ids=ids, documents=documents, metadatas=metadatas)
except Exception as e:
self.session.rollback()
log.exception(f"Failed to retrieve data: {e}")
return None
def delete(
self,
collection_name: str,
ids: Optional[List[str]] = None,
filter: Optional[Dict[str, Any]] = None,
) -> None:
try:
query = self.session.query(DocumentChunk).filter(
DocumentChunk.collection_name == collection_name
)
if ids:
query = query.filter(DocumentChunk.id.in_(ids))
if filter:
for key, value in filter.items():
query = query.filter(
DocumentChunk.vmetadata[key].astext == str(value)
)
deleted = query.delete(synchronize_session=False)
self.session.commit()
log.info(f"Deleted {deleted} items from collection '{collection_name}'")
except Exception as e:
self.session.rollback()
log.exception(f"Failed to delete data: {e}")
raise
def reset(self) -> None:
try:
deleted = self.session.query(DocumentChunk).delete()
self.session.commit()
log.info(f"Reset completed. Deleted {deleted} items")
except Exception as e:
self.session.rollback()
log.exception(f"Reset failed: {e}")
raise
def close(self) -> None:
pass
def has_collection(self, collection_name: str) -> bool:
try:
exists = (
self.session.query(DocumentChunk)
.filter(DocumentChunk.collection_name == collection_name)
.first()
is not None
)
self.session.rollback()
return exists
except Exception as e:
self.session.rollback()
log.exception(f"Failed to check collection existence: {e}")
return False
def delete_collection(self, collection_name: str) -> None:
self.delete(collection_name)
log.info(f"Collection '{collection_name}' has been deleted")
@@ -2,7 +2,7 @@ from opensearchpy import OpenSearch
from opensearchpy.helpers import bulk
from typing import Optional
from open_webui.retrieval.vector.utils import stringify_metadata
from open_webui.retrieval.vector.utils import process_metadata
from open_webui.retrieval.vector.main import (
VectorDBBase,
VectorItem,
@@ -113,7 +113,11 @@ class OpenSearchClient(VectorDBBase):
self.client.indices.delete(index=self._get_index_name(collection_name))
def search(
self, collection_name: str, vectors: list[list[float | int]], limit: int
self,
collection_name: str,
vectors: list[list[float | int]],
filter: Optional[dict] = None,
limit: int = 10,
) -> Optional[SearchResult]:
try:
if not self.has_collection(collection_name):
@@ -201,13 +205,13 @@ class OpenSearchClient(VectorDBBase):
"_source": {
"vector": item["vector"],
"text": item["text"],
"metadata": stringify_metadata(item["metadata"]),
"metadata": process_metadata(item["metadata"]),
},
}
for item in batch
]
bulk(self.client, actions)
self.client.indices.refresh(self._get_index_name(collection_name))
self.client.indices.refresh(index=self._get_index_name(collection_name))
def upsert(self, collection_name: str, items: list[VectorItem]):
self._create_index_if_not_exists(
@@ -223,14 +227,14 @@ class OpenSearchClient(VectorDBBase):
"doc": {
"vector": item["vector"],
"text": item["text"],
"metadata": stringify_metadata(item["metadata"]),
"metadata": process_metadata(item["metadata"]),
},
"doc_as_upsert": True,
}
for item in batch
]
bulk(self.client, actions)
self.client.indices.refresh(self._get_index_name(collection_name))
self.client.indices.refresh(index=self._get_index_name(collection_name))
def delete(
self,
@@ -259,7 +263,7 @@ class OpenSearchClient(VectorDBBase):
self.client.delete_by_query(
index=self._get_index_name(collection_name), body=query_body
)
self.client.indices.refresh(self._get_index_name(collection_name))
self.client.indices.refresh(index=self._get_index_name(collection_name))
def reset(self):
indices = self.client.indices.get(index=f"{self.index_prefix}_*")
@@ -55,10 +55,8 @@ from open_webui.config import (
ORACLE_DB_POOL_MAX,
ORACLE_DB_POOL_INCREMENT,
)
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class Oracle23aiClient(VectorDBBase):
@@ -258,8 +256,7 @@ class Oracle23aiClient(VectorDBBase):
with connection.cursor() as cursor:
try:
log.info("Creating Table document_chunk")
cursor.execute(
"""
cursor.execute("""
BEGIN
EXECUTE IMMEDIATE '
CREATE TABLE IF NOT EXISTS document_chunk (
@@ -276,12 +273,10 @@ class Oracle23aiClient(VectorDBBase):
RAISE;
END IF;
END;
"""
)
""")
log.info("Creating Index document_chunk_collection_name_idx")
cursor.execute(
"""
cursor.execute("""
BEGIN
EXECUTE IMMEDIATE '
CREATE INDEX IF NOT EXISTS document_chunk_collection_name_idx
@@ -293,12 +288,10 @@ class Oracle23aiClient(VectorDBBase):
RAISE;
END IF;
END;
"""
)
""")
log.info("Creating VECTOR INDEX document_chunk_vector_ivf_idx")
cursor.execute(
"""
cursor.execute("""
BEGIN
EXECUTE IMMEDIATE '
CREATE VECTOR INDEX IF NOT EXISTS document_chunk_vector_ivf_idx
@@ -314,8 +307,7 @@ class Oracle23aiClient(VectorDBBase):
RAISE;
END IF;
END;
"""
)
""")
connection.commit()
log.info("Database initialization completed successfully.")
@@ -523,7 +515,11 @@ class Oracle23aiClient(VectorDBBase):
raise
def search(
self, collection_name: str, vectors: List[List[Union[float, int]]], limit: int
self,
collection_name: str,
vectors: List[List[Union[float, int]]],
filter: Optional[dict] = None,
limit: int = 10,
) -> Optional[SearchResult]:
"""
Search for similar vectors in the database.
@@ -717,7 +713,7 @@ class Oracle23aiClient(VectorDBBase):
)
try:
limit = limit or 1000
limit = 1000 # Hardcoded limit for get operation
with self.get_connection() as connection:
with connection.cursor() as cursor:
@@ -1,4 +1,4 @@
from typing import Optional, List, Dict, Any
from typing import Optional, List, Dict, Any, Tuple
import logging
import json
from sqlalchemy import (
@@ -22,12 +22,12 @@ from sqlalchemy.pool import NullPool, QueuePool
from sqlalchemy.orm import declarative_base, scoped_session, sessionmaker
from sqlalchemy.dialects.postgresql import JSONB, array
from pgvector.sqlalchemy import Vector
from pgvector.sqlalchemy import Vector, HALFVEC
from sqlalchemy.ext.mutable import MutableDict
from sqlalchemy.exc import NoSuchTableError
from open_webui.retrieval.vector.utils import stringify_metadata
from open_webui.retrieval.vector.utils import process_metadata
from open_webui.retrieval.vector.main import (
VectorDBBase,
VectorItem,
@@ -44,15 +44,21 @@ from open_webui.config import (
PGVECTOR_POOL_MAX_OVERFLOW,
PGVECTOR_POOL_TIMEOUT,
PGVECTOR_POOL_RECYCLE,
PGVECTOR_INDEX_METHOD,
PGVECTOR_HNSW_M,
PGVECTOR_HNSW_EF_CONSTRUCTION,
PGVECTOR_IVFFLAT_LISTS,
PGVECTOR_USE_HALFVEC,
)
from open_webui.env import SRC_LOG_LEVELS
VECTOR_LENGTH = PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH
USE_HALFVEC = PGVECTOR_USE_HALFVEC
VECTOR_TYPE_FACTORY = HALFVEC if USE_HALFVEC else Vector
VECTOR_OPCLASS = "halfvec_cosine_ops" if USE_HALFVEC else "vector_cosine_ops"
Base = declarative_base()
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def pgcrypto_encrypt(val, key):
@@ -67,7 +73,7 @@ class DocumentChunk(Base):
__tablename__ = "document_chunk"
id = Column(Text, primary_key=True)
vector = Column(Vector(dim=VECTOR_LENGTH), nullable=True)
vector = Column(VECTOR_TYPE_FACTORY(dim=VECTOR_LENGTH), nullable=True)
collection_name = Column(Text, nullable=False)
if PGVECTOR_PGCRYPTO:
@@ -83,9 +89,9 @@ class PgvectorClient(VectorDBBase):
# if no pgvector uri, use the existing database connection
if not PGVECTOR_DB_URL:
from open_webui.internal.db import Session
from open_webui.internal.db import ScopedSession
self.session = Session
self.session = ScopedSession
else:
if isinstance(PGVECTOR_POOL_SIZE, int):
if PGVECTOR_POOL_SIZE > 0:
@@ -114,34 +120,26 @@ class PgvectorClient(VectorDBBase):
# Ensure the pgvector extension is available
# Use a conditional check to avoid permission issues on Azure PostgreSQL
if PGVECTOR_CREATE_EXTENSION:
self.session.execute(
text(
"""
self.session.execute(text("""
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'vector') THEN
CREATE EXTENSION IF NOT EXISTS vector;
END IF;
END $$;
"""
)
)
"""))
if PGVECTOR_PGCRYPTO:
# Ensure the pgcrypto extension is available for encryption
# Use a conditional check to avoid permission issues on Azure PostgreSQL
self.session.execute(
text(
"""
self.session.execute(text("""
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pgcrypto') THEN
CREATE EXTENSION IF NOT EXISTS pgcrypto;
END IF;
END $$;
"""
)
)
"""))
if not PGVECTOR_PGCRYPTO_KEY:
raise ValueError(
@@ -157,13 +155,9 @@ class PgvectorClient(VectorDBBase):
connection = self.session.connection()
Base.metadata.create_all(bind=connection)
# Create an index on the vector column if it doesn't exist
self.session.execute(
text(
"CREATE INDEX IF NOT EXISTS idx_document_chunk_vector "
"ON document_chunk USING ivfflat (vector vector_cosine_ops) WITH (lists = 100);"
)
)
index_method, index_options = self._vector_index_configuration()
self._ensure_vector_index(index_method, index_options)
self.session.execute(
text(
"CREATE INDEX IF NOT EXISTS idx_document_chunk_collection_name "
@@ -177,6 +171,76 @@ class PgvectorClient(VectorDBBase):
log.exception(f"Error during initialization: {e}")
raise
@staticmethod
def _extract_index_method(index_def: Optional[str]) -> Optional[str]:
if not index_def:
return None
try:
after_using = index_def.lower().split("using ", 1)[1]
return after_using.split()[0]
except (IndexError, AttributeError):
return None
def _vector_index_configuration(self) -> Tuple[str, str]:
if PGVECTOR_INDEX_METHOD:
index_method = PGVECTOR_INDEX_METHOD
log.info(
"Using vector index method '%s' from PGVECTOR_INDEX_METHOD.",
index_method,
)
elif USE_HALFVEC:
index_method = "hnsw"
log.info(
"VECTOR_LENGTH=%s exceeds 2000; using halfvec column type with hnsw index.",
VECTOR_LENGTH,
)
else:
index_method = "ivfflat"
if index_method == "hnsw":
index_options = f"WITH (m = {PGVECTOR_HNSW_M}, ef_construction = {PGVECTOR_HNSW_EF_CONSTRUCTION})"
else:
index_options = f"WITH (lists = {PGVECTOR_IVFFLAT_LISTS})"
return index_method, index_options
def _ensure_vector_index(self, index_method: str, index_options: str) -> None:
index_name = "idx_document_chunk_vector"
existing_index_def = self.session.execute(
text("""
SELECT indexdef
FROM pg_indexes
WHERE schemaname = current_schema()
AND tablename = 'document_chunk'
AND indexname = :index_name
"""),
{"index_name": index_name},
).scalar()
existing_method = self._extract_index_method(existing_index_def)
if existing_method and existing_method != index_method:
raise RuntimeError(
f"Existing pgvector index '{index_name}' uses method '{existing_method}' but configuration now "
f"requires '{index_method}'. Automatic rebuild is disabled to prevent long-running maintenance. "
"Drop the index manually (optionally after tuning maintenance_work_mem/max_parallel_maintenance_workers) "
"and recreate it with the new method before restarting Open WebUI."
)
if not existing_index_def:
index_sql = (
f"CREATE INDEX IF NOT EXISTS {index_name} "
f"ON document_chunk USING {index_method} (vector {VECTOR_OPCLASS})"
)
if index_options:
index_sql = f"{index_sql} {index_options}"
self.session.execute(text(index_sql))
log.info(
"Ensured vector index '%s' using %s%s.",
index_name,
index_method,
f" {index_options}" if index_options else "",
)
def check_vector_length(self) -> None:
"""
Check if the VECTOR_LENGTH matches the existing vector column dimension in the database.
@@ -196,16 +260,19 @@ class PgvectorClient(VectorDBBase):
if "vector" in document_chunk_table.columns:
vector_column = document_chunk_table.columns["vector"]
vector_type = vector_column.type
if isinstance(vector_type, Vector):
db_vector_length = vector_type.dim
if db_vector_length != VECTOR_LENGTH:
raise Exception(
f"VECTOR_LENGTH {VECTOR_LENGTH} does not match existing vector column dimension {db_vector_length}. "
"Cannot change vector size after initialization without migrating the data."
)
else:
expected_type = HALFVEC if USE_HALFVEC else Vector
if not isinstance(vector_type, expected_type):
raise Exception(
"The 'vector' column exists but is not of type 'Vector'."
"The 'vector' column type does not match the expected type "
f"('{expected_type.__name__}') for VECTOR_LENGTH {VECTOR_LENGTH}."
)
db_vector_length = getattr(vector_type, "dim", None)
if db_vector_length is not None and db_vector_length != VECTOR_LENGTH:
raise Exception(
f"VECTOR_LENGTH {VECTOR_LENGTH} does not match existing vector column dimension {db_vector_length}. "
"Cannot change vector size after initialization without migrating the data."
)
else:
raise Exception(
@@ -232,8 +299,7 @@ class PgvectorClient(VectorDBBase):
# Ensure metadata is converted to its JSON text representation
json_metadata = json.dumps(item["metadata"])
self.session.execute(
text(
"""
text("""
INSERT INTO document_chunk
(id, vector, collection_name, text, vmetadata)
VALUES (
@@ -242,8 +308,7 @@ class PgvectorClient(VectorDBBase):
pgp_sym_encrypt(:metadata_text, :key)
)
ON CONFLICT (id) DO NOTHING
"""
),
"""),
{
"id": item["id"],
"vector": vector,
@@ -265,7 +330,7 @@ class PgvectorClient(VectorDBBase):
vector=vector,
collection_name=collection_name,
text=item["text"],
vmetadata=stringify_metadata(item["metadata"]),
vmetadata=process_metadata(item["metadata"]),
)
new_items.append(new_chunk)
self.session.bulk_save_objects(new_items)
@@ -285,8 +350,7 @@ class PgvectorClient(VectorDBBase):
vector = self.adjust_vector_length(item["vector"])
json_metadata = json.dumps(item["metadata"])
self.session.execute(
text(
"""
text("""
INSERT INTO document_chunk
(id, vector, collection_name, text, vmetadata)
VALUES (
@@ -299,8 +363,7 @@ class PgvectorClient(VectorDBBase):
collection_name = EXCLUDED.collection_name,
text = EXCLUDED.text,
vmetadata = EXCLUDED.vmetadata
"""
),
"""),
{
"id": item["id"],
"vector": vector,
@@ -323,7 +386,7 @@ class PgvectorClient(VectorDBBase):
if existing:
existing.vector = vector
existing.text = item["text"]
existing.vmetadata = stringify_metadata(item["metadata"])
existing.vmetadata = process_metadata(item["metadata"])
existing.collection_name = (
collection_name # Update collection_name if necessary
)
@@ -333,7 +396,7 @@ class PgvectorClient(VectorDBBase):
vector=vector,
collection_name=collection_name,
text=item["text"],
vmetadata=stringify_metadata(item["metadata"]),
vmetadata=process_metadata(item["metadata"]),
)
self.session.add(new_chunk)
self.session.commit()
@@ -349,7 +412,8 @@ class PgvectorClient(VectorDBBase):
self,
collection_name: str,
vectors: List[List[float]],
limit: Optional[int] = None,
filter: Optional[Dict[str, Any]] = None,
limit: int = 10,
) -> Optional[SearchResult]:
try:
if not vectors:
@@ -360,11 +424,11 @@ class PgvectorClient(VectorDBBase):
num_queries = len(vectors)
def vector_expr(vector):
return cast(array(vector), Vector(VECTOR_LENGTH))
return cast(array(vector), VECTOR_TYPE_FACTORY(VECTOR_LENGTH))
# Create the values for query vectors
qid_col = column("qid", Integer)
q_vector_col = column("q_vector", Vector(VECTOR_LENGTH))
q_vector_col = column("q_vector", VECTOR_TYPE_FACTORY(VECTOR_LENGTH))
query_vectors = (
values(qid_col, q_vector_col)
.data(
@@ -397,9 +461,47 @@ class PgvectorClient(VectorDBBase):
)
# Build the lateral subquery for each query vector
where_clauses = [DocumentChunk.collection_name == collection_name]
# Apply metadata filter if provided
if filter:
for key, value in filter.items():
if isinstance(value, dict) and "$in" in value:
# Handle $in operator: {"field": {"$in": [values]}}
in_values = value["$in"]
if PGVECTOR_PGCRYPTO:
where_clauses.append(
pgcrypto_decrypt(
DocumentChunk.vmetadata,
PGVECTOR_PGCRYPTO_KEY,
JSONB,
)[key].astext.in_([str(v) for v in in_values])
)
else:
where_clauses.append(
DocumentChunk.vmetadata[key].astext.in_(
[str(v) for v in in_values]
)
)
else:
# Handle simple equality: {"field": "value"}
if PGVECTOR_PGCRYPTO:
where_clauses.append(
pgcrypto_decrypt(
DocumentChunk.vmetadata,
PGVECTOR_PGCRYPTO_KEY,
JSONB,
)[key].astext
== str(value)
)
else:
where_clauses.append(
DocumentChunk.vmetadata[key].astext == str(value)
)
subq = (
select(*result_fields)
.where(DocumentChunk.collection_name == collection_name)
.where(*where_clauses)
.order_by(
(DocumentChunk.vector.cosine_distance(query_vectors.c.q_vector))
)
@@ -31,15 +31,12 @@ from open_webui.config import (
PINECONE_METRIC,
PINECONE_CLOUD,
)
from open_webui.env import SRC_LOG_LEVELS
from open_webui.retrieval.vector.utils import stringify_metadata
from open_webui.retrieval.vector.utils import process_metadata
NO_LIMIT = 10000 # Reasonable limit to avoid overwhelming the system
BATCH_SIZE = 100 # Recommended batch size for Pinecone operations
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class PineconeClient(VectorDBBase):
@@ -185,7 +182,7 @@ class PineconeClient(VectorDBBase):
point = {
"id": item["id"],
"values": item["vector"],
"metadata": stringify_metadata(metadata),
"metadata": process_metadata(metadata),
}
points.append(point)
return points
@@ -393,7 +390,11 @@ class PineconeClient(VectorDBBase):
)
def search(
self, collection_name: str, vectors: List[List[Union[float, int]]], limit: int
self,
collection_name: str,
vectors: List[List[Union[float, int]]],
filter: Optional[dict] = None,
limit: int = 10,
) -> Optional[SearchResult]:
"""Search for similar vectors in a collection."""
if not vectors or not vectors[0]:
@@ -22,12 +22,10 @@ from open_webui.config import (
QDRANT_TIMEOUT,
QDRANT_HNSW_M,
)
from open_webui.env import SRC_LOG_LEVELS
NO_LIMIT = 999999999
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class QdrantClient(VectorDBBase):
@@ -147,7 +145,11 @@ class QdrantClient(VectorDBBase):
)
def search(
self, collection_name: str, vectors: list[list[float | int]], limit: int
self,
collection_name: str,
vectors: list[list[float | int]],
filter: Optional[dict] = None,
limit: int = 10,
) -> Optional[SearchResult]:
# Search for the nearest neighbor items based on the vectors and return 'limit' number of results.
if limit is None:
@@ -13,7 +13,6 @@ from open_webui.config import (
QDRANT_TIMEOUT,
QDRANT_HNSW_M,
)
from open_webui.env import SRC_LOG_LEVELS
from open_webui.retrieval.vector.main import (
GetResult,
SearchResult,
@@ -30,7 +29,6 @@ TENANT_ID_FIELD = "tenant_id"
DEFAULT_DIMENSION = 384
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def _tenant_filter(tenant_id: str) -> models.FieldCondition:
@@ -105,6 +103,13 @@ class QdrantClient(VectorDBBase):
Returns:
tuple: (collection_name, tenant_id)
WARNING: This mapping relies on current Open WebUI naming conventions for
collection names. If Open WebUI changes how it generates collection names
(e.g., "user-memory-" prefix, "file-" prefix, web search patterns, or hash
formats), this mapping will break and route data to incorrect collections.
POTENTIALLY CAUSING HUGE DATA CORRUPTION, DATA CONSISTENCY ISSUES AND INCORRECT
DATA MAPPING INSIDE THE DATABASE.
"""
# Check for user memory collections
tenant_id = collection_name
@@ -249,7 +254,11 @@ class QdrantClient(VectorDBBase):
)
def search(
self, collection_name: str, vectors: List[List[float | int]], limit: int
self,
collection_name: str,
vectors: List[List[float | int]],
filter: Optional[Dict] = None,
limit: int = 10,
) -> Optional[SearchResult]:
"""
Search for the nearest neighbor items based on the vectors with tenant isolation.
@@ -1,4 +1,4 @@
from open_webui.retrieval.vector.utils import stringify_metadata
from open_webui.retrieval.vector.utils import process_metadata
from open_webui.retrieval.vector.main import (
VectorDBBase,
VectorItem,
@@ -6,13 +6,11 @@ from open_webui.retrieval.vector.main import (
SearchResult,
)
from open_webui.config import S3_VECTOR_BUCKET_NAME, S3_VECTOR_REGION
from open_webui.env import SRC_LOG_LEVELS
from typing import List, Optional, Dict, Any, Union
import logging
import boto3
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class S3VectorClient(VectorDBBase):
@@ -117,15 +115,16 @@ class S3VectorClient(VectorDBBase):
def has_collection(self, collection_name: str) -> bool:
"""
Check if a vector index (collection) exists in the S3 vector bucket.
Check if a vector index exists using direct lookup.
This avoids pagination issues with list_indexes() and is significantly faster.
"""
try:
response = self.client.list_indexes(vectorBucketName=self.bucket_name)
indexes = response.get("indexes", [])
return any(idx.get("indexName") == collection_name for idx in indexes)
self.client.get_index(
vectorBucketName=self.bucket_name, indexName=collection_name
)
return True
except Exception as e:
log.error(f"Error listing indexes: {e}")
log.error(f"Error checking if index '{collection_name}' exists: {e}")
return False
def delete_collection(self, collection_name: str) -> None:
@@ -185,7 +184,7 @@ class S3VectorClient(VectorDBBase):
metadata["text"] = item["text"]
# Convert metadata to string format for consistency
metadata = stringify_metadata(metadata)
metadata = process_metadata(metadata)
# Filter metadata to comply with S3 Vector API limit of 10 keys
metadata = self._filter_metadata(metadata, item["id"])
@@ -256,7 +255,7 @@ class S3VectorClient(VectorDBBase):
metadata["text"] = item["text"]
# Convert metadata to string format for consistency
metadata = stringify_metadata(metadata)
metadata = process_metadata(metadata)
# Filter metadata to comply with S3 Vector API limit of 10 keys
metadata = self._filter_metadata(metadata, item["id"])
@@ -296,7 +295,11 @@ class S3VectorClient(VectorDBBase):
raise
def search(
self, collection_name: str, vectors: List[List[Union[float, int]]], limit: int
self,
collection_name: str,
vectors: List[List[Union[float, int]]],
filter: Optional[dict] = None,
limit: int = 10,
) -> Optional[SearchResult]:
"""
Search for similar vectors in a collection using multiple query vectors.

Some files were not shown because too many files have changed in this diff Show More