Compare commits

...
2214 Commits
Author SHA1 Message Date
Tim Jaeryang Baek 5fbfe2bdca Merge pull request #15879 from open-webui/dev
0.6.18
2025-07-19 23:26:01 +04:00
Timothy Jaeryang Baek 6e0328798d chore: format 2025-07-19 23:23:54 +04:00
Tim Jaeryang Baek 0119036a11 Merge pull request #15871 from Classic298/cors
Enh: dev.sh default port export
2025-07-19 23:20:52 +04:00
Tim Jaeryang Baek 7a023e0841 Merge pull request #15875 from leebeomhun/i18n-ko
i18n: update and fix ko-KR translation
2025-07-19 23:20:29 +04:00
Timothy Jaeryang Baek b397008ee0 doc: changelog 2025-07-19 23:20:14 +04:00
Timothy Jaeryang Baek b329c63c80 refac 2025-07-19 23:17:14 +04:00
Timothy Jaeryang Baek 95b11207a1 fix: group user load 2025-07-19 23:16:44 +04:00
BHLee 74d0e6b2c7 i18n korean Fix typo 2025-07-20 04:10:26 +09:00
BHLee 7ffc12567f i18n update and fix:ko-KR translation.json 2025-07-20 03:46:37 +09:00
Classic298 d8ed4fa0c2 Merge branch 'open-webui:main' into cors 2025-07-19 19:53:13 +02:00
Classic298 549294ee30 Update dev.sh 2025-07-19 19:43:08 +02:00
Tim Jaeryang Baek b249809d2d Merge pull request #15738 from open-webui/dev
0.6.17
2025-07-19 21:39:46 +04:00
Timothy Jaeryang Baek a01e5126a3 refac 2025-07-19 21:34:38 +04:00
Timothy Jaeryang Baek c100442102 refac: compress image 2025-07-19 21:27:37 +04:00
Timothy Jaeryang Baek 32c85c59b1 refac 2025-07-19 21:22:12 +04:00
Timothy Jaeryang Baek 699c8073ff refac 2025-07-19 21:20:28 +04:00
Tim Jaeryang Baek 6f0def4ee3 Merge pull request #15869 from Classic298/patch-7
enh: Update CHANGELOG.md
2025-07-19 21:15:59 +04:00
Classic298 55d0534977 Update CHANGELOG.md 2025-07-19 19:13:46 +02:00
Classic298 9c82070ed9 Update CHANGELOG.md 2025-07-19 19:13:03 +02:00
Classic298 6bfe16367b Update CHANGELOG.md 2025-07-19 19:11:40 +02:00
Classic298 fb916087be Update CHANGELOG.md 2025-07-19 19:07:39 +02:00
Classic298 418a58674f Update CHANGELOG.md 2025-07-19 18:27:18 +02:00
Timothy Jaeryang Baek 4628097df9 doc: changelog 2025-07-19 19:51:47 +04:00
Timothy Jaeryang Baek 1159f3a781 enh: add folder modal 2025-07-19 19:46:35 +04:00
Timothy Jaeryang Baek a638a187bb refac 2025-07-19 19:32:52 +04:00
Timothy Jaeryang Baek 44a23a5c99 chore 2025-07-19 19:20:14 +04:00
Timothy Jaeryang Baek 08ff9863d5 refac 2025-07-19 19:15:05 +04:00
Timothy Jaeryang Baek d9a3b02e3f refac 2025-07-19 19:03:01 +04:00
Timothy Jaeryang Baek 53d94d6f61 enh: link prompt variable doc 2025-07-19 19:00:49 +04:00
Timothy Jaeryang Baek 81f8187e57 chore: format 2025-07-19 18:57:59 +04:00
Timothy Jaeryang Baek 4364dac226 refac 2025-07-19 18:20:17 +04:00
Timothy Jaeryang Baek 6362845065 refac 2025-07-19 18:12:16 +04:00
Timothy Jaeryang Baek 2db9d6b5d2 enh: disable edit button during generation 2025-07-19 18:08:58 +04:00
Timothy Jaeryang Baek 71ee33bf82 enh: search chat preview 2025-07-19 18:06:59 +04:00
Timothy Jaeryang Baek c94ce71ca3 refac 2025-07-19 16:44:28 +04:00
Timothy Jaeryang Baek 3af2938efc enh: note selection edit support 2025-07-19 16:35:03 +04:00
Tim Jaeryang Baek e35f7748f6 Merge pull request #15861 from tcx4c70/feat/log/trace_info
feat(logger): Add trace_id and span_id to log
2025-07-19 14:52:48 +04:00
Timothy Jaeryang Baek 157daa6def enh: allow direct file upload to knowledge attachment 2025-07-19 14:50:36 +04:00
Adam Tao 739098ab60 feat(logger): Add trace_id and span_id to log
Signed-off-by: Adam Tao <tcx4c70@gmail.com>
2025-07-19 18:36:54 +08:00
Timothy Jaeryang Baek 37c2fb0aa8 enh: folders 2025-07-19 14:29:08 +04:00
Timothy Jaeryang Baek 3251f8b14d refac 2025-07-19 12:44:23 +04:00
Timothy Jaeryang Baek ea18ffdade refac/fix 2025-07-19 12:17:35 +04:00
Tim Jaeryang Baek 2d4675dc1c Merge pull request #15842 from expruc/fix/handle_remove_files_from_vdb
fix: deleting files from vector DB upon file deletion
2025-07-19 12:13:03 +04:00
expruc 30a079cba8 added handler for deleting files from vdb upon files deletion 2025-07-18 18:40:29 +03:00
Timothy Jaeryang Baek 6478a50c72 refac 2025-07-18 18:26:43 +04:00
Timothy Jaeryang Baek c414f0c3ce enh: follow up prompts behaviour 2025-07-18 17:49:24 +04:00
Timothy Jaeryang Baek 5e4e1cf662 refac 2025-07-18 16:57:49 +04:00
Timothy Jaeryang Baek 18adb28da0 enh: chatFadeStreamingText toggle 2025-07-18 16:45:56 +04:00
Timothy Jaeryang Baek 853cf82ed8 enh: text fade in effect 2025-07-18 16:38:43 +04:00
Timothy Jaeryang Baek dcb1c0a146 refac 2025-07-18 16:22:49 +04:00
Timothy Jaeryang Baek c1c589d609 refac/enh: rich text input 2025-07-18 15:58:06 +04:00
Tim Jaeryang Baek 032fa52190 Merge pull request #15833 from rgaricano/dev-audio_Mime_comment
FIX: Correction of mimetypes string
2025-07-18 15:20:22 +04:00
Timothy Jaeryang Baek 671f577264 feat/enh: forward chat id in header 2025-07-18 15:03:46 +04:00
_00_ f08f6ac171 FIX: Correction of mimetypes string
Removed " , * for all) " from mimetype help string.

If mimetypes is set as * any file is tried to transcribe.
2025-07-18 12:54:08 +02:00
Tim Jaeryang Baek a395abd5d7 Merge pull request #15794 from expruc/feat/otel_user_metrics
feat: open telemetry user metrics
2025-07-18 14:22:21 +04:00
Tim Jaeryang Baek 25c3e7f851 Merge pull request #15810 from rgaricano/dev-Fix_AddPostHog_lib
FIX: Add posthog lib forcing version to avoid opentelemetry issue
2025-07-18 14:21:58 +04:00
Tim Jaeryang Baek 6bb5248812 Merge pull request #15789 from azurewtl/hotfix-web-config-clean-reranker
- fix: keep reranker_model config been removed by web search config
2025-07-18 13:00:40 +04:00
Tim Jaeryang Baek 3500d26931 Merge pull request #15825 from EventHorizon-AI/fix/ollama-tool-call
fix: ollama tool call
2025-07-18 12:58:07 +04:00
Tim Jaeryang Baek 0d3f066afc Merge pull request #15824 from zetavg/update-zh-tw-translation-0718
i18n: Update & fix zh-TW translation
2025-07-18 12:56:49 +04:00
Timothy Jaeryang Baek bcee131971 refac/fix: note chat permissions issue 2025-07-18 12:55:08 +04:00
Timothy Jaeryang Baek 3e67ce6e8e refac/fix: shortcut modal 2025-07-18 12:47:22 +04:00
Timothy Jaeryang Baek 43e11aa807 refac 2025-07-18 12:41:21 +04:00
Timothy Jaeryang Baek 2fa8aff82f refac/fix: tag handling 2025-07-18 11:56:40 +04:00
Timothy Jaeryang Baek 6ccf783f00 refac: /users/all endpoint 2025-07-18 11:46:14 +04:00
Pokai Chang bb19b71e39 i18n: Update & fix zh-TW translation 2025-07-18 06:50:03 +08:00
EntropyYue 260137fc12 fix: ollama tool call 2025-07-18 06:11:53 +08:00
Tim Jaeryang Baek 67bc431247 Merge pull request #15798 from MonsieurBibo/i18n-fr
feat(i18n): Add missing translations to fr-FR locale
2025-07-18 02:11:01 +04:00
Timothy Jaeryang Baek 86e46ebe6a chore 2025-07-17 17:53:36 +04:00
Timothy Jaeryang Baek d4ece7384c enh/refac: note image upload 2025-07-17 17:36:06 +04:00
Timothy Jaeryang Baek 9d633b062b refac/fix 2025-07-17 16:25:26 +04:00
Timothy Jaeryang Baek da91aba10f chore: tiptap bump 2025-07-17 14:43:42 +04:00
_00_ ca1a61ce60 Fix: Add posthog lib to requirement for avoid opentelemetry issue
Add "posthog==5.4.0" lib to requirements to avoid chromadb issue for lib versions incompatibility with that lib (chromadb  requiere posthog>=2.4.0,<6.0.0 ) which produce error in opentelemetry.
2025-07-17 10:15:18 +02:00
_00_ 5a0d0c6930 FIX: Add posthog lib to avoid opentelemetry issue
Add "posthog==5.4.0" lib to requirements to avoid chromadb issue for lib versions incompatibility with that lib (chromadb  requiere posthog>=2.4.0,<6.0.0 ) which produce error in opentelemetry.
2025-07-17 10:12:37 +02:00
Timothy Jaeryang Baek fce27202d9 refac 2025-07-17 02:37:18 +04:00
Timothy Jaeryang Baek 9634df4347 refac/enh: group add/remove users endpoints 2025-07-17 01:50:37 +04:00
MonsieurBibo 2e59d47e7d feat(i18n): Add missing translations to fr-FR locale
Completes various previously untranslated UI strings in the French translation file.
2025-07-16 22:25:14 +02:00
expruc cbbc4cfd26 added active and total user metrics 2025-07-16 22:06:36 +03:00
Timothy Jaeryang Baek 51242f4484 refac/fix 2025-07-16 22:22:59 +04:00
Azure Wang 9aff166f83 - fix: keep reranker_model config been removed by web search config 2025-07-16 23:51:23 +08:00
Timothy Jaeryang Baek 4f5d949af6 enh: OAUTH_TOKEN_ENDPOINT_AUTH_METHOD 2025-07-16 15:46:33 +04:00
Timothy Jaeryang Baek 19f1286cc7 fix: emoji call 2025-07-16 15:38:48 +04:00
Timothy Jaeryang Baek 82093cf690 refac/fix: disable file upload by drag with unsupported model 2025-07-16 15:25:56 +04:00
Timothy Jaeryang Baek 83c09f15ac refac/enh: reasoning tag handling 2025-07-16 15:20:03 +04:00
Timothy Jaeryang Baek 980b7c55a5 refac/fix: sources handling 2025-07-16 15:12:04 +04:00
Timothy Jaeryang Baek 8807ac8cac refac/enh: file upload with paste 2025-07-16 15:01:03 +04:00
Timothy Jaeryang Baek 20b6892d9b refac: chat exception handling 2025-07-16 14:54:56 +04:00
Tim Jaeryang Baek 5832c3ea97 Merge pull request #15718 from sihyeonn/feat/sh-support-failover
feat: add Redis Sentinel failover support for high availability
2025-07-16 14:39:42 +04:00
Tim Jaeryang Baek ff8c429687 Merge pull request #15749 from Classic298/patch-1
i18n: small german updates
2025-07-16 13:58:04 +04:00
Timothy Jaeryang Baek abe280f0a3 refac/fix: reranking function 2025-07-16 13:56:02 +04:00
Sihyeon Jang 9067eac4ca chore: add log for redis.exceptions
Signed-off-by: Sihyeon Jang <sihyeon.jang@navercorp.com>
2025-07-16 15:23:18 +09:00
Sihyeon Jang 65882c30cb refactor: change MAX_RETRY_COUNT as env
Signed-off-by: Sihyeon Jang <sihyeon.jang@navercorp.com>
2025-07-16 15:23:18 +09:00
Sihyeon Jang 1b7ac7c739 chore: format, lint
Signed-off-by: Sihyeon Jang <sihyeon.jang@navercorp.com>
2025-07-16 15:23:18 +09:00
Sihyeon Jang 5fe0c835b1 chore: update lock file for pytest-asyncio
Signed-off-by: Sihyeon Jang <sihyeon.jang@navercorp.com>
2025-07-16 15:23:18 +09:00
Sihyeon Jang 728c39eed7 chore: add tests with pytest-asyncio
Signed-off-by: Sihyeon Jang <sihyeon.jang@navercorp.com>
2025-07-16 15:23:18 +09:00
Sihyeon Jang 423d0923d9 feat: add Redis Sentinel failover support for high availability
- Implement SentinelRedisProxy class with automatic master discovery
- Add retry logic for handling connection failures and read-only errors
- Support both async and sync Redis operations with Sentinel
- Ensure backward compatibility with existing Redis configurations
- Provide seamless failover during master node outages

This enhancement significantly improves system reliability by eliminating
single points of failure in Redis deployments and ensuring continuous
service availability during infrastructure issues.

Signed-off-by: Sihyeon Jang <sihyeon.jang@navercorp.com>
2025-07-16 15:23:17 +09:00
Classic298 5b73f5065b i18n: small german updates 2025-07-15 21:34:49 +02:00
Tim Jaeryang Baek ae716bddee Merge pull request #15717 from sihyeonn/fix/sh-cleanup
fix: improve cleanup_response positioning for better resource management
2025-07-15 22:48:10 +04:00
Timothy Jaeryang Baek 500e6e64fe refac 2025-07-15 21:57:24 +04:00
Tim Jaeryang Baek 1f0cc2724a Merge pull request #15743 from byg1004/dev
fix: i18n korean
2025-07-15 21:43:27 +04:00
YG aea1a27f2f fix: i18n korean 2025-07-15 21:42:02 +04:00
Timothy J. Baek cecb3a42b0 refac 2025-07-15 21:24:05 +04:00
Timothy J. Baek fa91795b77 refac 2025-07-15 20:59:28 +04:00
Tim Jaeryang Baek 0aabbdd23a Merge pull request #15714 from macnow/patch-1
Update pl_PL locale
2025-07-15 20:50:02 +04:00
Tim Jaeryang Baek e03925ffc0 Merge pull request #15729 from rgaricano/dev-issues_15728
Fix: some FileItemModal.svelte strings to i18n
2025-07-15 20:49:34 +04:00
Tim Jaeryang Baek 2d46cc6d97 Merge pull request #15736 from ryanmrodriguez/ryanmrodriguez-patch-1
fix: DB Environment - Avoid Duplicate '@' Symbol in Credentials
2025-07-15 20:47:47 +04:00
Timothy J. Baek 14ef2e5dea refac: collaborative editor 2025-07-15 20:43:49 +04:00
Ryan Rodriguez 74392f4598 Avoid duplicate @ 2025-07-15 10:42:41 -05:00
Timothy Jaeryang Baek 6343f4293a refac 2025-07-15 18:45:59 +04:00
Timothy Jaeryang Baek e72da0df5a refac/fix: WEBSOCKET_REDIS_LOCK_TIMEOUT type 2025-07-15 18:10:36 +04:00
Timothy Jaeryang Baek f7fae947a7 fix/refac: hide system prompt for users w/o permission 2025-07-15 16:52:21 +04:00
_00_ 120b8f2a83 fix: update FileItemModal.svelte- locales strings
add strings "Using Entire Document" and "Using Focused Retrieval"  to i18n locales,

to address issue: #15728
2025-07-15 14:05:10 +02:00
_00_ 1092827895 Fix some FileItemModal.svelte- locales strings
convert strings "Using Entire Document" and "Using Focused Retrieval"  to i18n locales,

to address issue: https://github.com/open-webui/open-webui/issues/15728
2025-07-15 13:55:49 +02:00
Sihyeon Jang 058369adea fix: improve cleanup_response positioning for better resource management
Signed-off-by: Sihyeon Jang <sihyeon.jang@navercorp.com>
2025-07-15 10:14:30 +09:00
Sihyeon Jang 17f0bef2e2 refactor: use cleanup_response on openai
Signed-off-by: Sihyeon Jang <sihyeon.jang@navercorp.com>
2025-07-15 10:11:22 +09:00
Maciej Nowakowski 07bb5cc4aa Update pl_PL locale 2025-07-14 23:48:15 +02:00
Tim Jaeryang Baek 9063942055 Merge pull request #15712 from qingchunnh/Update_zh-CN-25715-2
i18n: Fix zh-CN
2025-07-14 23:58:39 +04:00
qingchun 8a4ea2b801 i18n: Fix zh-CN 2025-07-15 03:18:12 +08:00
Tim Jaeryang Baek 2470da8336 Merge pull request #15708 from open-webui/dev
fix/refac: icon styling
2025-07-14 22:02:36 +04:00
Timothy Jaeryang Baek 9b6dd59b86 fix/refac: icon styling 2025-07-14 22:02:14 +04:00
Tim Jaeryang Baek f966935d1d Merge pull request #15448 from open-webui/dev
0.6.16
2025-07-14 21:39:26 +04:00
Timothy Jaeryang Baek 045984e86f doc: changelog 2025-07-14 21:25:00 +04:00
Timothy Jaeryang Baek b1752cfb6a refac: styling 2025-07-14 21:24:31 +04:00
Tim Jaeryang Baek 21eff61940 Merge pull request #15707 from Classic298/patch-1
fix: the changelog
2025-07-14 21:21:47 +04:00
Classic298 4897cfa9c8 rem backticks and md tags 2025-07-14 19:21:08 +02:00
Tim Jaeryang Baek f79746b148 Merge pull request #15706 from qingchunnh/Update_zh-CN-25715
i18n: Update zh-CN
2025-07-14 21:20:26 +04:00
Classic298 ebd2abdd0a Update CHANGELOG.md 2025-07-14 19:17:16 +02:00
Classic298 69fd8436aa Update CHANGELOG.md 2025-07-14 18:52:07 +02:00
Classic298 7063d7d103 Update CHANGELOG.md 2025-07-14 18:42:53 +02:00
Classic298 541f7bc82c Update CHANGELOG.md 2025-07-14 18:32:57 +02:00
qingchun ed8438628e i18n: Update zh-CN 2025-07-15 00:14:28 +08:00
Timothy Jaeryang Baek 288fba874e refac: styling 2025-07-14 20:08:49 +04:00
Timothy Jaeryang Baek 8f5d23ec37 refac 2025-07-14 19:57:41 +04:00
Timothy Jaeryang Baek d5b8acfbe7 chore: dep bump pyproject 2025-07-14 19:53:06 +04:00
Timothy Jaeryang Baek 50ed194c48 doc: changelog 2025-07-14 19:51:12 +04:00
Timothy Jaeryang Baek 8b8ce2592d refac: styling 2025-07-14 19:34:40 +04:00
Timothy Jaeryang Baek 493e147be6 chore: format 2025-07-14 19:13:54 +04:00
Timothy Jaeryang Baek 51691adcbc refac 2025-07-14 19:12:17 +04:00
Timothy Jaeryang Baek 6a4542a321 refac: folder delete 2025-07-14 19:09:54 +04:00
Timothy Jaeryang Baek 92c9068369 refac 2025-07-14 17:50:03 +04:00
Timothy Jaeryang Baek f19d5b05a8 refac: styling 2025-07-14 17:47:21 +04:00
Timothy Jaeryang Baek 7f1f39058a enh/refac: distributed crdt 2025-07-14 17:14:56 +04:00
Timothy Jaeryang Baek fb71a4106f refac 2025-07-14 16:45:30 +04:00
Timothy Jaeryang Baek d76a3d5ce2 refac: ydoc 2025-07-14 16:29:42 +04:00
Timothy Jaeryang Baek 203c5b1956 refac 2025-07-14 14:37:28 +04:00
Timothy Jaeryang Baek f402957f05 refac 2025-07-14 14:35:13 +04:00
Timothy Jaeryang Baek 18bd83413b refac 2025-07-14 14:05:06 +04:00
Timothy Jaeryang Baek 0013f5c1fc refac/enh: forward user info header to reranker 2025-07-14 13:59:10 +04:00
Timothy Jaeryang Baek b4f04ff3a7 enh/refac: pgvector pool support 2025-07-14 12:18:44 +04:00
Timothy Jaeryang Baek 9c49d9e641 refac: aria label model item 2025-07-14 12:09:08 +04:00
Tim Jaeryang Baek c067cf996e Merge pull request #15687 from Classic298/patch-1
i18n: german translations
2025-07-14 10:12:19 +04:00
Tim Jaeryang Baek d87d6f99d3 Merge pull request #15692 from qingchunnh/Update_zh-CN-25714
i18n: Update & Improve & Fix zh-CN
2025-07-14 10:12:07 +04:00
Tim Jaeryang Baek a03bdba711 Merge pull request #15685 from rgaricano/dev-add_requirements_lib
UPD: requirements.txt adding httpx & optionals
2025-07-14 10:11:27 +04:00
qingchun 6649201fe5 i18n: Fix zh-CN 2025-07-14 02:07:09 +08:00
qingchun e89e0dea4a i18n: Improve zh-CN 2025-07-14 01:56:57 +08:00
qingchun 6ee0c0411e i18n: Update & Improve zh-CN 2025-07-14 01:45:22 +08:00
Timothy Jaeryang Baek 8e7731f139 refac: styling 2025-07-13 14:10:35 +04:00
Classic298 30fd6822f8 i18n: german translations 2025-07-13 12:08:58 +02:00
Timothy Jaeryang Baek a123629f74 refac 2025-07-13 14:08:19 +04:00
_00_ 3cde5fb3aa UPD: requirements.txt adding httpx
Update requirements.txt to add httpx and optional support for:

h2 - HTTP/2 support. (Optional, with httpx[http2])
socksio - SOCKS proxy support. (Optional, with httpx[socks])
rich - Rich terminal support. (Optional, with httpx[cli])
click - Command line client support. (Optional, with httpx[cli])
brotli or brotlicffi - Decoding for "brotli" compressed responses. (Optional, with httpx[brotli])
zstandard - Decoding for "zstd" compressed responses. (Optional, with httpx[zstd])
httpx[socks,http2,zstd,cli,brotli]==0.28.1
2025-07-13 11:01:25 +02:00
Tim Jaeryang Baek bd44ab64d0 refac: note editor model selection 2025-07-13 04:53:27 +04:00
Tim Jaeryang Baek d4dfece4f9 Merge pull request #15675 from rgaricano/dev_es-ES
UPD: Locale es-ES translation, rev.1 updated to v.0.6.16
2025-07-13 03:53:24 +04:00
Timothy Jaeryang Baek 98d38657cb refac: styling 2025-07-13 03:52:06 +04:00
Timothy Jaeryang Baek 6f55679a8a refac 2025-07-13 03:50:35 +04:00
Timothy Jaeryang Baek 217bd440c3 refac 2025-07-13 03:46:39 +04:00
Timothy Jaeryang Baek 2b9e30a107 refac 2025-07-13 03:43:30 +04:00
Timothy Jaeryang Baek 57156065e3 refac 2025-07-13 03:30:42 +04:00
Timothy Jaeryang Baek 751e32e427 refac 2025-07-13 03:26:52 +04:00
Timothy Jaeryang Baek ada78cb952 chore: format 2025-07-13 03:23:38 +04:00
Timothy Jaeryang BaekandClassic298 7607c53bd5 feat: folders as projects
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2025-07-13 03:23:05 +04:00
_00_ 027c08430a UPD: Locale es-ES translation, rev.1 updated to v.0.6.16
UPD: Locale es-ES translation, updated to v.0.6.16
rev.1

Description:

es-ES translation updated to v.0.6.16
2025-07-13 00:55:32 +02:00
Timothy Jaeryang BaekandClassic298 5abc03f4dd enh: enter into folder
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2025-07-13 02:40:48 +04:00
Timothy Jaeryang BaekandClassic298 6176dba3c9 refac
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2025-07-13 01:50:01 +04:00
Timothy Jaeryang BaekandClassic298 80f3c97668 refac: folder chat handling
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2025-07-13 01:26:56 +04:00
Timothy Jaeryang BaekandClassic298 5b14d15b00 feat: migrate folder table
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2025-07-13 00:51:58 +04:00
Timothy Jaeryang Baek 847a78a276 refac: styling 2025-07-13 00:37:24 +04:00
Timothy Jaeryang Baek d8ccb087ac refac: styling 2025-07-13 00:37:01 +04:00
Timothy Jaeryang Baek 3b4344c1bf fix: leaflet marker 2025-07-13 00:33:19 +04:00
Timothy Jaeryang Baek 87847ab31a chore: format 2025-07-13 00:15:16 +04:00
Timothy Jaeryang Baek 9ae22c5efe refac 2025-07-13 00:00:40 +04:00
Timothy Jaeryang Baek b5cbac7b28 refac 2025-07-12 23:50:41 +04:00
Timothy Jaeryang Baek 3be734fd84 enh: note editor prompts 2025-07-12 23:47:27 +04:00
Timothy Jaeryang Baek 6f896c4352 enh: note editor prompts 2025-07-12 23:34:40 +04:00
Timothy Jaeryang Baek 3e01286b61 refac 2025-07-12 23:31:04 +04:00
Timothy Jaeryang Baek b711c88bab refac: note browser title 2025-07-12 23:26:01 +04:00
Timothy Jaeryang Baek de343021c1 enh: note search 2025-07-12 23:08:46 +04:00
Timothy Jaeryang Baek f5613fb3c2 refac 2025-07-12 23:00:46 +04:00
Timothy Jaeryang Baek 9cabef67a7 refac 2025-07-12 22:55:45 +04:00
Timothy Jaeryang Baek 0df488e456 enh: generate note title 2025-07-12 22:43:09 +04:00
Timothy Jaeryang Baek bc7e5c7c04 refac 2025-07-12 22:18:52 +04:00
Timothy Jaeryang Baek 12edc3221e refac: styling 2025-07-12 22:14:41 +04:00
Timothy Jaeryang Baek 60fb643109 refac/fix: code block input 2025-07-12 21:54:05 +04:00
Timothy Jaeryang Baek fb78bff3c0 refac 2025-07-12 21:39:31 +04:00
Timothy Jaeryang Baek ce7cc37077 refac 2025-07-12 18:15:35 +04:00
Tim Jaeryang Baek 81d6e217f1 Merge pull request #15670 from rgaricano/dev_es-ES
UPD: Locale es-ES translation, updated to v.0.6.16
2025-07-12 17:18:57 +04:00
Tim Jaeryang Baek 33487283e8 Merge pull request #15669 from moblangeois/dev
i18n: Update fr-FR and fr-CA translation
2025-07-12 17:18:43 +04:00
Timothy Jaeryang Baek af59bee2ea refac 2025-07-12 17:17:31 +04:00
_00_ d3da236d96 UPD: Locale es-ES translation, updated to v.0.6.16
UPD: Locale es-ES translation, updated to v.0.6.16
2025-07-12 13:44:28 +02:00
Tim Jaeryang Baek fbaab9e446 refac 2025-07-12 15:36:30 +04:00
Tim Jaeryang Baek 4a69d857f3 refac 2025-07-12 15:32:26 +04:00
Tim Jaeryang Baek 290f4023b1 refac: add copy link functionality to notes view 2025-07-12 15:20:21 +04:00
Morgan Blangeois 4da2b050c6 Update translations files for fr-Fr and fr-CA 2025-07-12 10:12:11 +00:00
Timothy Jaeryang Baek d99698f7eb refac 2025-07-12 03:39:58 +04:00
Timothy Jaeryang Baek 12c4a7a3f7 refac: styling 2025-07-12 03:15:31 +04:00
Timothy Jaeryang Baek 955c2e2570 refac 2025-07-12 03:00:40 +04:00
Timothy Jaeryang Baek 627f680659 refac 2025-07-12 02:57:11 +04:00
Timothy Jaeryang Baek 01420fd189 refac 2025-07-12 02:51:51 +04:00
Timothy Jaeryang Baek 4351702587 refac 2025-07-12 02:38:52 +04:00
Timothy Jaeryang Baek b52a162597 refac 2025-07-12 02:20:32 +04:00
Timothy Jaeryang Baek baa1a44397 refac 2025-07-12 02:17:52 +04:00
Timothy Jaeryang Baek 3c80219808 refac 2025-07-12 01:47:41 +04:00
Timothy Jaeryang Baek bcfe1a087f refac 2025-07-12 00:38:25 +04:00
Timothy Jaeryang Baek d509ca1133 refac 2025-07-12 00:34:18 +04:00
Tim Jaeryang Baek 9b84a8e443 Merge pull request #15632 from athoik/quote
fix: don't over quote forwarded headers
2025-07-12 00:24:29 +04:00
Tim Jaeryang Baek 3402bfd96c Merge pull request #15655 from rgaricano/dev-audio_Mime_comment
DOC: Extend Audio MIME Type Setting Explanation
2025-07-12 00:23:32 +04:00
Timothy Jaeryang Baek 627b39cdbb refac 2025-07-12 00:19:24 +04:00
Timothy Jaeryang Baek 9b8d6ed6d2 refac: notes 2025-07-12 00:16:57 +04:00
Timothy Jaeryang Baek 2fbff741da feat: collaborative note 2025-07-11 23:59:48 +04:00
_00_ e3c8e1bb71 DOC: Extend Audio MIME Type Setting Explanation
DOC: Extend Audio MIME Type Setting Explanation

Add explanation to add multiple mime types in audio settings labeling
2025-07-11 20:18:54 +02:00
Timothy Jaeryang Baek d3b14ff827 refac 2025-07-11 18:41:09 +04:00
Timothy Jaeryang Baek 6820e41eb2 refac 2025-07-11 18:14:56 +04:00
Timothy Jaeryang Baek 22052d2c24 refac: task 2025-07-11 18:14:48 +04:00
Timothy Jaeryang Baek 0202926e35 refac 2025-07-11 18:09:24 +04:00
Timothy Jaeryang Baek 47cd0770e4 refac 2025-07-11 17:54:38 +04:00
Timothy Jaeryang Baek 5b50f3cd69 chore: dep 2025-07-11 17:54:27 +04:00
Timothy Jaeryang Baek 788e7d0487 refac 2025-07-11 17:53:53 +04:00
Timothy Jaeryang Baek 11914d20b4 refac: styling 2025-07-11 12:39:58 +04:00
Timothy Jaeryang Baek 77c1905609 refac 2025-07-11 12:35:42 +04:00
Timothy Jaeryang Baek 033d07ee23 refac: file handling 2025-07-11 12:29:17 +04:00
Timothy Jaeryang Baek 0db8bedf45 refac: tts input 2025-07-11 12:15:13 +04:00
Timothy Jaeryang Baek 3bc5485867 enh: proper undo/redo note editor support 2025-07-11 12:00:41 +04:00
Timothy Jaeryang Baek 3b9d86de0b refac 2025-07-11 12:00:21 +04:00
Timothy Jaeryang Baek 6a3b0e3110 refac 2025-07-11 01:46:06 +04:00
Timothy Jaeryang Baek 249b12e44f refac: knowledge selector 2025-07-11 01:41:40 +04:00
Timothy Jaeryang Baek 84bf1892bb refac 2025-07-11 01:34:24 +04:00
Athanasios Oikonomou 96758176cc fix: don't over quote forwarded headers
Fix introduced on #15035 is over quoting headers.

Eg mails instead of user@example.com shown as user%40example.com
Eg names instead of First Last shown as First%20Last

Also we are spending some time quoting ids and roles without required.

Keep quote only on user name, initially had problem based on the discussion
https://github.com/open-webui/open-webui/discussions/14391

Also add space in safe characters, in order remove %20 from names.
2025-07-10 22:08:28 +03:00
Timothy Jaeryang Baek 1f641ce1fb refac 2025-07-10 14:06:15 +04:00
Timothy Jaeryang Baek 22af53f60c refac 2025-07-10 11:14:53 +04:00
Tim Jaeryang Baek d1b0a5739d Merge pull request #15619 from amberyufangchiu/amberyufangchiu-dev
i18n: Update zh-TW (Traditional Chinese) translation.json
2025-07-10 10:33:30 +04:00
Tim Jaeryang Baek 18c19743c4 Merge branch 'dev' into amberyufangchiu-dev 2025-07-10 10:33:12 +04:00
Amber Chiu 77acb9bb41 i18n: Update zh-TW (Traditional Chinese) translation.json 2025-07-10 09:40:58 +08:00
Timothy Jaeryang Baek 24f0da172e fix: table align markdown rendering 2025-07-09 23:39:54 +04:00
Timothy Jaeryang Baek 45d58e73a9 refac: styling 2025-07-09 23:22:59 +04:00
Timothy Jaeryang Baek 8d84b4c2a4 enh/refac: temp chat file upload behaviour
client-side content extraction
2025-07-09 22:59:37 +04:00
Timothy Jaeryang Baek 8fb54b133e refac 2025-07-09 22:33:36 +04:00
Timothy Jaeryang Baek 6d31fc7875 refac: styling 2025-07-09 13:50:45 +04:00
Timothy Jaeryang Baek 49e57b6d13 refac 2025-07-09 13:47:10 +04:00
Timothy Jaeryang Baek 3b9f4a6f5e enh: note word/char counter 2025-07-09 13:38:54 +04:00
Timothy Jaeryang Baek 6ee4274b3b enh: rich text input link 2025-07-09 13:29:27 +04:00
Timothy Jaeryang Baek 04962922d7 refac 2025-07-09 13:00:28 +04:00
Tim Jaeryang Baek 9d8378d2e2 Merge pull request #15604 from EventHorizon-AI/fix/tool-results
fix: tool results
2025-07-09 12:45:10 +04:00
Tim Jaeryang Baek 67c88f90dc Merge pull request #15601 from headwAI-GmbH/streamline-resource-loading-url
chore: streamline the URL used for resources like favicon.png
2025-07-09 12:43:58 +04:00
Timothy Jaeryang Baek b615d13e46 refac: styling 2025-07-09 11:44:51 +04:00
guenhter 196af9eaf7 chore: streamline the URL used for resources like favicon.png 2025-07-09 09:14:51 +02:00
Timothy Jaeryang Baek 0a3c448a52 refac: styling 2025-07-09 11:06:40 +04:00
EntropyYue 5b8007fcb5 fix: tool result frontend 2025-07-09 15:01:19 +08:00
EntropyYue dc0d420af5 fix: tool result 2025-07-09 14:40:53 +08:00
Tim Jaeryang Baek 8882df5829 Merge pull request #15596 from qingchunnh/Update_zh-CN-2579
i18n: Update & Improve zh-CN
2025-07-09 10:26:50 +04:00
qingchun 9b5c9ffb30 i18n: Improve zh-CN 2025-07-09 10:03:09 +08:00
qingchun b09973cafe i18n: Update & Improve zh-CN 2025-07-09 09:44:55 +08:00
Timothy Jaeryang Baek 40b564dedd refac: styling 2025-07-09 04:53:31 +04:00
Timothy Jaeryang Baek a1ddbff744 refac: styling 2025-07-09 04:36:21 +04:00
Timothy Jaeryang Baek 04ac14463b refac: styling 2025-07-09 04:33:52 +04:00
Timothy Jaeryang Baek 784c5b5471 enh: remember note editor preference 2025-07-09 03:50:20 +04:00
Timothy Jaeryang Baek b93ee37f9a refac 2025-07-09 03:43:41 +04:00
Timothy Jaeryang Baek bdab9dd596 refac: task list handling 2025-07-09 03:39:57 +04:00
Timothy Jaeryang Baek e37e6e6b03 enh/refac: allow setting full context mode from model editor 2025-07-09 03:22:24 +04:00
Timothy Jaeryang Baek dd87111c80 refac 2025-07-09 03:15:20 +04:00
Timothy Jaeryang Baek c2ac797650 feat: formatting buttons 2025-07-09 03:11:51 +04:00
Timothy Jaeryang Baek 9a3a0070d6 refac: styling 2025-07-09 02:04:09 +04:00
Timothy Jaeryang Baek e2f5d7caec refac: styling 2025-07-09 01:53:51 +04:00
Timothy Jaeryang Baek 9632f40335 enh: support commands in note chat 2025-07-09 01:49:43 +04:00
Timothy Jaeryang Baek e9821c0881 refac 2025-07-09 01:31:28 +04:00
Timothy Jaeryang Baek b3c4bc6041 enh: allow full context mode for collections 2025-07-09 01:29:49 +04:00
Timothy Jaeryang Baek d5f9bbc7a7 enh: reference note in chat 2025-07-09 01:17:25 +04:00
Timothy Jaeryang Baek f2ee99d760 refac: styling 2025-07-09 01:15:32 +04:00
Tim Jaeryang Baek 91628f5392 Merge pull request #15593 from tarmst/add-openai-to-pyodide
Feat: Add OpenAI package to Pyodide code blocks
2025-07-09 00:54:06 +04:00
Timothy Jaeryang Baek 1de03c364f refac: checkbox styling 2025-07-09 00:34:03 +04:00
Timothy Jaeryang Baek 6bf218f720 refac: styling 2025-07-09 00:22:25 +04:00
Timothy Jaeryang Baek 40a715be89 refac: styling 2025-07-09 00:05:59 +04:00
Timothy Jaeryang Baek 248fd10406 enh: task list rich text input 2025-07-08 23:57:34 +04:00
tarmst 0033b3fe4e Add OpenAI package to pyodide code blocks 2025-07-08 19:28:43 +00:00
Timothy Jaeryang Baek f69db54b7e enh: model workspace tag filter view 2025-07-08 23:05:39 +04:00
Timothy Jaeryang Baek 0765f6c230 refac 2025-07-08 22:56:46 +04:00
Timothy Jaeryang Baek 42f2de68bd refac: styling 2025-07-08 22:38:31 +04:00
Timothy Jaeryang Baek 6f98379c82 enh: checkbox input variable 2025-07-08 21:33:51 +04:00
Timothy Jaeryang Baek 2cde746e38 refac: styling 2025-07-08 16:46:14 +04:00
Timothy Jaeryang Baek 74cafce4d2 refac: note revision history 2025-07-08 16:43:26 +04:00
Timothy Jaeryang Baek d3ba1a0592 refac: prompt variable handling 2025-07-08 16:30:06 +04:00
Tim Jaeryang Baek e3b8f700e4 Merge pull request #14264 from diwakar-s-maurya/patch-6
feat: add langchain markdown document splitter
2025-07-08 15:55:20 +04:00
Timothy Jaeryang Baek ed72ad1728 refac: styling 2025-07-08 14:51:50 +04:00
Timothy Jaeryang Baek 27aa628c86 chore: i18n 2025-07-08 13:29:04 +04:00
Tim Jaeryang Baek a748f19ac2 Merge pull request #15548 from expruc/fix/docling_ignore_html
fix: text/html files being detected as text when loaded with docling/tika
2025-07-08 13:16:01 +04:00
Timothy Jaeryang Baek 5d5bb9d1f2 refac 2025-07-08 13:14:22 +04:00
Timothy Jaeryang Baek 8f74070688 refac: styling 2025-07-08 12:54:11 +04:00
Timothy Jaeryang Baek 09b5c1b4dc refac 2025-07-08 12:50:06 +04:00
Timothy Jaeryang Baek 8febc54a88 refac: styling 2025-07-08 12:39:04 +04:00
Timothy Jaeryang Baek 7cfcf9b21d refac: styling 2025-07-08 12:35:35 +04:00
Timothy Jaeryang Baek 9a064d9623 feat: edit notes via chat 2025-07-08 12:31:31 +04:00
Timothy Jaeryang Baek 45085d04eb refac 2025-07-08 12:31:22 +04:00
Timothy Jaeryang Baek c107a0cbf4 refac 2025-07-08 10:09:32 +04:00
Timothy Jaeryang Baekandpython4004 793648bbd6 fix/security: note access control
Co-Authored-By: python4004 <36403473+python4004@users.noreply.github.com>
2025-07-08 10:04:13 +04:00
Timothy Jaeryang Baek c42a6d8029 refac/fix: node dom issue 2025-07-08 10:00:20 +04:00
Tim Jaeryang Baek a56e9e586b Merge pull request #15573 from flefevre/main
i18n french
2025-07-08 09:58:34 +04:00
Timothy Jaeryang BaekandG30 0450c7cf48 refac: banners
Co-Authored-By: G30 <50341825+silentoplayz@users.noreply.github.com>
2025-07-08 01:37:10 +04:00
Francois LE FEVRE c17b2f3ca0 i18n french 2025-07-07 22:15:39 +02:00
Timothy Jaeryang Baek 30521bcac7 refac: styling 2025-07-08 00:07:50 +04:00
Timothy Jaeryang Baek 3a9d5b0494 refac 2025-07-07 23:51:01 +04:00
Timothy Jaeryang Baek 78dce85b03 fix: thread image only message 2025-07-07 22:00:38 +04:00
Timothy Jaeryang Baek 2d0e55e8f6 refac 2025-07-07 21:52:46 +04:00
Timothy Jaeryang Baek 5722da8e3b feat/enh: insert prompt as rich text 2025-07-07 21:43:28 +04:00
Timothy Jaeryang Baek ade4b0d691 feat: insert chat message to notes 2025-07-07 21:22:07 +04:00
Timothy Jaeryang Baek a3c2018a4a feat: chat with notes 2025-07-07 20:19:17 +04:00
Timothy Jaeryang Baek b3001f95f6 refac 2025-07-07 19:58:10 +04:00
Timothy Jaeryang Baek 8cd7de3740 refac: styling 2025-07-07 19:56:31 +04:00
Timothy Jaeryang Baek d04d0c0510 refac 2025-07-07 19:47:32 +04:00
Timothy Jaeryang Baek 821243ef40 refac 2025-07-07 19:42:08 +04:00
Timothy Jaeryang Baek 67063e6400 refac 2025-07-07 19:33:54 +04:00
Timothy Jaeryang Baek a5367145bf refac 2025-07-07 19:32:41 +04:00
Timothy Jaeryang Baek 7003395a5f refac: experimental wording 2025-07-07 19:30:05 +04:00
Timothy Jaeryang Baek 9bd001a14b feat: note chat 2025-07-07 19:26:12 +04:00
Timothy Jaeryang Baek a31a1f3c0d refac: note editor 2025-07-07 18:26:52 +04:00
Timothy Jaeryang Baek 3392e2ef15 refac/fix: channel input 2025-07-07 17:49:03 +04:00
Timothy Jaeryang Baek 57f9eadd75 feat: models?id= support for admin settings 2025-07-07 17:16:51 +04:00
Timothy Jaeryang Baek a519c62250 chore: bump 2025-07-07 16:13:18 +04:00
Timothy Jaeryang Baek 3e15c8ab69 refac 2025-07-07 15:56:05 +04:00
Timothy Jaeryang Baek be6633e3be chore: bump ddgs 2025-07-07 15:55:26 +04:00
Timothy Jaeryang Baek cfcfefb20c refac: tools handling 2025-07-07 11:42:52 +04:00
Timothy Jaeryang Baek 05a9b72670 refac/fix: base model cache 2025-07-07 11:30:27 +04:00
Timothy Jaeryang Baek 31c63d8898 refac 2025-07-06 21:40:53 +04:00
expruc 453a2bd9b5 fixed issue where text/html files being detected as text when loaded 2025-07-06 20:10:26 +03:00
Timothy Jaeryang Baek 4a825dce7f refac 2025-07-06 17:51:03 +04:00
Tim Jaeryang Baek 7cb105abd4 Merge pull request #15119 from denispol/dev
fix: Preserve line breaks on multi-line paste in mobile WebViews
2025-07-06 17:41:09 +04:00
Timothy Jaeryang Baek e84c521177 refac/fix: rich text input in ff 2025-07-06 17:32:03 +04:00
Timothy Jaeryang Baek be7166d6fc refac 2025-07-06 16:16:05 +04:00
Timothy Jaeryang Baek 1c41e95ba6 fix/refac: ollama api backward compatibility 2025-07-06 15:02:10 +04:00
Timothy Jaeryang Baek 9a476ae41d refac: ollama api backward compatibility 2025-07-06 14:40:21 +04:00
Timothy Jaeryang Baek 2eab43c35e fix 2025-07-06 14:38:28 +04:00
Timothy Jaeryang Baek f92890b741 refac 2025-07-06 14:32:39 +04:00
Timothy Jaeryang Baek fc2fc6516e enh: autofocus to first input variable input 2025-07-06 14:31:32 +04:00
Timothy Jaeryang Baek 0a1f9966ef refac: audio error handling 2025-07-06 14:20:38 +04:00
Timothy Jaeryang Baek 44754e4c4a enh/refac: show input modal by default for prompt variables 2025-07-06 14:16:34 +04:00
Timothy Jaeryang Baek 7cc2afe973 refac: rich text input 2025-07-06 14:15:45 +04:00
Tim Jaeryang Baek a4885ac549 Merge pull request #15527 from gmacario/gmacario-fix-api-show
FIX: Ollama.py API endpoints to match Ollama's
2025-07-05 21:23:32 +04:00
Tim Jaeryang Baek 26ce14d163 Merge pull request #15541 from open-webui/main
refac: readme
2025-07-05 21:06:51 +04:00
Timothy Jaeryang Baek 5eca495d3e refac: readme 2025-07-05 21:06:32 +04:00
Timothy Jaeryang Baek aeef5d4dcd refac: mobile navbar 2025-07-05 02:26:52 +04:00
Timothy Jaeryang BaekandClassic298 9f87a0cf21 feat: input variables
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2025-07-05 02:16:44 +04:00
Timothy Jaeryang Baek 5526b66165 chore: qdrant-client bump 2025-07-04 23:56:02 +04:00
Tim Jaeryang Baek a3add18fa9 Merge pull request #15289 from Anush008/main
refactor: Updated Qdrant multi-tenancy implementation
2025-07-04 23:49:11 +04:00
Timothy Jaeryang Baek 198b89faa5 refac: suggestion prompts 2025-07-04 23:40:31 +04:00
Timothy Jaeryang Baek db0d2ae6d4 refac: Improve banner handling and command visibility in chat UI 2025-07-04 20:55:31 +04:00
Timothy Jaeryang Baek c9e1f454fc refac 2025-07-04 20:30:34 +04:00
Tim Jaeryang Baek f58cb09d27 Merge pull request #15480 from itk-dev/feature/message-input-visible-outlines
Feat: message input visible outlines
2025-07-04 20:27:57 +04:00
Timothy Jaeryang Baek 2e2a63c201 fix: various rich text input issues
#15140
2025-07-04 20:26:01 +04:00
Gianpaolo Macario bdd5120407 fix: rename Ollama API parameter "name" to "model" 2025-07-04 17:33:57 +02:00
Timothy Jaeryang Baek 9b5da77ffc refac 2025-07-04 15:29:35 +04:00
Sine Jespersen 86044feaca button to div: avoid nested button, code intepreter button outline 2025-07-04 11:18:30 +02:00
Anush008 17debaa6de chore: Raise if QDRANT_URI is not set
Signed-off-by: Anush008 <anushshetty90@gmail.com>
2025-07-04 13:17:46 +05:30
Anush008 c8a49d373a refactor: Removed more swallows
Signed-off-by: Anush008 <anushshetty90@gmail.com>
2025-07-04 12:38:22 +05:30
Anush008 0ac57a088f refactor: More implementation improvements
Signed-off-by: Anush008 <anushshetty90@gmail.com>
2025-07-04 12:33:54 +05:30
Anush008 7c734d3fea Merge remote-tracking branch 'origin/dev' into Anush008/main
Signed-off-by: Anush008 <anushshetty90@gmail.com>
2025-07-04 12:22:08 +05:30
Tim Jaeryang Baek 600344f2e8 Merge pull request #15510 from kopero2000/bug/oauth_logout_fix
fix/oauth logout fix
2025-07-04 10:30:02 +04:00
Timothy Jaeryang Baek 1b237c7f60 enh: submit searchParam 2025-07-04 02:53:41 +04:00
Timothy Jaeryang Baek e28bc37e41 enh: AUDIT_UVICORN_LOGGER_NAMES 2025-07-03 21:54:25 +04:00
Timothy Jaeryang Baek 75096c24ac refac 2025-07-03 21:28:09 +04:00
Timothy Jaeryang Baek d2485bbf09 refac 2025-07-03 18:13:23 +04:00
8661c4416a feat: configurable db url
Co-Authored-By: Ryan Rodriguez <62253265+ryanmrodriguez@users.noreply.github.com>
Co-Authored-By: Mario Trangoni <mario@mariotrangoni.de>
2025-07-03 17:47:12 +04:00
Tim Jaeryang Baek e98dc0a745 Merge pull request #14957 from Classic298/fix-postgresql-null-value-search
Fix: postgresql null value search
2025-07-03 17:36:27 +04:00
Tim Jaeryang Baek ed6bbddf6e Merge pull request #15483 from headwAI-GmbH/support-deactivate-update-check
feat: Support deactivate update check without OFFLINE_MODE
2025-07-03 17:22:22 +04:00
Tim Jaeryang Baek bdbc8f048c Merge pull request #15501 from Green-PT/fix-oauth-signout-redirect
fix: sign out redirect for oauth
2025-07-03 17:18:25 +04:00
Tim Jaeryang Baek 74167f4b48 Merge pull request #15490 from sihyeonn/chore/sh-print
refactor: improve logging in task management system
2025-07-03 17:14:35 +04:00
Tim Jaeryang Baek 195e0221b8 Merge pull request #15493 from aleixdorca/dev
i18n: Update Catalan translation.json
2025-07-03 17:12:47 +04:00
Tim Jaeryang Baek 7b0fc0ad1c Merge pull request #15492 from qingchunnh/Update_zh-CN-2573
i18n: Update zh-CN
2025-07-03 16:13:17 +04:00
Tim Jaeryang Baek c093b88abe Merge pull request #15500 from aindriu80/update-ga-strings-june
i18n (ga): Update Irish translations
2025-07-03 16:12:59 +04:00
Aindriú Mac Giolla Eoin 34ec073b3d Merge branch 'dev' into update-ga-strings-june 2025-07-03 13:07:38 +01:00
Tim etc. bb0176085d fix signout redirect for oauth 2025-07-03 13:59:48 +02:00
Aindriú Mac Giolla Eoin 54e5e4bb4b i18n(ga): Update Irish translations 2025-07-03 12:40:58 +01:00
Aleix Dorca 4c7436a05c Update Catalan translation.json 2025-07-03 06:44:41 +02:00
qingchun ee00bdef63 i18n: Fix zh-CN 2025-07-03 11:52:45 +08:00
qingchun e5415b8a87 i18n: Fix zh-CN 2025-07-03 11:50:58 +08:00
qingchun 1f4e51edc6 i18n: Update zh-CN 2025-07-03 11:46:17 +08:00
Sihyeon Jang a9804e2566 refactor: replace print statements with appropriate logging levels
Signed-off-by: Sihyeon Jang <uneedsihyeon@gmail.com>
2025-07-03 08:34:25 +09:00
Timothy Jaeryang Baek e1efd54a70 fix/refac: heic image handling 2025-07-03 00:04:13 +04:00
Vizi Béla Ákos d1ee4e0925 Merge branch 'open-webui:main' into bug/oauth_logout_fix 2025-07-02 18:01:08 +02:00
Bela Vizi 9623ef4360 add trust env to clientsession 2025-07-02 17:59:56 +02:00
Timothy Jaeryang Baek bb506edcc3 refac 2025-07-02 17:50:07 +04:00
Timothy Jaeryang Baek f55e93cabe feat: additional valve input types 2025-07-02 17:45:42 +04:00
guenhter 661ea773f4 fix: avoid 'checking for updates...' always present in offline-mode 2025-07-02 15:20:43 +02:00
guenhter 34ddd62951 feat: support suppressing version checks 2025-07-02 15:20:43 +02:00
Timothy Jaeryang Baekandsilentoplayz 7adb15d4b0 refac: user pending screen
Co-Authored-By: silentoplayz <50341825+silentoplayz@users.noreply.github.com>
2025-07-02 16:11:13 +04:00
Timothy Jaeryang Baek 21f9982c5a chore: chart.js 2025-07-02 15:58:16 +04:00
Timothy Jaeryang Baek ff3dec9ef2 refac: styling 2025-07-02 15:14:48 +04:00
Timothy Jaeryang Baek 05773bc831 refac: styling 2025-07-02 14:58:28 +04:00
Tim Jaeryang Baek 6ff84d8ec0 Merge pull request #15479 from itk-dev/feature/suggestions-role-list
FEAT: give suggestions role list and items role listitem
2025-07-02 14:50:30 +04:00
Sine Jespersen 98317f4576 give suggestions role list and items role listitem 2025-07-02 12:45:15 +02:00
Timothy Jaeryang Baek 139e4f93cb refac 2025-07-02 14:30:05 +04:00
Tim Jaeryang Baek a3c9b46e9a Merge pull request #15477 from itk-dev/feature/accessibility-user-settings
Feat: accessibility user settings dropdown menu
2025-07-02 14:22:37 +04:00
Timothy Jaeryang Baekandconql 2c6227e4b6 fix: preserve dates for chat imports
Co-Authored-By: conql <49243542+conql@users.noreply.github.com>
2025-07-02 14:21:36 +04:00
Timothy Jaeryang Baek 3f800c069e refac: banner newlines 2025-07-02 14:06:06 +04:00
Timothy Jaeryang Baek 9b718e7817 refac: styling 2025-07-02 13:59:14 +04:00
Timothy Jaeryang Baek 7e273ebe86 refac: banners 2025-07-02 13:48:18 +04:00
Sine Jespersen 225d20fb84 aria hidden on decorative svgs to enhance accessibility 2025-07-02 11:29:03 +02:00
Sine Jespersen b7255854d2 change interactive elements in dropdownmenu to dropdownitems 2025-07-02 11:14:12 +02:00
Tim Jaeryang Baek bab7a65c2c Merge pull request #15452 from expruc/chore/remove_embedder_openai_api_key_requirement
chore: removed requirement for providing api key in openai embedder
2025-07-02 12:55:00 +04:00
Timothy Jaeryang Baek 1cc15e6470 refac: splash screen 2025-07-02 12:51:03 +04:00
Sine Jespersen 7e49c8733b remove nested button and add label for assistive technology 2025-07-02 10:12:48 +02:00
Tim Jaeryang Baek 40fc1a1f95 Merge pull request #15466 from itk-dev/feature/accessibility-dropdown-model-selector
Feat: accessibility in dropdown model selector
2025-07-02 12:10:29 +04:00
Sine Jespersen d7f247d7d2 make margin depend on high contrast mode, based on comment on pr 2025-07-02 09:55:47 +02:00
Timothy Jaeryang Baek 57972605f2 refac 2025-07-02 11:48:01 +04:00
Timothy Jaeryang Baek d54896ca66 chore: bump pydantic 2025-07-02 11:41:58 +04:00
Sine Jespersen 6a644cd03a aria hidden on svg, as this is decorative 2025-07-02 08:57:15 +02:00
Sine Jespersen 64b583dc29 add margin to make outline visible 2025-07-02 08:56:56 +02:00
Sine Jespersen bed2c7e34f remove nested buttons, as this is not valid html 2025-07-02 08:56:17 +02:00
Sine Jespersen e5435c6480 high contrast mode on model selector to enhance accessibility 2025-07-02 08:46:32 +02:00
expruc 555c449f49 removed requirement for providing api key in openai embedder 2025-07-01 22:03:50 +03:00
Tim Jaeryang Baek a85ecadfeb Merge pull request #15447 from itk-dev/feature/accessibility-file-item
Feat: accessibility of file input in messageinput
2025-07-01 15:57:39 +04:00
Timothy Jaeryang Baek 355a3eaedb enh: dynamic commands height 2025-07-01 15:57:01 +04:00
Timothy Jaeryang Baek 8d49288499 refac: skeleton 2025-07-01 15:32:32 +04:00
Tim Jaeryang Baek 6d0bd226c1 Merge pull request #15445 from itk-dev/feature/chat-page-accessibility
Feat: chat page accessibility
2025-07-01 15:23:06 +04:00
Sine Jespersen 3d4752480e aria label on remove file button to enchance accessibility 2025-07-01 13:21:40 +02:00
Tim Jaeryang Baek af9d264405 Merge pull request #15402 from itk-dev/feature/aria-hide-button-and-img
Feat: aria hide button and img
2025-07-01 15:18:21 +04:00
Sine Jespersen 1dd475cccf aria hidden on svg 2025-07-01 13:16:18 +02:00
Sine Jespersen 09ddf69a73 high contrast mode on file item to enhance accessibility 2025-07-01 13:15:38 +02:00
Sine Jespersen 67ac39b72b aria hidden on some svgs for accessibility 2025-07-01 12:40:03 +02:00
Sine Jespersen ab3c19dd36 aria labels and high contrast mode for accessibility 2025-07-01 12:39:35 +02:00
Tim Jaeryang Baek 7a25c9637a Merge pull request #15443 from open-webui/main
dev
2025-07-01 14:08:13 +04:00
Timothy Jaeryang Baek 59ba21bdf8 refac 2025-07-01 14:06:45 +04:00
Sine Jespersen 988fc3a72c aria-hidden and aria-label to enhance accessibility 2025-07-01 11:31:07 +02:00
Tim Jaeryang Baek 8b43641849 Merge pull request #15427 from open-webui/dependabot/npm_and_yarn/dev/html2canvas-pro-1.5.11
build(deps): bump html2canvas-pro from 1.5.8 to 1.5.11
2025-07-01 12:49:09 +04:00
Tim Jaeryang Baek b8c8c64720 Merge pull request #15419 from open-webui/dependabot/pip/backend/dev/uvicorn-standard--0.35.0
build(deps): bump uvicorn[standard] from 0.34.2 to 0.35.0 in /backend
2025-07-01 12:48:50 +04:00
Tim Jaeryang Baek 557be6c824 Merge pull request #15418 from open-webui/dependabot/pip/backend/dev/azure-identity-1.23.0
build(deps): bump azure-identity from 1.21.0 to 1.23.0 in /backend
2025-07-01 12:48:09 +04:00
Tim Jaeryang Baek c38177bcd5 Merge pull request #15438 from open-webui/dependabot/npm_and_yarn/dev/sortablejs-1.15.6
build(deps): bump sortablejs from 1.15.2 to 1.15.6
2025-07-01 12:44:58 +04:00
Sine Jespersen 00e2ad1a9f Aria labels on image 2025-07-01 10:10:37 +02:00
dependabot[bot] fffe0b8580 build(deps): bump sortablejs from 1.15.2 to 1.15.6
Bumps [sortablejs](https://github.com/SortableJS/Sortable) from 1.15.2 to 1.15.6.
- [Release notes](https://github.com/SortableJS/Sortable/releases)
- [Commits](https://github.com/SortableJS/Sortable/compare/1.15.2...1.15.6)

---
updated-dependencies:
- dependency-name: sortablejs
  dependency-version: 1.15.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-01 06:59:56 +00:00
Tim Jaeryang Baek 030e9ab1ae Merge pull request #15417 from jackthgu/websearch-searxng-url-required
fix: settings/web-search : make “Searxng Query URL” field required
2025-07-01 10:56:55 +04:00
dependabot[bot] b685fd4a33 build(deps): bump html2canvas-pro from 1.5.8 to 1.5.11
Bumps [html2canvas-pro](https://github.com/yorickshan/html2canvas-pro) from 1.5.8 to 1.5.11.
- [Release notes](https://github.com/yorickshan/html2canvas-pro/releases)
- [Changelog](https://github.com/yorickshan/html2canvas-pro/blob/main/CHANGELOG.md)
- [Commits](https://github.com/yorickshan/html2canvas-pro/compare/v1.5.8...v1.5.11)

---
updated-dependencies:
- dependency-name: html2canvas-pro
  dependency-version: 1.5.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-01 05:55:54 +00:00
dependabot[bot] 876bb4bdc6 build(deps): bump uvicorn[standard] from 0.34.2 to 0.35.0 in /backend
Bumps [uvicorn[standard]](https://github.com/encode/uvicorn) from 0.34.2 to 0.35.0.
- [Release notes](https://github.com/encode/uvicorn/releases)
- [Changelog](https://github.com/encode/uvicorn/blob/master/docs/release-notes.md)
- [Commits](https://github.com/encode/uvicorn/compare/0.34.2...0.35.0)

---
updated-dependencies:
- dependency-name: uvicorn[standard]
  dependency-version: 0.35.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-01 05:21:20 +00:00
dependabot[bot] 534c12b1b9 build(deps): bump azure-identity from 1.21.0 to 1.23.0 in /backend
Bumps [azure-identity](https://github.com/Azure/azure-sdk-for-python) from 1.21.0 to 1.23.0.
- [Release notes](https://github.com/Azure/azure-sdk-for-python/releases)
- [Changelog](https://github.com/Azure/azure-sdk-for-python/blob/main/doc/esrp_release.md)
- [Commits](https://github.com/Azure/azure-sdk-for-python/compare/azure-identity_1.21.0...azure-identity_1.23.0)

---
updated-dependencies:
- dependency-name: azure-identity
  dependency-version: 1.23.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-01 05:21:17 +00:00
Taehong Gu 15821af252 fix(admin/web-search): add required validation to “Searxng Query URL” field 2025-07-01 14:08:01 +09:00
Tim Jaeryang Baek a463ef3c03 Merge pull request #15407 from silentoplayz/bannerlengthincrease
refac: Increase banners height limit
2025-06-30 21:41:12 +04:00
Silentoplayz 9813019489 Update Banner.svelte 2025-06-30 12:22:39 -04:00
Timothy Jaeryang Baek de018f0912 refac 2025-06-30 18:04:11 +04:00
Timothy Jaeryang Baek 8b35ea6eea enh: OTEL_OTLP_SPAN_EXPORTER 2025-06-30 15:52:32 +04:00
Timothy Jaeryang Baek bab1453b4c refac: styling 2025-06-30 15:39:43 +04:00
Timothy Jaeryang Baek a14cfb6448 enh: more info in feedback modal 2025-06-30 15:38:25 +04:00
Timothy Jaeryang Baek 71d550dd26 enh: offline mode 2025-06-30 15:20:41 +04:00
Timothy Jaeryang Baek 2748fc1e4d refac 2025-06-30 13:29:47 +04:00
Timothy Jaeryang Baek ce311598cb refac: styling 2025-06-30 13:29:23 +04:00
Timothy Jaeryang Baek 8a334decf6 refac: ENABLE_MODEL_LIST_CACHE -> ENABLE_BASE_MODELS_CACHE 2025-06-30 13:27:07 +04:00
Timothy Jaeryang Baek eac2f36f4f refac: temporary enforced behaviour 2025-06-30 13:16:46 +04:00
Tim Jaeryang Baek c75d0bd20b Merge pull request #15389 from headwAI-GmbH/add-qdrant-indices
feat: Add qdrant indices
2025-06-30 11:36:31 +04:00
Tim Jaeryang Baek 74912a5383 Merge pull request #15380 from taylorwilsdon/issues/15074/REDIS_KEY_PREFIX
feat: Support configuration of redis key prefix via environment variable
2025-06-30 10:43:35 +04:00
guenhter 5c2e0e4beb feat: add qdrant indices for metadata fields
All fieldnames which are part of a query should
have an index for performance reasons. This is
even enforced on some qdrant cluster like those
on qdrant.io, and queries using a unindexed column
fail with an error.
2025-06-29 15:30:55 +02:00
Taylor Wilsdon 7b5df819b1 fmt 2025-06-28 10:18:30 -07:00
Taylor Wilsdon 345922a970 add REDIS_KEY_PREFIX env var and default to current hardcoded value 2025-06-28 10:11:26 -07:00
Timothy Jaeryang Baek 7523613fbc refac 2025-06-28 19:29:28 +04:00
Timothy Jaeryang Baek 962e078a9a refac 2025-06-28 19:15:52 +04:00
Timothy Jaeryang Baek 1b064a6c85 chore: format 2025-06-28 15:21:20 +04:00
Timothy Jaeryang Baek e06cdea0b5 refac 2025-06-28 15:20:38 +04:00
Timothy Jaeryang Baek 8f9e202a88 refac 2025-06-28 15:13:21 +04:00
Timothy Jaeryang Baek 1a52585769 enh: ENABLE_MODEL_LIST_CACHE 2025-06-28 15:12:31 +04:00
Timothy Jaeryang Baek 2b88f66762 refac: MODEL_LIST_CACHE_TTL -> MODELS_CACHE_TTL 2025-06-28 14:44:47 +04:00
Timothy Jaeryang Baek 53a08eb00d refac: styling 2025-06-28 14:36:26 +04:00
Timothy Jaeryang Baek ffd7b004f4 fix: admin chat share issue 2025-06-28 14:02:07 +04:00
Timothy Jaeryang Baek f123d70cff fix: enforced temp chat issue 2025-06-28 14:01:58 +04:00
Timothy Jaeryang Baek 372836c87c chore: langchain dep bump 2025-06-28 00:53:07 +04:00
Timothy Jaeryang Baek ff639a20d0 refac 2025-06-28 00:48:24 +04:00
Tim Jaeryang Baek bfd92ec4af Merge pull request #15366 from akiraro/feat/oidc-httpx-timeout
feat: Custom oauth timeout
2025-06-28 00:46:21 +04:00
Haziq Yusof 88ea0f523f feat: custom oauth timeout 2025-06-28 03:08:46 +08:00
Timothy Jaeryang Baek dd1e4ee983 refac 2025-06-27 18:29:37 +04:00
Timothy Jaeryang Baek c91b0cd8c4 refac: vite config 2025-06-27 16:42:10 +04:00
Timothy Jaeryang Baek 3ff864eb63 refac: select action model behaviour 2025-06-27 16:29:04 +04:00
Timothy Jaeryang Baek 90257442e3 refac: styling 2025-06-27 16:15:16 +04:00
Timothy Jaeryang Baek 0447790e6a feat: MODEL_LIST_CACHE_TTL 2025-06-27 15:59:30 +04:00
Timothy Jaeryang Baek 09f0c6beee fix: windows start script 2025-06-27 15:46:38 +04:00
Timothy Jaeryang Baek c8e8111dd9 refac 2025-06-27 15:44:26 +04:00
Timothy Jaeryang Baek 38dfa6e6db refac 2025-06-27 15:30:00 +04:00
Tim Jaeryang Baek 276bfa02d0 Merge pull request #15314 from silentoplayz/spinner-and-xmark-icon-components
refac: Spinner and XMark components
2025-06-27 15:23:29 +04:00
Tim Jaeryang Baek 79aa73347e Merge pull request #15318 from hula-la/fix/ko-translation
i18n: Fix Korean translation errors in UI messages
2025-06-27 15:22:33 +04:00
Timothy Jaeryang Baek 2ae9bc30bd refac: feedback item click issue 2025-06-27 15:21:46 +04:00
Timothy Jaeryang Baek aa13a7caf9 refac 2025-06-27 14:58:23 +04:00
Tim Jaeryang Baek 8c5beddb5f Merge pull request #15310 from silentoplayz/icons
chore: Remove `MagnifyingGlass` icon component and update to use Search icon component
2025-06-27 14:58:06 +04:00
Tim Jaeryang Baek c303232657 Merge pull request #15344 from silentoplayz/small-fixes
fix: A couple small fixes
2025-06-27 14:31:01 +04:00
Tim Jaeryang Baek 8b49d6dda0 Merge pull request #15325 from headwAI-GmbH/make-qdrant-collection-prefix-configurable
feat: support better qdrant collection isolation
2025-06-27 14:30:26 +04:00
Silentoplayz e3bacc3296 fix: Small fixes 2025-06-27 04:35:28 -04:00
Tim Jaeryang Baek 767c4426cd Merge pull request #15328 from acwoo97/feat/lock
feat : Retry acquiring usage cleanup lock to handle potential stale locks
2025-06-27 11:52:13 +04:00
안찬우[AI Product] 789e6a0db3 feat : Retry acquiring usage cleanup lock to handle potential stale locks 2025-06-26 22:24:57 +09:00
guenhter a66206f44f feat: support better qdrant collection isolation
The prefix string for qdrant collection is now
configurable,  which means the same qdrant cluster
can be used to host more open webui instances and
to be able to separate the collections between the
different owui instances.
2025-06-26 13:52:26 +02:00
soo-jin-kim 2f4604921f fix: fix korean translation 2025-06-26 17:18:07 +09:00
Timothy Jaeryang Baek 21d616f8ed fix: code interpreter code editor issue 2025-06-26 11:05:17 +04:00
Silentoplayz f429dd667f chore: remove comment 2025-06-25 18:56:08 -04:00
Silentoplayz d1e3940abd refac: Spinner and XMark components 2025-06-25 18:44:45 -04:00
Silentoplayz cf24e39404 chore: add strokeWidth back 2025-06-25 17:36:23 -04:00
Silentoplayz dd0e6bf58f remove class="w-5 h-5" /> 2025-06-25 14:04:40 -04:00
Silentoplayz 88c4f55a51 chore: update 2025-06-25 14:00:48 -04:00
Silentoplayz 3f865f0b66 chore: MagnifyingGlass -> Search 2025-06-25 13:42:18 -04:00
Tim Jaeryang Baek b6e2b2e514 Merge pull request #15236 from headwAI-GmbH/password-login-type
fix: Use correct password autocomplete type on signup
2025-06-25 13:39:08 +04:00
Timothy Jaeryang Baek 8e95e5267c refac 2025-06-25 13:36:41 +04:00
Tim Jaeryang Baek d4fcf561e5 Merge pull request #15293 from itk-dev/feature/make-toggle-button-accessible
feat: aria-pressed and aria-label
2025-06-25 13:36:02 +04:00
Sine Jespersen 0ba84a670d add aria-pressed and aria-label to toggle button to make it accessible 2025-06-25 10:34:40 +02:00
Timothy Jaeryang Baek 1f123eb100 refac 2025-06-25 12:20:08 +04:00
Tim Jaeryang Baek 6676824947 Merge pull request #15238 from manelcomiche/main
feat(i18n): update Spanish and Catalan translations
2025-06-25 11:05:31 +04:00
Tim Jaeryang Baek 2bad7eaa07 Merge pull request #15277 from hankewyczz/bug/restore-exa-search
fix Restore exa
2025-06-25 11:04:48 +04:00
Tim Jaeryang Baek d60c800d66 Merge pull request #15276 from zhangtyzzz/update_brave_search
[fix] Update brave.py to use the correct field
2025-06-25 11:04:06 +04:00
Anush008 05bee5663d Merge remote-tracking branch 'origin/dev' 2025-06-25 12:04:23 +05:30
Tim Jaeryang Baek baa5920b7a Merge pull request #15250 from dlamoris/dev
fix: opensearch race condition, use keyword search instead of full text search for filter query
2025-06-25 10:30:36 +04:00
Tim Jaeryang Baek 889a3e1c48 Merge pull request #15262 from bobend/updated-da-DK
i18n: Updated da-DK translation
2025-06-25 10:30:18 +04:00
Tim Jaeryang Baek 565ec1567b Merge pull request #15268 from jackthgu/added-docker-compose-otel
build: add docker-compose.otel.yaml for OpenTelemetry
2025-06-25 10:26:46 +04:00
Tim Jaeryang Baek 7f681e8c02 Merge pull request #15273 from c0mplex10/translate
i18n: Improve Turkish translations
2025-06-25 10:26:15 +04:00
Zachar Hankewycz 45d7726ee0 Restore exa 2025-06-24 21:24:53 -04:00
zhangtyzzz ac5567f78d Update brave.py to use the correct field
fixing issues caused by incorrect field names.
2025-06-25 09:11:58 +08:00
c0mplex10 1875d7d726 i18n: Improve Turkish translations 2025-06-25 01:40:23 +03:00
Taehong Gu c77c4ff3f7 build: add docker-compose.otel.yaml for OpenTelemetry 2025-06-25 00:11:06 +09:00
Bo Bendtsen 2a25d625b2 Merge remote-tracking branch 'upstream/dev' into updated-da-DK 2025-06-24 12:54:17 +02:00
Bo Bendtsen 559c25fa3c Updated da-DK translation 2025-06-24 12:45:48 +02:00
Anush008 9dd24ee8fa chore: Bump qdrant-client version
Signed-off-by: Anush008 <anushshetty90@gmail.com>
2025-06-24 14:15:07 +05:30
Anush008 5dba298c1e refactor: Updated Qdrant multi-tenancy implementation
Signed-off-by: Anush008 <anushshetty90@gmail.com>
2025-06-24 14:12:44 +05:30
Tim Jaeryang Baek 0b1db0b216 Merge pull request #15242 from c0mplex10/patch-2
i18n: Improve Turkish translations
2025-06-24 11:34:59 +04:00
Timothy Jaeryang Baek 947b77db24 refac 2025-06-24 10:36:33 +04:00
Doris Lam 74ae9ab897 fix opensearch race condition, use keyword search instead of full text search in filter query 2025-06-23 18:43:33 -07:00
c0mplex10 c9c5340762 Update translation.json 2025-06-23 17:46:12 +03:00
denispol 77a96870ea Merge branch 'open-webui:dev' into dev 2025-06-23 15:42:32 +02:00
LHCLYT e237600e51 feat(i18n): update Catalan translations
Updated several Catalan translation strings to improve grammar, consistency, and clarity.

Includes fixes for technical terms and overall localization quality.
2025-06-23 12:53:49 +02:00
LHCLYT 708010a16a feat(i18n): update Spanish translations
Updated several Spanish translation strings to improve grammar, consistency, and clarity.

Includes fixes for technical terms and overall localization quality.
2025-06-23 12:43:14 +02:00
guenhter f0e60c28df fix: use correct password autocomplete for signup
Password manager act based on the 'autocomplet' attribute
of the password fields. If the attribut is set to
"current-password" they try to fill the password with an
existing one. If it is set to "new-password" they try to
support the user by generating a new password.
For signup in owui, the password was always set to
"current-password", so the password manager never proposed
a password on signup.
2025-06-23 11:48:51 +02:00
Tim Jaeryang Baek 610680ac14 Merge pull request #15213 from beecho01/patch-1
i18n: en-GB English translation updates
2025-06-23 13:12:44 +04:00
Timothy Jaeryang Baek f4b92868c4 refac 2025-06-23 12:54:50 +04:00
Tim Jaeryang Baek 9b2c3dec3a Merge pull request #15220 from prilosac/allow_auto_gpt_image_1
fix: Allowing 'auto' for IMAGE_SIZE on gpt-image-1
2025-06-23 10:50:44 +04:00
Tim Jaeryang Baek 6c7a7bc891 Merge pull request #15219 from qingchunnh/Update_zh-CN-25623
i18n: Update & Improve zh-CN
2025-06-23 10:49:41 +04:00
qingchun 018000c41b i18n: Improve zh-CN 2025-06-23 04:56:35 +08:00
Lucas Teixeira 95610080fb allowing auto on gpt-image-1 2025-06-22 13:55:29 -07:00
qingchun a5cdd25802 i18n: Update & Improve zh-CN 2025-06-23 04:20:47 +08:00
James Beeching 2df1f83e79 Merge branch 'dev' into patch-1 2025-06-22 14:17:24 +01:00
James Beeching 13cd025a7d Update missed spelling to en-GB equivalent 2025-06-22 14:03:36 +01:00
James Beeching 986e583594 Update translation.json for en-GB 2025-06-22 13:52:16 +01:00
Tim Jaeryang Baek 0563738940 Merge pull request #15208 from yukebrillianth/main
i18n: Fix translation placeholder for SSO provider in Bahasa Indonesia
2025-06-22 15:32:32 +04:00
Tim Jaeryang Baek 21d0431178 Merge pull request #15211 from Keram-Yasin/main
i18n: translation edit for ug-CN
2025-06-22 15:32:16 +04:00
Keram Yasin bb1f8f8ae9 Uyghur Translations Editted
1. The Uyghur translations are edited based on live instance.
2025-06-22 12:25:19 +02:00
Yuke Brilliant Hestiavin b1f0ad79a8 fix: SSO provider translation in Bahasa Indonesia 2025-06-22 07:00:44 +07:00
Timothy Jaeryang Baek aef0ad2d10 refac 2025-06-21 19:12:43 +04:00
Timothy Jaeryang Baek 38e8209d71 enh: ENABLE_COMPRESSION_MIDDLEWARE env var 2025-06-21 17:58:56 +04:00
Tim Jaeryang Baek 2eae63bcd4 Merge pull request #15195 from prene/proper-default-pooling-settings
[PERF] Proper default pooling settings
2025-06-21 17:39:27 +04:00
René Pfitzner 63d99abf41 Change default db pooling 2025-06-21 14:01:57 +01:00
Tim Jaeryang Baek 7513dc7e34 Merge pull request #15146 from rgaricano/dev
i18n: en-US correction of num_keep parameter description
2025-06-21 15:11:26 +04:00
Timothy Jaeryang Baek 2658ab1e46 chore: format 2025-06-21 15:07:52 +04:00
Timothy Jaeryang Baek df060df88b refac: styling 2025-06-20 21:10:56 +04:00
Timothy Jaeryang Baek e035d5d767 refac: styling 2025-06-20 20:57:13 +04:00
Timothy Jaeryang Baek 493595c891 refac: styling 2025-06-20 20:51:40 +04:00
Timothy Jaeryang Baek cd70469a8a chore: format 2025-06-20 20:32:30 +04:00
Timothy Jaeryang Baek 17b9a81504 chore: format 2025-06-20 20:32:23 +04:00
Tim Jaeryang Baek e3e2bf841e Merge pull request #15170 from Keram-Yasin/main
i18n: add translation for ug-CN
2025-06-20 20:28:53 +04:00
Tim Jaeryang Baek 2f94b3947e Merge pull request #15167 from ascodeasice/dayjs-zh-tw-locale
i18n: Add zh-tw locale support for dayjs
2025-06-20 20:28:16 +04:00
ascodeasice 9f33abf384 feat(dayjs): add zh-tw locale support 2025-06-20 16:24:04 +08:00
Tim Jaeryang Baek a21519f8f2 Merge pull request #15034 from Classic298/system-prompt-high-contrast
feat: System prompt input fields high contrast mode
2025-06-20 10:07:41 +04:00
Timothy Jaeryang Baek e1f5f1e0b1 refac 2025-06-20 09:58:44 +04:00
Tim Jaeryang Baek 4e50dd4df6 Merge pull request #15122 from tcx4c70/feat/add_stream_options_to_azure
feat(azure): Add stream_options to payload if api_version supports
2025-06-20 09:57:27 +04:00
Tim Jaeryang Baek 13c16b974c Merge pull request #15154 from Classic298/patch-1
i18n: de-DE German Translation Updates
2025-06-20 09:55:37 +04:00
Tim Jaeryang Baek b5445b5db2 Merge pull request #15148 from jackthgu/none-dbconnect-warning-message
fix: Enhance database migration error logging
2025-06-20 09:55:02 +04:00
Taehong Gu c7ef6025af Provide more detailed information to the user upon database connection failure
modified:   backend/open_webui/internal/db.py
2025-06-20 14:13:43 +09:00
Classic298 f0dea9a7c4 Update translation.json 2025-06-19 23:27:14 +02:00
Keram Yasin f7c7d72857 Uyghur Translation Review 1. The translations are reviewed and corrected for three times. 2025-06-19 22:19:17 +02:00
Timothy Jaeryang Baek 81b8267e85 feat: odt file parse support 2025-06-19 18:39:00 +04:00
_00_ f939646364 i18n: en-US correction of num_keep parameter description
i18n: en-US correction of num_keep parameter description

Correction of num_keep parameter,
In actual description example is indicated that "last" x tokens will be retained, that is wrong, it have to say "first" x tokens.

Tokens to Keep on Context Refresh (num_keep): Retains part of the previous conversation. It's used when the n_ctx limit is reached. A new prompt will be constructed with the first n_keep characters of the original prompt plus the second half of the output to free up space for more conversation. Example: Keeping 50 first tokens helps the model remember the main topic when refreshing.

https://github.com/open-webui/open-webui/discussions/3794#discussioncomment-12691428
2025-06-19 14:18:15 +02:00
Tim Jaeryang Baek 4f8f349c8f Merge pull request #15142 from Classic298/fix-oauth
fix: OAuth authentication failure on second login due to stale session state
2025-06-19 15:56:26 +04:00
Classic298 bbdb098b87 Update auths.py 2025-06-19 13:44:03 +02:00
Keram 646b3ca7f6 Uyghur Translation Uploaded
1. All the file is translated to Uyghur for the first time.
2. The next step is to review all the translations.
2025-06-19 12:30:42 +02:00
Timothy Jaeryang Baek a196b9dc26 fix: direct tool servers not displaying 2025-06-19 14:03:36 +04:00
Tim Jaeryang Baek b08fc297cf Merge pull request #15138 from zhangtyzzz/exa_search_missing
fix: Resolve exa search not being processed correctly
2025-06-19 10:34:51 +04:00
zhangtyzzz 5f60b30320 add missed exa 2025-06-19 13:52:58 +08:00
Keram Yasin 088e54c4fa Uyghur Localization Files Added
1. Uyghur localization file is added.
2. Uyghur language code is added to language list.
2025-06-18 22:11:45 +02:00
Adam Tao baafdb752c feat(openai): Add stream_options to payload if api_version supports
Signed-off-by: Adam Tao <tcx4c70@gmail.com>
2025-06-18 21:17:09 +08:00
Tim Jaeryang Baek d6f13bee5e Merge pull request #15094 from silentoplayz/small-fix
fix: sanatizeResponseContent for stop token display
2025-06-18 16:57:27 +04:00
denispol 6555cf9d7a fix: enhance paste handling for mobile WebViews and large text 2025-06-18 14:50:16 +02:00
Timothy Jaeryang Baek 1547235d47 refac: chat input object localStorage -> sessionStorage 2025-06-18 16:02:29 +04:00
Classic298 b9ea6130c7 Update General.svelte 2025-06-18 13:10:02 +02:00
Classic298 3814bbf7b3 Update General.svelte 2025-06-18 13:09:21 +02:00
Tim Jaeryang Baek 34440f95dc Merge pull request #15093 from silentoplayz/fix-sidebar-drag-and-drop
fix: refine folder component's drag-&-drop error handling
2025-06-18 14:04:52 +04:00
Tim Jaeryang Baek 1abc3847ab Merge pull request #15108 from itk-dev/feature/telemetry-auth
feat: Added support for basic auth wiht OTEL exporter
2025-06-18 14:04:15 +04:00
Timothy Jaeryang Baek 6186bbf337 refac/fix: stt supported type 2025-06-18 14:01:14 +04:00
Jesper Kristensen 4119ab261e Added support for basic auth wiht OTEL exporter 2025-06-18 11:42:33 +02:00
Tim Jaeryang Baek e79f31b9c4 Merge pull request #15029 from Classic298/correct-markdown-rendering-model-descriptions
fix: Correct markdown rendering model descriptions
2025-06-18 12:50:31 +04:00
Tim Jaeryang Baek 1d179399f3 Merge pull request #15069 from d2a-pnagel/codex/repareer-fouten-in-nl-vertaalbestand
i18n: Fix errors in Dutch translations
2025-06-18 12:49:59 +04:00
Tim Jaeryang Baek 294d6ce060 Merge pull request #15101 from itk-dev/feature/high-contrast-mode-tools-section
FEAT: high contrast mode tools section
2025-06-18 12:34:24 +04:00
Sine Jespersen 3ff5db229b accessibility: labels and placeholder contrast 2025-06-18 09:50:04 +02:00
Sine Jespersen 27f7df8ad6 accessibility: placeholder contrast on sensitiveinput 2025-06-18 09:50:04 +02:00
Sine Jespersen bd818b4524 outline on switch button 2025-06-18 09:50:04 +02:00
Sine Jespersen 99b153a132 add aria label to button 2025-06-18 09:50:04 +02:00
Sine Jespersen d837e6e579 decorative svgs hidden from screen reader 2025-06-18 09:50:04 +02:00
Sine Jespersen 226a7578c6 labels for inputs 2025-06-18 09:50:04 +02:00
Sine Jespersen f36f943a54 screen readers: label button, and remove decorative svg 2025-06-18 09:50:04 +02:00
Sine Jespersen 3cdefff0d2 change div to h1, as it is the modal header 2025-06-18 09:50:04 +02:00
Tim Jaeryang Baek e05560a49d Merge pull request #15072 from PierreMesure/better-swedish-translation-1
Better swedish translation 1
2025-06-18 10:59:43 +04:00
Tim Jaeryang Baek 05aaefd8a5 Merge pull request #15085 from qingchunnh/Update_zh-CN-25618
i18n: Update & Improve zh-CN
2025-06-18 10:59:09 +04:00
Tim Jaeryang Baek 58b5d2b727 Merge pull request #15097 from silentoplayz/index-update
Refactor: Add missing Type definitions
2025-06-18 10:55:44 +04:00
Timothy Jaeryang Baek 0134b5eca4 fix: action reserved user param 2025-06-18 10:50:49 +04:00
Silentoplayz 3be4fb25cd fix: add ? 2025-06-18 02:01:21 -04:00
Silentoplayz d66285f926 chore: add toolIds type 2025-06-18 01:55:43 -04:00
Silentoplayz 35be40c932 chore: add more Settings types in stores 2025-06-18 01:48:34 -04:00
Silentoplayz 898b65a653 fix: sanatizeResponseContent for stop token display 2025-06-18 00:12:11 -04:00
Silentoplayz b120637127 fix: refine folder component's drag-&-drop error handling 2025-06-17 23:27:45 -04:00
qingchun 70968a6caf i18n: Update & Improve zh-CN 2025-06-18 01:17:41 +08:00
Pierre 8a169d5c8e Update Swedish translation 2025-06-17 19:09:38 +02:00
d2a-pnagel 5f82631ae6 Update translation.json 2025-06-17 14:23:53 +02:00
d2a-pnagel fdeded4a20 Fix obvious typos in Dutch translation 2025-06-17 14:22:46 +02:00
Tim Jaeryang Baek 3b70841a7d Merge pull request #15049 from silentoplayz/fixopeninnewtab
fix: regression for "open in new tab"
2025-06-17 15:27:11 +04:00
Tim Jaeryang Baek 23c679d8db Merge pull request #15065 from aleixdorca/dev
i18n: Update Catalan translation.json
2025-06-17 14:25:53 +04:00
Tim Jaeryang Baek 8a587f9f84 Merge pull request #15066 from bogdanr/dev
i18n: Added a few Romanian translations
2025-06-17 14:25:44 +04:00
Timothy Jaeryang Baek fb26be7dd6 fix: confirm dialog input reset on show 2025-06-17 14:24:23 +04:00
Aleix Dorca 8908e834ac Update Catalan translation.json 2025-06-17 12:07:06 +02:00
Bogdan Radulescu 098091c95e Added a few Romanian translations 2025-06-17 12:57:09 +03:00
Tim Jaeryang Baek e866fba27c Merge pull request #15059 from d2a-raudenaerde/main
i18n Fix a incorrect translation of dutch ("Analyzing" -> from "Analysiseren" to  "Analyseren")
2025-06-17 13:23:54 +04:00
Classic298 872b0ac943 Update General.svelte 2025-06-17 11:23:24 +02:00
Classic298 22b02af703 Update General.svelte 2025-06-17 11:22:34 +02:00
Classic298 7de0e49dc6 Update Controls.svelte 2025-06-17 11:21:24 +02:00
Classic298 c330f06542 Merge branch 'dev' into system-prompt-high-contrast 2025-06-17 11:10:03 +02:00
Rob Audenaerde 6a613c2dd6 Update translation.json 2025-06-17 10:44:23 +02:00
Tim Jaeryang Baek 4254469a33 Merge pull request #15025 from Mte90/patch-1
Italian translation improved
2025-06-17 12:23:28 +04:00
Tim Jaeryang Baek 52cffef16e Merge pull request #15028 from Classic298/system-prompt-resizing
feat: System prompt input field resizable
2025-06-17 12:23:06 +04:00
Tim Jaeryang Baek d50449ea71 Merge pull request #15035 from decent-engineer-decent-datascientist/dev
fix: Resolve UnicodeEncodeError for non-ASCII usernames in forwarded headers
2025-06-17 12:22:02 +04:00
Timothy Jaeryang Baek cd975a1047 Update pyproject.toml 2025-06-17 12:17:02 +04:00
Silentoplayz 354dd3caa9 fix: regression for "open in new tab" 2025-06-16 23:27:16 -04:00
Classic298 c66576805b Merge branch 'open-webui:main' into fix-postgresql-null-value-search 2025-06-16 21:11:30 +02:00
Classic298 beb1f3f7fd Merge branch 'open-webui:main' into system-prompt-resizing 2025-06-16 21:11:21 +02:00
Classic298 c8c4e1642f Merge branch 'open-webui:main' into correct-markdown-rendering-model-descriptions 2025-06-16 21:11:18 +02:00
Classic298 135b57ca8e Update Placeholder.svelte 2025-06-16 20:28:50 +02:00
priten 8a63a601de Fix typo on ollama patch 2025-06-16 12:35:09 -05:00
priten f7920df870 Fix non-ascii error issue on ENABLE_FORWARD_USER_INFO_HEADERS 2025-06-16 12:33:11 -05:00
Classic298 5261d3d31e Update General.svelte 2025-06-16 19:26:57 +02:00
Classic298 088243349d Update Controls.svelte 2025-06-16 19:26:33 +02:00
Classic298 9d37350b97 Update Markdown.svelte 2025-06-16 18:08:20 +02:00
Classic298 4f9ce28a15 Update ChatPlaceholder.svelte 2025-06-16 18:07:30 +02:00
Classic298 9f936a0c19 Update General.svelte 2025-06-16 17:48:45 +02:00
Classic298 85724940ee Update Controls.svelte 2025-06-16 17:48:00 +02:00
Daniele Scasciafratte a7772b4b35 Italian translation improved 2025-06-16 17:09:29 +02:00
Tim Jaeryang Baek b5f4c85bb1 Merge pull request #15014 from open-webui/dev
0.6.15
2025-06-16 18:34:32 +04:00
Timothy Jaeryang Baek 340d9820b8 refac 2025-06-16 18:33:45 +04:00
Timothy Jaeryang Baek 597709c64c doc: changelog 2025-06-16 18:30:51 +04:00
Timothy Jaeryang Baek 84fd41e6ef doc: changelog 2025-06-16 18:29:38 +04:00
Timothy Jaeryang Baek 3917b04ed3 doc: changelog 2025-06-16 18:27:18 +04:00
Timothy Jaeryang Baek b6ea043d18 chore: format 2025-06-16 17:59:07 +04:00
Timothy Jaeryang Baek b082a7edbe enh: allow iframe in content 2025-06-16 17:43:17 +04:00
Timothy Jaeryang Baek 72df23ed79 refac 2025-06-16 17:24:55 +04:00
Timothy Jaeryang Baek 11476eb6ee chore: format 2025-06-16 17:21:22 +04:00
Tim Jaeryang Baek 7096baff1b Merge pull request #15021 from rgaricano/dev
i18n: es-ES Translation v.0.6.15
2025-06-16 17:19:44 +04:00
Tim Jaeryang Baek 2885ed9648 Merge pull request #15019 from itk-dev/feature/high-contrast-mode-tools-section
Feature/high contrast mode tools section
2025-06-16 17:19:30 +04:00
Timothy Jaeryang Baek ab877e1d7e refac 2025-06-16 17:18:43 +04:00
_00_ b13a824047 UPD: es-ES Translation v.0.6.15
UPD: es-ES Translation v.0.6.15
2025-06-16 15:06:33 +02:00
Timothy Jaeryang Baek 9cbe2644b1 refac 2025-06-16 16:59:47 +04:00
Timothy Jaeryang Baek 6c54ca552a feat: global image compression 2025-06-16 16:52:57 +04:00
Timothy Jaeryang Baek 2949be4f27 refac 2025-06-16 16:33:48 +04:00
Timothy Jaeryang Baek 8e139b04f0 refac: iframe include allow-downloads by default 2025-06-16 16:24:51 +04:00
Timothy Jaeryang Baek 76fe344eb8 refac 2025-06-16 16:21:57 +04:00
Timothy Jaeryang BaekandWilliamGates f3cae94028 fix: bypass webloader
Co-Authored-By: WilliamGates <3852641+williamgateszhao@users.noreply.github.com>
2025-06-16 16:17:52 +04:00
Timothy Jaeryang BaekandBryan Berns 7a1afa9c66 feat: custom stt content type
Co-Authored-By: Bryan Berns <berns@uwalumni.com>
2025-06-16 16:13:40 +04:00
Sine Jespersen 437b7c5171 add aria label to button without label for accessibility 2025-06-16 13:33:45 +02:00
Sine Jespersen 5211602c2f add text high contrast mode for accessibility 2025-06-16 13:33:25 +02:00
Timothy Jaeryang Baek 6a5aac43df refac 2025-06-16 15:29:40 +04:00
Tim Jaeryang Baek d6664369a1 Merge pull request #14886 from Constantinople-AI/fix-no-arg-mcp-call
Isolated fix to single section
2025-06-16 15:28:48 +04:00
Timothy Jaeryang Baek bc7defbdd0 refac: allow editing own email 2025-06-16 15:23:03 +04:00
Timothy Jaeryang Baek 99e18d49dc enh: system prompt user permission 2025-06-16 15:12:32 +04:00
Timothy Jaeryang Baek 1af128fb24 chore 2025-06-16 15:05:35 +04:00
Tim Jaeryang Baek b8103807cf Merge pull request #15018 from itk-dev/feature/interface-tab-settings-modal-accessibility
Feat: interface tab settings modal accessibility
2025-06-16 14:25:01 +04:00
Tim Jaeryang Baek d4f67bb266 Merge pull request #15017 from Bek85/i18n-uzbek
i18n: Add Uzbek (Cyrillic and Latin) i18n support
2025-06-16 14:24:30 +04:00
Timothy Jaeryang Baek f3deb8b8de refac 2025-06-16 14:01:03 +04:00
Timothy Jaeryang Baek 7753f57d42 chore: format 2025-06-16 13:48:50 +04:00
Timothy Jaeryang Baek 26d069080a refac 2025-06-16 13:45:36 +04:00
Timothy Jaeryang Baek d802c718a4 refac: max file count behaviour 2025-06-16 13:28:31 +04:00
Bekzod Akbarov ace2345a69 Add Uzbek (Cyrillic and Latin) i18n support
- Added `uz-Cyrl-UZ` and `uz-Latn-UZ` folders with `translation.json` files
- Updated `languages.json` to include both Uzbek language variants
2025-06-16 14:25:22 +05:00
Timothy Jaeryang Baek 59324a82e0 refac 2025-06-16 13:19:00 +04:00
Timothy Jaeryang Baek e46e87889e refac: pass through ollama error message 2025-06-16 13:03:56 +04:00
Sine Jespersen 6fcdd11fb2 add labels to image compression inputs 2025-06-16 10:56:54 +02:00
Sine Jespersen 047d8cec5d aria-labelledby on buttons to connect to labels 2025-06-16 10:51:12 +02:00
Tim Jaeryang Baek 61d8d2f2cb Merge pull request #14197 from jk-f5/otel_metrics
feat: Add OpenTelemetry Metrics Support via OTLP Exporter
2025-06-16 12:49:41 +04:00
Timothy Jaeryang Baek 51e05988bd refac 2025-06-16 12:46:04 +04:00
Tim Jaeryang Baek b37907671e Merge pull request #15010 from itk-dev/feature/outline-hidden-depend-on-high-contrast-mode
Feat: outline hidden depend on high contrast mode
2025-06-16 12:43:33 +04:00
Tim Jaeryang Baek 0e245c0afc Merge pull request #15011 from itk-dev/feature/outline-hidden-depend-on-high-contrast-mode-selects-general-tab
outline-hidden depend on highcontrastmode in selects
2025-06-16 12:42:54 +04:00
Timothy Jaeryang Baek b748d5edee fix: direct tool servers permissions 2025-06-16 12:42:16 +04:00
Timothy Jaeryang Baek debe3c752f chore: python-pptx bump 2025-06-16 12:39:42 +04:00
Timothy Jaeryang Baek ea578af45f refac: use first user message as title instead of 'new chat' 2025-06-16 12:37:41 +04:00
Timothy Jaeryang Baek 8258dfb5af enh: enable deepgram smart_format 2025-06-16 12:34:01 +04:00
Sine Jespersen 1909505466 make header h1 instead of div 2025-06-16 10:33:48 +02:00
Timothy Jaeryang Baek f98ea78964 refac: remove user & access_control field from files 2025-06-16 12:31:52 +04:00
Sine Jespersen f1e38fa376 outline-hidden depend on highcontrastmode in selects 2025-06-16 10:26:54 +02:00
Timothy Jaeryang Baek 090234735a Update apple-touch-icon.png 2025-06-16 12:13:51 +04:00
Timothy Jaeryang Baek afca606063 refac: banner scrollbar 2025-06-16 12:06:33 +04:00
Sine Jespersen 0d473a456a maje outline-hidden depend on highContrastMode due to accessibility 2025-06-16 10:05:34 +02:00
Timothy Jaeryang Baek 4a2428123b refac 2025-06-16 12:04:31 +04:00
Timothy Jaeryang Baek ba6fc385bb refac: styling 2025-06-16 12:04:15 +04:00
Timothy Jaeryang Baek 4db0da2b75 refac 2025-06-16 12:02:34 +04:00
Timothy Jaeryang Baek 5ab95cbc3a refac: remove redundant overlay with temp chat 2025-06-16 11:57:56 +04:00
Timothy Jaeryang BaekandClassic298 3b0da53457 feat: load-url search param added
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2025-06-16 11:55:23 +04:00
Tim Jaeryang Baek 5d90e31ab6 Merge pull request #15009 from itk-dev/feature/password-button-accessibility-again
Feat: password button accessibility again
2025-06-16 11:52:39 +04:00
Timothy Jaeryang Baek dc3055873b refac: display full model name on hover 2025-06-16 11:50:26 +04:00
Timothy Jaeryang Baek f5d250820b refac 2025-06-16 11:46:02 +04:00
Timothy Jaeryang Baek 8892c14984 refac 2025-06-16 11:44:07 +04:00
Sine Jespersen 75883dc835 outline-hidden depend on high contrast mode 2025-06-16 09:43:47 +02:00
Sine Jespersen d1677635a5 add label visible to screen readers to sensitive input 2025-06-16 09:41:29 +02:00
Sine Jespersen 400c4c3d12 add aria pressed to toggle visibility button, to show current state 2025-06-16 09:40:41 +02:00
Timothy Jaeryang Baek a47e3b429e fix/refac: enforce multiple models permission to imported chats 2025-06-16 11:40:13 +04:00
Timothy Jaeryang Baek 805e980ae5 fix: notes permissions issue 2025-06-16 11:35:01 +04:00
Timothy Jaeryang Baek b50cfd0940 refac 2025-06-16 11:14:24 +04:00
Sine Jespersen 53d044f8a2 add aria label to button with no other label 2025-06-16 09:12:12 +02:00
Sine Jespersen 0e95af01ea remove decorative svg from screen readers 2025-06-16 09:11:50 +02:00
Tim Jaeryang Baek c5b48ec551 Merge pull request #14992 from sreesdas/dev
Fix: Added support for multiple pages in external document loader
2025-06-16 11:01:33 +04:00
Timothy Jaeryang Baek e0ffe234ba refac: styling 2025-06-16 10:58:04 +04:00
Timothy Jaeryang Baek e0917e9f38 refac: new chat button moved to the leftside 2025-06-16 10:54:50 +04:00
Timothy Jaeryang Baek 177f2902db refac 2025-06-16 10:51:30 +04:00
Timothy Jaeryang Baek 423a35782b refac: usage event handling 2025-06-16 10:42:34 +04:00
Tim Jaeryang Baek deaa7133a2 Merge pull request #14903 from vaclcer/vaclavs-docling-picture-describe-fix
fix: Docling - serialize picture description parameters
2025-06-16 09:58:05 +04:00
Tim Jaeryang Baek 37658e06a2 Merge pull request #14857 from Classic298/feat/code-interpreter-url-param
feat: Enable Code Interpreter using URL Parameter
2025-06-16 09:57:25 +04:00
Tim Jaeryang Baek 1f7a93b0fb Merge pull request #14937 from silentoplayz/situational-keyboard-shortcuts
feat: Add New Keyboard Shortcuts with asterisk and tooltip to imply situational
2025-06-16 09:54:05 +04:00
Tim Jaeryang Baek 866c61bac6 Merge pull request #14982 from silentoplayz/fix-drag
fix: Replace Inline SVGs with Icon Components and Convert Links to Buttons
2025-06-16 09:53:17 +04:00
Tim Jaeryang Baek c2b4b76a90 Merge pull request #14944 from silentoplayz/sort-feedbacks
feat: Implement Client-Side Sorting for Feedback History Table
2025-06-16 09:48:17 +04:00
Tim Jaeryang Baek 5a4a293a93 Merge branch 'dev' into sort-feedbacks 2025-06-16 09:47:51 +04:00
Timothy Jaeryang Baek c060e70089 refac: styling 2025-06-16 09:43:34 +04:00
Timothy Jaeryang Baek e34f11f5cc refac: styling 2025-06-16 09:40:40 +04:00
Timothy Jaeryang Baek 3e55c7f333 refac 2025-06-16 09:36:29 +04:00
Tim Jaeryang Baek b8a4824305 Merge pull request #14985 from Classic298/multi-model-chat-highlighting
Feat: Improve selected message highlighting for multi-model chats
2025-06-16 09:21:05 +04:00
Tim Jaeryang Baek 2251a50451 Merge pull request #14990 from SadmL/patch-1
[i18n] Russian locale update
2025-06-16 09:18:33 +04:00
sree 62bfe73964 Fix: Added support for multiple pages in external document loader, added filename in api request header 2025-06-15 19:59:05 +05:30
Link [Связной] 2eeffc8c59 [i18n] Russian locale update 2025-06-15 13:54:49 +03:00
Silentoplayz 6e8c6e269e fix: select-none w-full 2025-06-14 18:26:27 -04:00
Classic298 14cfd4599c Improve selected message highlight (#12) 2025-06-14 21:49:33 +02:00
Silentoplayz 4fd7bfe19b refac: Icon files for UserMenu 2025-06-14 14:31:20 -04:00
Silentoplayz 9ccb9a87e6 fix: Prevent draggable links for Admin Panel & Playground in UserMenu 2025-06-14 13:56:30 -04:00
Tim Jaeryang Baek 53060a44ae Merge pull request #14941 from silentoplayz/chat-bubble-timestamp-on-mouse-hover
Feat: Show timestamp on hover of user message in Chat Bubble UI mode
2025-06-14 18:06:47 +04:00
Tim Jaeryang Baek 4c7c5dd860 Merge pull request #14911 from ayanahye/feedback-evaluation-modal
Feat: add modal to display more evaluation information in Feedback History
2025-06-14 18:01:05 +04:00
Tim Jaeryang Baek 69be2a1bc9 Merge pull request #14949 from ayanahye/leaderboard-evaluation-additions
Feat: add initial modal for the leaderboard
2025-06-14 18:00:17 +04:00
Tim Jaeryang Baek 42b8463fcc Merge pull request #14959 from itk-dev/feature/accessibility-edit-connection-modal
Feature/accessibility edit connection modal
2025-06-14 17:58:38 +04:00
Tim Jaeryang Baek 35df44d07e Merge pull request #14971 from qingchunnh/Update_zh-CN-25614
i18n: Update zh-CN
2025-06-14 16:53:16 +04:00
qingchun 652e1a6b19 i18n: Update zh-CN 2025-06-14 02:34:08 +08:00
Sine Jespersen 78fcfbb328 hide decorative svg 2025-06-13 14:43:03 +02:00
Sine Jespersen 33a3aed698 remove repeated classes 2025-06-13 14:42:55 +02:00
Sine Jespersen d0a114869e make taglist a list and add aria-labels 2025-06-13 14:10:29 +02:00
Sine Jespersen 7d8fb116b6 Use <ul and <li because the items are a list 2025-06-13 14:10:29 +02:00
Sine Jespersen 6f73561f7a remove decorative svg from screen readers 2025-06-13 14:10:29 +02:00
Sine Jespersen a710ea1f07 add aria label to delete button 2025-06-13 14:10:29 +02:00
Sine Jespersen b95bc727df add label for screen readers for model id input 2025-06-13 14:10:29 +02:00
Sine Jespersen ba9e9e8095 add aria label to button with no label 2025-06-13 14:10:29 +02:00
Sine Jespersen 47c65bac6c remove decorative svg from screen readers 2025-06-13 14:10:29 +02:00
Sine Jespersen 85018dd106 high contrast mode on placeholder 2025-06-13 14:10:29 +02:00
Sine Jespersen 669a9f0777 highContrastMode (for accessibility) on text color in add connection modal 2025-06-13 14:10:28 +02:00
Sine Jespersen 1160a03644 change label for url to label instead of div 2025-06-13 14:10:28 +02:00
Sine Jespersen 843810cc9f add aria hidden to decorative svg 2025-06-13 14:10:28 +02:00
Sine Jespersen 8b5f5bb327 add label to verify connection button 2025-06-13 14:10:28 +02:00
Sine Jespersen 15efb07961 add label to toggle connection button in configure connect modal 2025-06-13 14:10:28 +02:00
Sine Jespersen a1034a4ca3 correct classname to inputclassname as prop is named this 2025-06-13 14:10:28 +02:00
Tim Jaeryang Baek 4978dd9085 Merge pull request #14943 from jk-f5/fix/oauth
Fix double‐slash (//auth) in OAuth callback redirect
2025-06-13 16:08:28 +04:00
Tim Jaeryang Baek 0b8f86d27b Merge pull request #14946 from t-rekttt/patch-1
fix: Page number 1 not displaying in citation references
2025-06-13 16:06:07 +04:00
Tim Jaeryang Baek 6918eb44ac Merge pull request #14953 from itk-dev/feature/aria-label-on-cog-button
Feat: aria label on cog button
2025-06-13 16:04:55 +04:00
Tim Jaeryang Baek 91af2698be Merge pull request #14958 from Ithanil/disable_console_log
feat: Disable console.log/debug in production
2025-06-13 16:04:13 +04:00
Jan Kessler 4681b06206 also disable console.debug 2025-06-13 13:54:40 +02:00
Jan Kessler aecbc93c16 disable console.log entirely 2025-06-13 13:39:16 +02:00
Classic298 033e5c1e00 Update chats.py 2025-06-13 13:07:41 +02:00
Classic298 e50cde80f3 Fix search unicode error (#11) 2025-06-13 13:05:33 +02:00
Sine Jespersen 0313ac0166 aria-hidden true on decorative svg 2025-06-13 10:46:39 +02:00
Sine Jespersen 87a837f850 add aria label to cog button in connections tab in settings modal 2025-06-13 10:46:27 +02:00
Ayana H 4bb41d3ead Merge branch 'dev' into leaderboard-evaluation-additions 2025-06-12 22:41:20 -07:00
ayana edf927516e Add initial modal for the leadderboard as well 2025-06-12 22:28:06 -07:00
T-Rekt 190400d1e8 Fix: page number edge case
Fix when document?.metadata?.page == 0 (page 1), the page number will not be displayed
2025-06-13 04:49:17 +07:00
Silentoplayz 380a31fbac Feat: Implement Client-Side Sorting for Feedback History Table 2025-06-12 16:13:33 -04:00
Jason Kidd fced3efd98 fix: Trailing slash was never removed from request.base_url because it's not a string but rather a starlette.datastructures.URL 2025-06-12 12:29:08 -07:00
Silentoplayz d92d555c03 Feat: Show user message timestamps on hover in Chat Bubble UI mode 2025-06-12 15:13:24 -04:00
ayana 880323a4e4 update based on requested changes 2025-06-12 10:53:55 -07:00
Tim Jaeryang Baek 8a2723a7a6 Merge pull request #14922 from itk-dev/feature/high-contrast-mode-in-connections-tab-in-settings-modal
Feat: high contrast mode in connections tab in settings modal
2025-06-12 20:40:01 +04:00
Tim Jaeryang Baek 2ff889dce0 Merge pull request #14933 from itk-dev/feature/accessibility-add-edit-connection-modal
Feat: accessibility add edit connection modal
2025-06-12 20:38:35 +04:00
Tim Jaeryang Baek 6c677e4b28 Merge pull request #14868 from ayanahye/evaluation-additions
Feat: add ability to sort leaderboard
2025-06-12 20:32:47 +04:00
Silentoplayz a98661735f feat: Add New Keyboard Shortcuts with asterisk and tooltip to imply situational 2025-06-12 09:47:17 -04:00
Sine Jespersen b446bea6d1 hide decorative svg from screen readers 2025-06-12 14:37:01 +02:00
Sine Jespersen 6faa9db4a7 add aria-label to button without label 2025-06-12 14:36:44 +02:00
Sine Jespersen 2b276583c3 change div to h1, because it is the main header in the modal 2025-06-12 14:34:26 +02:00
Tim Jaeryang Baek 80a5aa7259 Merge pull request #14923 from flex-mincheol/feature/i18n-ko-kr
i18n: Update ko-KR (Korean) translation.json
2025-06-12 13:36:28 +04:00
Sine Jespersen 9d76f30f9b high contrast mode on text in connections tab on settings modal 2025-06-12 11:01:02 +02:00
Tim Jaeryang Baek 09bbcee391 Merge pull request #14893 from tcx4c70/fix/log-format-error
fix(tools): not all arguments converted during string formatting
2025-06-12 12:30:31 +04:00
Timothy Jaeryang Baek c2aa5b7399 chore: youtube-transcript-api bump 2025-06-12 12:25:28 +04:00
Timothy Jaeryang Baek 9b9d5d84f4 fix: trusted header email case sensitive issue 2025-06-12 12:22:15 +04:00
flex-mincheol 389dbd1983 i18n: Update ko-KR (Korean) translation.json 2025-06-12 17:08:43 +09:00
ayana 2f8c0b6b26 Feat: add modal to display for evaluation information in Feedbacks 2025-06-11 19:34:07 -07:00
ayana 464c67eb46 use different notation 2025-06-11 18:27:13 -07:00
Vaclav Cerny 4bbc32efa6 fix: serialize picture description parameters to JSON in DoclingLoader 2025-06-11 20:00:25 +02:00
Tim Jaeryang Baek c27d8a4a28 Merge pull request #14900 from silentoplayz/fix-references
fix: prevent redundant documents in expanded citation list
2025-06-11 21:13:30 +04:00
Timothy Jaeryang Baek 371bdd7afa refac 2025-06-11 20:40:19 +04:00
Tim Jaeryang Baek 0d00971dad Merge pull request #14898 from jackthgu/ldap-group-sync
feat: Configure LDAP group synchronization with Open WebUI
2025-06-11 20:23:53 +04:00
Tim Jaeryang Baek 80c359c24e Merge pull request #14888 from silentoplayz/i18n-fix2
chore: consistent apostrophe usage in components and i18n files
2025-06-11 20:19:32 +04:00
Timothy Jaeryang Baek 6076afd5f5 fix: url param issue
#14901
2025-06-11 20:16:53 +04:00
Silentoplayz 8e60174e0d fix: prevent redundant documents in expanded citation list 2025-06-11 11:37:46 -04:00
Taehong Gu ba591d8c41 Configure LDAP group synchronization with Open WebUI 2025-06-11 23:55:31 +09:00
Tim Jaeryang Baek 387c2377ba Merge pull request #14890 from itk-dev/feature/hide-decorative-svgs
Feat: aria hidden on svg/icons
2025-06-11 17:39:20 +04:00
Adam Tao 809d91a3df fix(tools): not all arguments converted during string formatting
Signed-off-by: Adam Tao <tcx4c70@gmail.com>
2025-06-11 21:36:36 +08:00
Sine Jespersen 99ff1684d0 aria hidden on svg/icons 2025-06-11 14:47:28 +02:00
Tim Jaeryang Baek 9a1d2a1844 Merge pull request #14878 from itk-dev/feature/settings-modal-tab-list
Feature: settings modal tab list
2025-06-11 16:36:06 +04:00
Sine Jespersen 5dd99a0ea7 add aria-controls and ids to connect tabs to content 2025-06-11 13:45:47 +02:00
Sine Jespersen aee60add1d add aria-selected to tabs 2025-06-11 13:42:50 +02:00
Sine Jespersen 6af91325e2 add roles tablist and tab to tabs in settings modal 2025-06-11 13:41:26 +02:00
Silentoplayz a3f3f58e54 chore: consistent apostrophe usage in components and i18n files 2025-06-11 07:15:44 -04:00
Timothy Jaeryang Baek bc5f7e31ac refac 2025-06-11 14:43:56 +04:00
Timothy Jaeryang Baek c70a037692 refac 2025-06-11 14:43:07 +04:00
Tim Jaeryang Baek bebbc24d27 Merge pull request #14860 from Classic298/patch-2
i18n: Add german translation for new model pinning feature
2025-06-11 14:15:48 +04:00
Tim Jaeryang Baek 4e61bdd109 Merge pull request #14858 from Kylapaallikko/dev
i18n: Update fi-FI translation
2025-06-11 14:15:38 +04:00
2underscores 88c8ea883c Isolated fix to single section 2025-06-11 19:58:55 +10:00
Timothy Jaeryang Baek 9123c57c8a refac 2025-06-11 12:12:29 +04:00
ayana af285297bc Feat: add ability to sort leaderboard 2025-06-10 21:09:25 -07:00
Classic298 5c156c854b Update translation.json 2025-06-10 21:19:24 +02:00
google-labs-jules[bot] 70333c4dcf feat: Enable Code Interpreter via URL parameter
Adds the ability to enable the Code Interpreter feature by appending
`code-interpreter=true` to the chat URL.

The `Chat.svelte` component's `initNewChat` function has been updated
to parse this URL parameter. If present, it sets the
`codeInterpreterEnabled` state variable to true.

This change leverages the existing mechanisms for:
- Saving and restoring `codeInterpreterEnabled` state to/from localStorage.
- Displaying and toggling the Code Interpreter state via the existing UI
  button in the `MessageInput` component.

No new UI elements were added as the existing UI correctly reflects
the feature's state.
2025-06-10 18:56:49 +00:00
Kylapaallikko 1ca70b886b Update fi-FI translation.json 2025-06-10 21:51:29 +03:00
Jason Kidd 210dc746f0 feat: Add OpenTelemetry Metrics Support via OTLP Exporter 2025-06-10 10:52:10 -07:00
Tim Jaeryang Baek 63256136ef Merge pull request #14675 from open-webui/dev
0.6.14
2025-06-10 18:17:50 +04:00
Timothy Jaeryang Baek fd8cdb383c doc: changelog 2025-06-10 18:10:20 +04:00
Timothy Jaeryang Baek ab42a192e6 refac: styling 2025-06-10 17:54:08 +04:00
Timothy Jaeryang Baek adab4dbf21 chore: bump 2025-06-10 17:50:32 +04:00
Tim Jaeryang Baek c14160d73a Merge pull request #14845 from itk-dev/feature/accessibility-aria-label-close-button-and-aria-hidden-svg
Feat: accessibility aria label close button and aria hidden svg
2025-06-10 17:37:31 +04:00
Timothy Jaeryang Baek a7330d97da chore: format 2025-06-10 16:53:37 +04:00
Timothy Jaeryang Baek b39d33cefb refac 2025-06-10 16:52:37 +04:00
Timothy Jaeryang Baek d2e7feea86 refac 2025-06-10 16:42:26 +04:00
Timothy Jaeryang Baek a018140c7d refac: admin settings models 2025-06-10 16:41:39 +04:00
Timothy Jaeryang Baek 9931ccef1e refac 2025-06-10 16:20:57 +04:00
Sine Jespersen 56b012e3a6 danish translation added 2025-06-10 14:06:14 +02:00
Sine Jespersen c9820d65a6 add aria-hidden to svg, as this is decorative and only cause confusion for users with assistive tech 2025-06-10 13:59:27 +02:00
Sine Jespersen 9c1c11cad0 use xmark icon instead of inline svg 2025-06-10 13:58:58 +02:00
Sine Jespersen 8b5ecfb2fe Add aria label to close button 2025-06-10 13:52:24 +02:00
Tim Jaeryang Baek 3e81705dd3 Merge pull request #14837 from itk-dev/feature/menu-link-anchor
Feat: menu link anchor
2025-06-10 15:10:58 +04:00
Tim Jaeryang Baek 5168d612dc Merge pull request #14838 from Classic298/Classic298-patch-1
i18n: Small german translation update
2025-06-10 15:09:16 +04:00
Classic298 e67ab489aa Merge branch 'dev' into Classic298-patch-1 2025-06-10 13:07:24 +02:00
Classic298 c11cc58b00 Small german translation update 2025-06-10 12:54:17 +02:00
Sine Jespersen 3288b82175 change admin settings link to anchor instead of button 2025-06-10 12:49:18 +02:00
Timothy Jaeryang Baek f67f86f06f refac: styling 2025-06-10 14:33:49 +04:00
Timothy Jaeryang Baek c4cd2b55f0 refac: styling 2025-06-10 14:13:52 +04:00
Timothy Jaeryang Baek bacf39902b refac: styling 2025-06-10 14:02:43 +04:00
Tim Jaeryang Baek aeb81767a9 Merge pull request #14834 from SadmL/patch-1
[i18n] Russian locale update
2025-06-10 13:57:23 +04:00
Timothy Jaeryang Baek 884282c4ed refac: styling 2025-06-10 13:57:13 +04:00
Timothy Jaeryang Baek f55579ce25 refac 2025-06-10 13:55:25 +04:00
Link [Связной] 02f5bfba73 [i18n] Russian locale update 2025-06-10 12:54:31 +03:00
Tim Jaeryang Baek f29c9d8dcf Merge pull request #14833 from open-webui/dependabot/pip/pip-512858e340
build(deps): bump requests from 2.32.3 to 2.32.4 in the pip group across 1 directory
2025-06-10 13:45:47 +04:00
Timothy Jaeryang Baek 4c65728443 chore: format 2025-06-10 13:43:18 +04:00
Timothy Jaeryang Baek b36b480b43 refac 2025-06-10 13:40:36 +04:00
Tim Jaeryang Baek b8450ecf01 Merge pull request #14832 from itk-dev/feature/high-contrast-mode-and-label-menu-search-bar
FEAT: high contrast mode and label menu search bar
2025-06-10 13:40:14 +04:00
Timothy Jaeryang Baek 464b086ec5 refac 2025-06-10 13:39:41 +04:00
dependabot[bot] fc83842ab2 build(deps): bump requests in the pip group across 1 directory
Bumps the pip group with 1 update in the / directory: [requests](https://github.com/psf/requests).


Updates `requests` from 2.32.3 to 2.32.4
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.32.3...v2.32.4)

---
updated-dependencies:
- dependency-name: requests
  dependency-version: 2.32.4
  dependency-type: direct:production
  dependency-group: pip
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-10 09:38:48 +00:00
Tim Jaeryang Baek 7b248fc1f4 Merge pull request #14830 from open-webui/dependabot/pip/backend/pip-512858e340
build(deps): bump requests from 2.32.3 to 2.32.4 in /backend in the pip group across 1 directory
2025-06-10 13:37:49 +04:00
Timothy Jaeryang Baek a28bec865b refac 2025-06-10 13:16:44 +04:00
Timothy Jaeryang Baek b7a91b1963 refac: ollama response 2025-06-10 13:10:31 +04:00
Timothy Jaeryang Baek 2ccc441b41 refac 2025-06-10 13:01:45 +04:00
dependabot[bot] 106494aa79 build(deps): bump requests
Bumps the pip group with 1 update in the /backend directory: [requests](https://github.com/psf/requests).


Updates `requests` from 2.32.3 to 2.32.4
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.32.3...v2.32.4)

---
updated-dependencies:
- dependency-name: requests
  dependency-version: 2.32.4
  dependency-type: direct:production
  dependency-group: pip
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-10 08:56:03 +00:00
Timothy Jaeryang Baek 0b336fb6f1 refac 2025-06-10 12:53:18 +04:00
Timothy Jaeryang Baek 8f68b25510 enh/refac: ollama advanced params 2025-06-10 12:48:34 +04:00
Sine Jespersen 7fd5304e76 add assistive technology accessible label for search input in settings modal 2025-06-10 10:27:06 +02:00
Sine Jespersen af0f2a678e highContrastMode on search input in settingsmodal for accessibility 2025-06-10 10:25:40 +02:00
Timothy Jaeryang Baek ad80643774 refac: styling 2025-06-10 12:15:47 +04:00
Tim Jaeryang Baek 6e11bf73a7 Merge pull request #14827 from itk-dev/feature/high-contrast-mode-menu
FEAT: high contrast mode menu
2025-06-10 11:49:47 +04:00
Sine Jespersen ce45939463 add highContrastMode to menu items/tabs to improve contrast 2025-06-10 09:39:10 +02:00
Timothy Jaeryang Baek d430fe9551 refac 2025-06-10 11:30:54 +04:00
Timothy Jaeryang Baek 7f488b3754 feat: experimental pgvector pgcrypto support 2025-06-09 18:14:33 +04:00
Tim Jaeryang Baek 6d4f449085 Merge pull request #14775 from janaki-sasidhar/fix/insecure-user-switching-when-trusted-email-header
fix: verify trusted email header matches active user session
2025-06-09 17:44:30 +04:00
Timothy Jaeryang Baek 7953aa5763 refac 2025-06-09 17:43:22 +04:00
Tim Jaeryang Baek 95f54f5778 Merge pull request #14811 from SadmL/patch-1
[i18n] Russian locale update
2025-06-09 17:40:35 +04:00
Timothy Jaeryang Baek 51fc98bed8 refac 2025-06-09 17:24:03 +04:00
Timothy Jaeryang Baek ea8dc333ee refac/fix: multi-replica tasks 2025-06-09 17:21:10 +04:00
Link [Связной] 5447ac2de6 [i18n] Russian locale update 2025-06-09 15:43:17 +03:00
Timothy Jaeryang Baek 5fae7360fb refac: hide profile image in mobile 2025-06-09 16:35:20 +04:00
Timothy Jaeryang Baek db3c26ab7a refac: async redis 2025-06-09 15:37:05 +04:00
Timothy Jaeryang Baek 8da1ce358f refac: model selector styling 2025-06-09 11:22:31 +04:00
Timothy Jaeryang Baek ce9455b9a2 refac: styling 2025-06-09 10:43:47 +04:00
Tim Jaeryang Baek 36e5e51510 Merge pull request #14797 from qingchunnh/Update_zh-CN-2569-2
i18n: Update zh-CN
2025-06-09 10:13:28 +04:00
qingchun 7f39097879 i18n: Update zh-CN 2025-06-09 13:04:54 +08:00
Timothy Jaeryang Baek 9409083d0b refac 2025-06-09 02:16:23 +04:00
Timothy Jaeryang Baek 2573882b88 refac 2025-06-09 02:13:25 +04:00
Timothy Jaeryang Baek d1be1de013 fix: typo 2025-06-09 01:50:10 +04:00
Timothy Jaeryang Baek 7c03601425 chore: bump 2025-06-09 01:49:01 +04:00
Timothy Jaeryang Baek 053ab2838b chore: format 2025-06-09 01:48:29 +04:00
Timothy Jaeryang Baek 602bb55434 refac: styling 2025-06-09 01:45:41 +04:00
Timothy Jaeryang Baek 19c09d216d revert 2025-06-09 01:36:32 +04:00
Tim Jaeryang Baek 7a11fd2a4d Merge pull request #14793 from qingchunnh/Update_zh-CN-2569
i18n: Update & Improve zh-CN
2025-06-09 01:31:51 +04:00
Tim Jaeryang Baek c04569fdd4 Merge pull request #14791 from aindriu80/my-clean-fix
i18n: Updated Irish translation
2025-06-09 01:31:44 +04:00
Timothy Jaeryang Baek b0da2c998b refac 2025-06-09 01:27:50 +04:00
Timothy Jaeryang Baek 86551de1f7 refac 2025-06-09 01:26:32 +04:00
Timothy Jaeryang Baek 2f338b6ae7 refac: styling 2025-06-09 01:25:42 +04:00
Timothy Jaeryang Baek a64667ca8d feat: pinned models 2025-06-09 01:24:11 +04:00
Timothy Jaeryang Baek 3448362e00 refac: styling 2025-06-09 01:16:08 +04:00
Timothy Jaeryang Baek 9cbe39718d refac 2025-06-09 01:14:40 +04:00
Timothy Jaeryang Baek 7969d8e8f3 refac 2025-06-09 00:53:16 +04:00
Timothy Jaeryang Baek c59b4cf379 chore: bump bits ui 2025-06-09 00:37:38 +04:00
Timothy Jaeryang Baek 5e91c2c1fe refac: styling 2025-06-09 00:33:41 +04:00
qingchun b66d86cb74 i18n: Update & Improve zh-CN 2025-06-09 03:24:43 +08:00
Aindriú Mac Giolla Eoin ec9e3ef2c3 Merge branch 'dev' into my-clean-fix 2025-06-08 19:32:34 +01:00
Aindriú Mac Giolla Eoin a3ed5045d3 i18n fix commit issue 2025-06-08 19:28:35 +01:00
Timothy Jaeryang Baek 7f75acff96 chore: format 2025-06-08 22:08:25 +04:00
Timothy Jaeryang Baek a944e1f526 fix: feedback score overflow styling 2025-06-08 21:29:05 +04:00
Timothy Jaeryang Baek 3a0696e1b0 refac 2025-06-08 21:21:51 +04:00
Timothy Jaeryang Baek d8d8380a78 refac/fix: multi-replica stop task (response) 2025-06-08 21:20:30 +04:00
Timothy Jaeryang Baek 0c57980e72 refac: tasks 2025-06-08 20:58:31 +04:00
Timothy Jaeryang Baek 51fe33395b refac 2025-06-08 20:27:08 +04:00
Timothy Jaeryang Baek 4fe45d4430 refac/security: python code format endpoint 2025-06-08 20:26:07 +04:00
Timothy Jaeryang Baek 87e5aee106 refac 2025-06-08 20:05:26 +04:00
Timothy Jaeryang Baek 0cd400f5ee refac: docling picture describe params 2025-06-08 20:02:14 +04:00
Timothy Jaeryang Baek b9c64d0936 refac: styling 2025-06-08 19:22:41 +04:00
Tim Jaeryang Baek 6cb519ca0e Merge pull request #14774 from rragundez/images-from-db
fix: Store and load code interpreter generated images from a central location (DB and/or cloud storage)
2025-06-08 19:09:28 +04:00
Tim Jaeryang Baek 6bf393a480 Merge pull request #14787 from vaclcer/vaclavs-custom-docling
feat: Customize Docling's "Describe Pictures" feature
2025-06-08 19:02:36 +04:00
Tim Jaeryang Baek ac4513a773 Merge pull request #14783 from mrexodia/local-dev-fixes
fix: Improve local development setup
2025-06-08 18:59:38 +04:00
Tim Jaeryang Baek dae4a7088e Merge pull request #14780 from qingchunnh/Update_zh-CN-2568
i18n: Update zh-CN
2025-06-08 18:40:44 +04:00
Tim Jaeryang Baek 50d9a2ac58 Merge pull request #14781 from lucyknada/patch-2
fix: fix #14752 and add manual transcription retrieval
2025-06-08 18:40:28 +04:00
Tim Jaeryang Baek 0b1c81791e Merge pull request #14779 from GewoonJaap/feature/audio-html-component
Add Audio HTML token support
2025-06-08 18:39:50 +04:00
Vaclav Cerny 99f05561f8 Add configuration options for picture description modes and update related components 2025-06-08 16:30:26 +02:00
Duncan Ogilvie 5e73fd569d Improve local development setup 2025-06-08 15:17:08 +02:00
Duncan Ogilvie 7010393a41 Change default CORS_ALLOW_ORIGIN to '*'
The local development setup defaults do not actually work currently.
2025-06-08 15:16:51 +02:00
Duncan Ogilvie 41220b379f Exclude the 'backend' folder from the Vite watcher.
This avoids unnecessary reloads when working on the backend.
2025-06-08 14:58:56 +02:00
lucy b0965a8184 fixes #14752 and adds manual transcription option 2025-06-08 14:26:24 +02:00
qingchun 76d1690a0c i18n: Update zh-CN 2025-06-08 19:56:11 +08:00
Jaap d269c4b8e6 Audio token 2025-06-08 12:08:07 +02:00
Tim Jaeryang Baek 2b9c466bed Merge pull request #14765 from diwakar-s-maurya/patch-10
fix: save Document settings only when Save button is pressed
2025-06-08 13:05:51 +04:00
Tim Jaeryang Baek 1facc4485a Merge pull request #14766 from EntropyYue/feature/secret-key-file
feat: add support for WEBUI_SECRET_KEY_FILE
2025-06-08 13:05:29 +04:00
Tim Jaeryang Baek de3e474224 Merge pull request #14769 from RGSS3/main
The translation might be confusing, in some cases for example tooltip…
2025-06-08 13:04:50 +04:00
Tim Jaeryang Baek cc563d9763 Merge pull request #14770 from silentoplayz/small-fix
fix: remove newline from Pending User Overlay Title input field
2025-06-08 13:04:19 +04:00
Tim Jaeryang Baek ffe7bdd213 Merge pull request #14772 from silentoplayz/fix-save-for-LDAP
Fix: Ensure Consistent LDAP Settings Save
2025-06-08 13:03:17 +04:00
sasidhar 6860dec08f fix: properly sign out user on trusted email mismatch
When using trusted email header authentication, properly sign out the user
when the logged-in user's email doesn't match the trusted email header value.
This ensures proper session cleanup when the OAuth server changes the
authenticated user.

- Add response parameter to get_current_user function
- Delete JWT token cookie on email mismatch
- Delete OAuth token cookie if present
- Force re-authentication with 401 error
2025-06-08 14:26:55 +05:30
sasidhar 61f49ff580 fix: ensure trusted email header matches logged-in user
When using trusted email header authentication, verify that the logged-in user's
email matches the value in the header. This prevents session conflicts when the
OAuth server changes the authenticated user.

- Move trusted email verification after user existence check
- Raise 401 if email mismatch is detected
- Only perform verification when WEBUI_AUTH_TRUSTED_EMAIL_HEADER is enabled
2025-06-08 14:16:10 +05:30
Rodrigo Agundez 41e28a9f6f Use standardized procedure to save and load images 2025-06-08 16:26:58 +08:00
Rodrigo Agundez 168997a58e Return tuple consistently and strip potential left string 2025-06-08 16:26:08 +08:00
Silentoplayz a6c7ddcb28 fix: Hidden delete button for playground chat messages 2025-06-08 04:16:41 -04:00
Silentoplayz 67e930e1d2 Fix: Ensure Consistent LDAP Settings Save 2025-06-08 02:17:41 -04:00
Silentoplayz 92c7549b06 fix: remove newline 2025-06-08 01:50:11 -04:00
seiran e97394958e Merge branch 'dev' into main 2025-06-08 05:58:24 +08:00
Diwakar Singh Maurya d600e60776 fix: save Document settings only when Save button is pressed 2025-06-07 15:39:13 +00:00
EntropyYue fd4c42b42e feat: add support for WEBUI_SECRET_KEY_FILE 2025-06-07 23:14:21 +08:00
Diwakar Singh Maurya 871efb4ad9 feat: add langchain markdown document splitter 2025-06-07 06:02:53 +00:00
Tim Jaeryang Baek da5fbc0e39 Merge pull request #14743 from PeterDaveHello/Improve-zh-TW-locale
i18n: improve zh-TW Traditional Chinese locale
2025-06-06 21:26:57 +04:00
Tim Jaeryang Baek 1ad4f7a244 Merge pull request #14736 from SadmL/patch-1
[i18n] Russian locale update
2025-06-06 17:46:14 +04:00
Link [Связной] c0f65ec28a [i18n] Russian locale update 2025-06-06 16:28:36 +03:00
Sine Jespersen 6fd91e58cf add highContrastMode to type Settings 2025-06-06 13:38:35 +02:00
Timothy Jaeryang Baek b433d30c26 chore: format 2025-06-06 15:27:05 +04:00
Tim Jaeryang Baek 1ff7cb7d2f Merge pull request #14720 from itk-dev/feature/accessibible-chat-buttons
Feat: accessibible chat buttons
2025-06-06 12:55:46 +04:00
Sine Jespersen 9c38183689 reindent xmlns 2025-06-06 10:52:13 +02:00
Sine Jespersen fd97882fca add aria labels to buttons that only contains decorative svgs 2025-06-06 10:46:50 +02:00
Sine Jespersen 2b4d4d2770 remove aria-label from tooltip, as tippy handles this out of the box 2025-06-06 10:32:29 +02:00
Sine Jespersen dc60bd2816 add aria-hidden to svg, as these are decorative 2025-06-06 10:32:01 +02:00
Timothy Jaeryang Baek 0b84b22b4e refac: chat item key 2025-06-05 22:38:46 +04:00
Timothy Jaeryang Baek 870c23f69d refac 2025-06-05 22:37:14 +04:00
Timothy Jaeryang Baek 12cfb7b0f0 refac: admin evaluation pages 2025-06-05 21:43:43 +04:00
Tim Jaeryang Baek 37f1d47a06 Merge pull request #14683 from headwAI-GmbH/more-ideomatic-matrix
chore: More ideomatic docker-build matrix
2025-06-05 20:48:31 +04:00
Tim Jaeryang Baek 14a890a0a5 Merge pull request #14687 from SadmL/patch-2
[i18n] Russian locale update
2025-06-05 20:43:37 +04:00
Tim Jaeryang Baek f155abc63c Merge pull request #14700 from silentoplayz/better-settings-search
feat: Enhance Settings Modal Search & Admin Settings Button Placement
2025-06-05 20:43:19 +04:00
Tim Jaeryang Baek f6f3360ef7 Merge pull request #14695 from TiancongLx/dev
i18n: update zh-TW
2025-06-05 20:42:04 +04:00
Tim Jaeryang Baek 99fe23ef66 Merge pull request #14694 from Bouby308/patch-1
fix: return json for successful userSignOut
2025-06-05 20:40:10 +04:00
Tim Jaeryang Baek 92c5773682 Merge pull request #14696 from ShirasawaSama/fix_old_ios_device_voice_recording
fix: fix old iOS device voice recording
2025-06-05 20:38:55 +04:00
Silentoplayz 3c5ae1f62e chore: Also define the standard property 'appearance' for compatibilitycss(vendorPrefix) 2025-06-05 12:02:31 -04:00
Silentoplayz 543ed432b1 Update SettingsModal.svelte 2025-06-05 11:42:05 -04:00
Peter Dave Hello a8b61204b4 i18n: improve zh-TW Traditional Chinese locale 2025-06-05 23:00:32 +08:00
Silentoplayz ec0124b521 Update SettingsModal.svelte 2025-06-05 10:48:33 -04:00
Shirasawa 07b2ecf6aa fix: fix old iOS device voice recording 2025-06-05 20:22:20 +08:00
Tiancong Li e7b86cc852 i18n: update zh-TW 2025-06-05 19:58:29 +08:00
Bouby308 eb3c080cce fix: return json for successful userSignOut
return json when signout request is successful so that frontend can get redirect url with res?.redirect_url on signout
2025-06-05 19:20:27 +08:00
Link [Связной] b3e328ed04 Update translation.json 2025-06-05 12:02:46 +03:00
Link [Связной] 6c8202d87d Update translation.json 2025-06-05 12:01:59 +03:00
Link [Связной] 936c3efe5e [i18n] Russian locale update 2025-06-05 11:55:46 +03:00
guenhter 0fe82d119f chore: make usage of matrix more ideomatic 2025-06-05 06:55:10 +02:00
Timothy Jaeryang Baek 5e35aab292 chore: format 2025-06-05 01:12:28 +04:00
Timothy Jaeryang Baek f17bfb3adb refac 2025-06-05 01:11:31 +04:00
Timothy Jaeryang Baek 00e4391a6f refac 2025-06-05 01:05:33 +04:00
Timothy Jaeryang Baek 93236cead0 refac 2025-06-05 00:46:39 +04:00
Timothy Jaeryang Baek ed1db62b12 refac 2025-06-05 00:44:15 +04:00
Timothy Jaeryang Baek ab36b8aeae refac: embeddings endpoint 2025-06-05 00:37:31 +04:00
Timothy Jaeryang Baek b02a3da4da refac: styling 2025-06-05 00:32:48 +04:00
Tim Jaeryang Baek 7c4f261aa2 Merge pull request #14616 from Davixk/feat/new-perplexity-options
feat: add Perplexity AI model and search context usage configuration options
2025-06-05 00:28:00 +04:00
Tim Jaeryang Baek 14e158fde9 Merge pull request #14667 from hdnh2006/main
feat: OpenAI-compatible `/api/embeddings` endpoint with provider-agnostic OpenWebUI architecture
2025-06-05 00:25:56 +04:00
Tim Jaeryang Baek f50755ccd9 Merge pull request #14659 from vaclcer/vaclavs-picture-description
**fix**: do_picture_description feature when using Docling as an external document parser
2025-06-05 00:24:10 +04:00
henry 04da2b05f6 convert embedding function name to be more consistence 2025-06-04 18:24:27 +02:00
Vaclav Cerny 9772c18b20 fix(loader): remove deprecated picture description configuration 2025-06-04 17:21:44 +02:00
henry cc12e9e1a3 Ollama embeddings adapted to pydantic 2025-06-04 17:06:38 +02:00
henry 3ddebefca2 openai embeddings function modified 2025-06-04 16:13:53 +02:00
henry 8f6c3f46d6 payload and response modifed for compatibility 2025-06-04 16:11:40 +02:00
henry e0769c6a1f new embedding.py added for handling openai and ollama embedding 2025-06-04 16:09:39 +02:00
Henry 415114504f Merge branch 'open-webui:main' into main 2025-06-04 15:37:33 +02:00
Tim Jaeryang Baek 94356dc3ca Merge pull request #14662 from SadmL/patch-2
[i18n] Russian locale update
2025-06-04 17:01:06 +04:00
Link [Связной] 7f04045697 [i18n] Russian locale update 2025-06-04 15:58:15 +03:00
Vaclav Cerny 0b3719cc65 This commit fixes do_picture_description feature when using Docling as an external document parser.
Merge branch 'dev' into vaclavs-picture-description
2025-06-04 14:29:41 +02:00
Vaclav Cerny c71236ba07 feat(loader): enhance picture description prompt for improved detail and clarity 2025-06-04 14:25:31 +02:00
Vaclav Cerny c4278f4784 fix description vs classification mismatch 2025-06-04 14:13:00 +02:00
Timothy Jaeryang Baek bbafeca495 refac: reserved __user__ param format 2025-06-04 15:53:07 +04:00
Tim Jaeryang Baek dcb71c2292 Merge pull request #14644 from Davixk/feat/enforce-lf-eol
refac: enforce consistent line endings across all platforms
2025-06-04 15:44:40 +04:00
Timothy Jaeryang Baek 91e826cc27 refac 2025-06-04 15:43:23 +04:00
Timothy Jaeryang BaekandRodrigo Agundez 9964ad0a5b refac: auth cache dir
Co-Authored-By: Rodrigo Agundez <rragundez@users.noreply.github.com>
2025-06-04 15:21:08 +04:00
Vaclav Cerny 8644e81a1c feat(loader): add picture description configuration for DoclingLoader 2025-06-04 12:34:39 +02:00
Tim Jaeryang Baek 7ef2345ba6 Merge pull request #14642 from ayanahye/fix-chat-title-error-message
fix: error message on chat title edit
2025-06-04 14:09:20 +04:00
Tim Jaeryang Baek 00b16cbaf2 Merge pull request #14645 from qingchunnh/Update_zh-CN-2564
i18n: Update & Improve zh-CN
2025-06-04 14:08:43 +04:00
Tim Jaeryang Baek 2b926a373c Merge pull request #14639 from silentoplayz/add-clone-to-cloned-prompts
fix: append `(Clone)` to title and `-clone` to id when cloning a workspace prompt
2025-06-04 14:08:30 +04:00
Tim Jaeryang Baek 5df4f98fc3 Merge pull request #14640 from silentoplayz/locales-is-undefined-typeerror-fix
fix: Uncaught (in promise) TypeError: locales is undefined
2025-06-04 14:07:38 +04:00
qingchun 717d58116a i18n: Improve zh-CN 2025-06-04 12:46:14 +08:00
qingchun 3e02618278 i18n: Update & Improve zh-CN 2025-06-04 12:33:07 +08:00
Dave 5fb5daf07f refac: standardize formatting in .gitattributes for consistency 2025-06-04 06:32:06 +02:00
Dave 059327cd19 refac: update .gitattributes and .prettierrc for consistent line endings 2025-06-04 06:27:25 +02:00
Ayana H fd53c737f6 update spacing 2025-06-03 19:49:52 -07:00
ayana dc05ccb73f fix: error message on chat title edit 2025-06-03 19:27:28 -07:00
Timothy Jaeryang Baek aeafcc7903 refac: styling 2025-06-04 02:51:42 +04:00
Silentoplayz 86111ad44a fix: Uncaught (in promise) TypeError: locales is undefined in Collapsible.svelte 2025-06-03 18:50:16 -04:00
Timothy Jaeryang Baek ec81e98fa2 refac 2025-06-04 02:46:30 +04:00
Timothy Jaeryang Baek fe69189dc9 refac: styling 2025-06-04 02:36:31 +04:00
Silentoplayz 5b4f81ce11 Update PromptEditor.svelte 2025-06-03 17:25:33 -04:00
Silentoplayz 6ebf522a27 Update PromptEditor.svelte 2025-06-03 17:24:54 -04:00
Silentoplayz 2a6d2392a0 fix: append (Clone) to cloned prompt title and clone to prompt id 2025-06-03 17:19:24 -04:00
Timothy Jaeryang Baek 25157ca6ba chore: format 2025-06-03 22:51:59 +04:00
Timothy Jaeryang Baek c01e29c47f refac: model tools selection not working 2025-06-03 22:04:13 +04:00
Tim Jaeryang Baek bb689b9480 Merge pull request #14630 from Yu-QX/main
fix: Rendering problem with Chinese quotation mark in **bold** & *italic* (after Chinese parentheses issue).
2025-06-03 21:59:12 +04:00
Timothy Jaeryang Baek db836c53f6 refac 2025-06-03 21:57:18 +04:00
Timothy Jaeryang Baek 7ca35068a6 refac: styling 2025-06-03 21:55:40 +04:00
Timothy Jaeryang Baek bbedc50f00 refac 2025-06-03 21:52:58 +04:00
Timothy Jaeryang Baek d2b5a1614a refac 2025-06-03 21:52:25 +04:00
Timothy Jaeryang Baek abae0a837f refac: styling 2025-06-03 21:51:12 +04:00
YuQX 4eef6f432f fix: Rendering problem with Chinese quotation mark in **bold** & *italic* (after Chinese parentheses issue). 2025-06-03 23:30:51 +08:00
Timothy Jaeryang Baek 0a1ff4a8fa refac 2025-06-03 18:49:42 +04:00
Timothy Jaeryang Baek 931e5ffa6e refac 2025-06-03 18:49:03 +04:00
Timothy Jaeryang Baek 185249623b feat: follow ups backend integration 2025-06-03 18:47:49 +04:00
YuQX ca894df2bd Merge pull request #2 from open-webui/main
Upgrade to v0.6.13
2025-06-03 22:36:28 +08:00
Timothy Jaeryang Baek 9e49fbc8bf feat: follow ups 2025-06-03 18:07:29 +04:00
Timothy Jaeryang Baek f8b941fb96 refac 2025-06-03 17:24:31 +04:00
Timothy Jaeryang Baek 2fee89dfa6 doc: readme 2025-06-03 16:49:24 +04:00
Timothy Jaeryang Baek 4d364e2967 refac: remove msg from known type 2025-06-03 16:27:28 +04:00
Tim Jaeryang Baek 8bdefa5481 Merge pull request #14592 from ER-EPR/main
Feat: Add a new docker tag cu126 to maintain support for NVIDIA cuda capability 7.0 and below (e.g. V100, GTX1080)
2025-06-03 12:48:15 +04:00
Tim Jaeryang Baek 34a8b95cc3 Merge pull request #14622 from Zyfax/patch-2
fix: remove trailing slash form test connection api url
2025-06-03 12:43:31 +04:00
Zyfax e353f15a3c fix: remove ending slash form test connection url
Removes the ending slash when testing openai and ollama api url.
2025-06-03 09:11:01 +02:00
Dave 77b357c73b fix: update label for search context usage to clarify its purpose 2025-06-03 00:27:07 +02:00
Dave 96e9bfe0e5 feat: add Perplexity model and search context usage configuration options 2025-06-03 00:19:08 +02:00
Tim Jaeryang Baek 3c32d2cada Merge pull request #14539 from PVBLIC-F/refac/mistral
perf mistral.py Enhance for Overall Speed and Efficiency
2025-06-02 23:52:59 +04:00
Timothy Jaeryang Baek 3da1802eec fix: message delete issue 2025-06-02 23:25:38 +04:00
Tim Jaeryang Baek 0ebe35c571 Merge pull request #14532 from PVBLIC-F/refac/pinecone
perf pinecone.py Improve Performance and Maintainability Using Current Best Practices
2025-06-02 23:12:48 +04:00
Tim Jaeryang Baek 29d6eb642f Merge pull request #14555 from diwakar-s-maurya/patch-8
feat: Add route for each tab in Settings
2025-06-02 22:36:36 +04:00
Tim Jaeryang Baek 7536a94585 Merge pull request #14552 from expruc/he-tr
i18n: updated hebrew locale
2025-06-02 21:42:45 +04:00
Tim Jaeryang Baek 89dcce0df2 Merge pull request #14553 from PeterDaveHello/Improve-zh-TW-locale
i18n: update and improve zh-TW Traditional Chinese locale
2025-06-02 21:40:01 +04:00
Tim Jaeryang Baek 6e2bffee69 Merge pull request #14566 from open-webui/dependabot/pip/backend/dev/uvicorn-standard--0.34.2
build(deps): bump uvicorn[standard] from 0.34.0 to 0.34.2 in /backend
2025-06-02 21:38:39 +04:00
Tim Jaeryang Baek 84c06aa582 Merge pull request #14565 from open-webui/dependabot/pip/backend/dev/pillow-11.2.1
build(deps): bump pillow from 11.1.0 to 11.2.1 in /backend
2025-06-02 21:37:45 +04:00
Tim Jaeryang Baek 281b74f4b4 Merge pull request #14563 from open-webui/dependabot/pip/backend/dev/azure-ai-documentintelligence-1.0.2
build(deps): bump azure-ai-documentintelligence from 1.0.0 to 1.0.2 in /backend
2025-06-02 21:37:37 +04:00
Tim Jaeryang Baek 816e35bd79 Merge pull request #14562 from open-webui/dependabot/pip/backend/dev/validators-0.35.0
build(deps): bump validators from 0.34.0 to 0.35.0 in /backend
2025-06-02 21:37:28 +04:00
Tim Jaeryang Baek 0469120709 Merge pull request #14573 from open-webui/dependabot/npm_and_yarn/dev/katex-0.16.22
build(deps): bump katex from 0.16.21 to 0.16.22
2025-06-02 21:37:19 +04:00
ER-EPR bf3897ec40 Merge pull request #1 from ER-EPR/ER-EPR-cuda126-1
Add cuda126 image in docker-build.yaml
2025-06-02 17:45:39 +08:00
ER-EPR d649db4ef7 Add cuda126 image in docker-build.yaml
add USE_CUDA_VER=cu126 version to support cuda capability 7.0 and below
2025-06-02 17:44:17 +08:00
dependabot[bot] 28db03e4f9 build(deps): bump katex from 0.16.21 to 0.16.22
Bumps [katex](https://github.com/KaTeX/KaTeX) from 0.16.21 to 0.16.22.
- [Release notes](https://github.com/KaTeX/KaTeX/releases)
- [Changelog](https://github.com/KaTeX/KaTeX/blob/main/CHANGELOG.md)
- [Commits](https://github.com/KaTeX/KaTeX/compare/v0.16.21...v0.16.22)

---
updated-dependencies:
- dependency-name: katex
  dependency-version: 0.16.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-01 02:49:05 +00:00
dependabot[bot] 26549244fc build(deps): bump uvicorn[standard] from 0.34.0 to 0.34.2 in /backend
Bumps [uvicorn[standard]](https://github.com/encode/uvicorn) from 0.34.0 to 0.34.2.
- [Release notes](https://github.com/encode/uvicorn/releases)
- [Changelog](https://github.com/encode/uvicorn/blob/master/docs/release-notes.md)
- [Commits](https://github.com/encode/uvicorn/compare/0.34.0...0.34.2)

---
updated-dependencies:
- dependency-name: uvicorn[standard]
  dependency-version: 0.34.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-01 02:28:37 +00:00
dependabot[bot] 3ccd2364ba build(deps): bump pillow from 11.1.0 to 11.2.1 in /backend
Bumps [pillow](https://github.com/python-pillow/Pillow) from 11.1.0 to 11.2.1.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/11.1.0...11.2.1)

---
updated-dependencies:
- dependency-name: pillow
  dependency-version: 11.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-01 02:28:34 +00:00
dependabot[bot] 1131a61b8e build(deps): bump azure-ai-documentintelligence in /backend
Bumps [azure-ai-documentintelligence](https://github.com/Azure/azure-sdk-for-python) from 1.0.0 to 1.0.2.
- [Release notes](https://github.com/Azure/azure-sdk-for-python/releases)
- [Changelog](https://github.com/Azure/azure-sdk-for-python/blob/main/doc/esrp_release.md)
- [Commits](https://github.com/Azure/azure-sdk-for-python/compare/azure-ai-documentintelligence_1.0.0...azure-ai-documentintelligence_1.0.2)

---
updated-dependencies:
- dependency-name: azure-ai-documentintelligence
  dependency-version: 1.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-01 02:28:27 +00:00
dependabot[bot] 2f3d4622db build(deps): bump validators from 0.34.0 to 0.35.0 in /backend
Bumps [validators](https://github.com/python-validators/validators) from 0.34.0 to 0.35.0.
- [Release notes](https://github.com/python-validators/validators/releases)
- [Changelog](https://github.com/python-validators/validators/blob/master/CHANGES.md)
- [Commits](https://github.com/python-validators/validators/compare/0.34.0...0.35.0)

---
updated-dependencies:
- dependency-name: validators
  dependency-version: 0.35.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-01 02:28:18 +00:00
Peter Dave Hello 96d8a52fe3 i18n: update and improve zh-TW Traditional Chinese locale 2025-06-01 04:08:33 +08:00
Diwakar Singh Maurya 1ddc784af9 feat: Add route for each tab in Settings 2025-05-31 19:18:05 +00:00
expruc 185224e4e1 Locale: updated he translation 2025-05-31 21:37:42 +03:00
Timothy Jaeryang Baek 2c15f8e676 refac 2025-05-31 15:07:28 +04:00
Timothy Jaeryang Baek 4e82c44f3e refac 2025-05-31 15:04:38 +04:00
Timothy Jaeryang Baek e41e375aab refac: role update ui 2025-05-31 15:00:27 +04:00
Tim Jaeryang Baek 343dac91c9 Merge pull request #14522 from SadmL/patch-2
[i18n] Russian locale update
2025-05-31 14:42:52 +04:00
PVBLIC Foundation cf3635ba25 Update mistral.py
1. Intelligent Error Handling
Added _is_retryable_error() method to distinguish retryable vs non-retryable errors
Prevents unnecessary retries on client errors (4xx) that won't succeed
Caps retry delay at 30 seconds to prevent excessive waiting
2. Optimized Timeout Configuration
Upload: Capped at 2 minutes (was using full 5-minute timeout)
URL requests: 30 seconds (should be fast)
OCR processing: Full timeout (can take time)
Cleanup: 30 seconds (should be quick)
3. Enhanced Connection Pool
Increased connection limits: 20 total, 10 per host
Longer DNS cache TTL (10 minutes vs 5 minutes)
Increased keepalive timeout (60s vs 30s)
Added async DNS resolver for better performance
Granular timeout controls (connect, read, total)
4. Concurrency Control for Batch Processing
Added semaphore-based concurrency control (default: 5 concurrent)
Prevents API overwhelming while maintaining throughput
Configurable concurrency limit per workload
5. Memory Efficient Result Processing
Early exit for empty content validation
Better error metadata for debugging
Added content length tracking
Streamlined page processing logic
6. General Performance Improvements
Better error logging with truncated responses
Optimized metadata creation
Improved debug logging efficiency
2025-05-30 20:06:29 -07:00
PVBLIC Foundation 66bde32623 Update pinecone.py 2025-05-30 18:47:23 -07:00
sanae artoria cbc514e681 The translation might be confusing, in some cases for example tooltips over a radio button 2025-05-31 01:13:55 +08:00
PVBLIC Foundation 4ecf2a8685 Update pinecone.py
May 2025 Latest Pinecone Best Practices
2025-05-30 09:33:57 -07:00
Link [Связной] 5555d889d5 Update translation.json 2025-05-30 11:55:22 +03:00
Link [Связной] 7dde16d0ac [i18n] Russian locale update 2025-05-30 11:52:33 +03:00
Tim Jaeryang Baek 53764fe648 Merge pull request #14486 from open-webui/dev
0.6.13
2025-05-30 01:37:21 +04:00
Timothy Jaeryang Baek 235489cfc5 refac 2025-05-30 01:33:49 +04:00
Timothy Jaeryang Baek b2befd486f refac 2025-05-30 01:24:54 +04:00
Timothy Jaeryang Baek 9306ae5972 refac 2025-05-30 01:19:56 +04:00
Timothy Jaeryang Baek 036ce12dd9 doc: changelog 2025-05-30 01:14:38 +04:00
Tim Jaeryang Baek d6c3f93cfd Merge pull request #14509 from r0mdau/fix/audio-format
fix: only trust codec_name for audio conversion
2025-05-30 01:12:22 +04:00
Romain Dauby b12a493fe5 fix: only trust codec_name for audio conversion
Some files have .wav extension with incompatible OpenAI codec
2025-05-29 16:57:23 -04:00
Timothy Jaeryang Baek f4827f0c18 chore: format 2025-05-30 00:35:59 +04:00
Timothy Jaeryang Baek e1e2c096e2 refac: PLEASE follow existing convention 2025-05-30 00:34:18 +04:00
Tim Jaeryang Baek ff353578db Merge pull request #14370 from daw/feat/add-azure-openai-embeddings-option
feat:Add Azure OpenAI embedding support
2025-05-30 00:18:55 +04:00
Timothy Jaeryang Baek be989f3645 refac: better memory error handling 2025-05-30 00:12:28 +04:00
Timothy Jaeryang Baek 4c45d67677 refac/fix: memory 2025-05-30 00:10:52 +04:00
Timothy Jaeryang Baek 59768e34f4 refac 2025-05-30 00:04:13 +04:00
Timothy Jaeryang Baek 4371d2c5a5 enh: better custom param handling 2025-05-29 23:32:14 +04:00
Timothy Jaeryang Baek 9be22cb637 fix: prompt access control 2025-05-29 23:21:56 +04:00
Tim Jaeryang Baek f95f51530b Merge pull request #14488 from TiancongLx/dev
i18n: update zh-TW
2025-05-29 13:35:10 +04:00
Tiancong Li c3e8e1cbf1 i18n: update zh-TW 2025-05-29 17:18:27 +08:00
Tim Jaeryang Baek 2327c32a74 Merge pull request #14485 from jackthgu/update-korean-translation
i18n: Korean locale update
2025-05-29 13:16:00 +04:00
Taehong Gu a214e63cab Update Korean translation - Documentation 2025-05-29 18:12:54 +09:00
Tim Jaeryang Baek f1507f2458 Merge pull request #14472 from Davixk/fix/chat-loading-error
fix: Chat page fails to load on undefined message
2025-05-29 13:07:08 +04:00
Timothy Jaeryang Baek 255367934b doc: typo 2025-05-29 13:02:12 +04:00
Tim Jaeryang Baek bd4e010c76 Merge pull request #14477 from qingchunnh/Update_zh-CN-25529
i18n: Update & Improve zh-CN
2025-05-29 13:00:14 +04:00
Tim Jaeryang Baek 155518e788 Merge pull request #14478 from Kylapaallikko/dev
i18n: Update fi-FI translation
2025-05-29 12:59:52 +04:00
Timothy Jaeryang Baek d43bbcae28 refac/fix: open webui params handling 2025-05-29 12:57:58 +04:00
Kylapaallikko 309380b098 Update fi-FI translation.json 2025-05-29 09:18:42 +03:00
qingchun 2c723e1f28 i18n: Update & Improve zh-CN 2025-05-29 13:40:39 +08:00
Dave 01eedd36bc Handles undefined message case in message list creation
Prevents potential errors by returning an empty list if the
specified message ID does not exist in the history. This
enhancement ensures robustness in scenarios where a message
ID may be missing, avoiding further processing and potential
exceptions.
2025-05-29 06:19:13 +02:00
Tim Jaeryang Baek ba0088f39b Merge pull request #14441 from open-webui/dev
0.6.12
2025-05-29 03:59:24 +04:00
Timothy Jaeryang Baek 661625f362 doc: changelog 2025-05-29 03:56:42 +04:00
Timothy Jaeryang Baek 30b7ab3591 refac 2025-05-29 03:48:07 +04:00
Timothy Jaeryang Baek 7dc7d5c028 refac: PLEASE FOLLOW EXISTING CONVENTION 2025-05-29 03:47:02 +04:00
Timothy Jaeryang Baek 2c31f5c725 doc: readme 2025-05-29 03:41:38 +04:00
Timothy Jaeryang Baek a6864db8ec chore: format 2025-05-29 03:37:58 +04:00
Timothy Jaeryang Baek 21f85e63bf refac: styling 2025-05-29 03:37:13 +04:00
Timothy Jaeryang Baek 9220afe7b3 feat: custom advanced params 2025-05-29 03:33:11 +04:00
Timothy Jaeryang Baek bb4115fa0e refac: allow all params 2025-05-29 02:56:37 +04:00
Timothy Jaeryang Baek 551597b9cc chore: format 2025-05-29 02:36:33 +04:00
Timothy Jaeryang Baek cb4299eb98 refac 2025-05-29 02:33:40 +04:00
Tim Jaeryang Baek 042c37ea34 Merge pull request #14311 from Hisma/marker-api-content-extraction
feat: Marker api content extraction support
2025-05-29 02:21:13 +04:00
Timothy Jaeryang Baek 85a384fab5 enh: load tool by url 2025-05-29 02:08:54 +04:00
Timothy Jaeryang Baek 4461122a0e fix: /api/v1/retrieval/query/collection endpoint 2025-05-28 18:45:47 +04:00
YuQX 24be96a6d4 Merge pull request #1 from open-webui/main
Upgrade to v0.6.11
2025-05-28 22:24:33 +08:00
Tim Jaeryang Baek c9f983f644 Merge pull request #14445 from NotYuSheng/chore/duplicate-css-elements
chore: removed duplicate css elements
2025-05-28 17:58:59 +04:00
Timothy Jaeryang Baek cc6cbf53e8 refac 2025-05-28 17:36:05 +04:00
Timothy Jaeryang Baek 4279762ea4 refac 2025-05-28 16:36:23 +04:00
notyusheng efedb7ab1f chore: removed duplicate css elements 2025-05-28 08:31:11 -04:00
Timothy Jaeryang Baek 32135a29bb fix: image preview/download 2025-05-28 15:57:40 +04:00
Timothy Jaeryang Baek 8a74bdce37 fix: message input issue 2025-05-28 15:36:04 +04:00
Timothy Jaeryang Baek 3d9a430927 refac: styling 2025-05-28 14:28:39 +04:00
Timothy Jaeryang Baek a27a095884 refac 2025-05-28 14:27:34 +04:00
Tim Jaeryang Baek 596298fa46 Merge pull request #14405 from SadmL/patch-2
[i18n] Russian locale update
2025-05-28 14:23:33 +04:00
Timothy Jaeryang Baek bf7a18a0f8 refac 2025-05-28 02:10:54 +04:00
Timothy Jaeryang Baek d81886e315 refac 2025-05-28 01:42:42 +04:00
Timothy Jaeryang Baek 7effb04782 refac 2025-05-28 01:41:49 +04:00
Timothy Jaeryang Baek f5fefb49d5 refac 2025-05-28 01:38:24 +04:00
Timothy Jaeryang Baek e4a53e0a3c refac 2025-05-28 01:34:53 +04:00
Link [Связной] e861ba5520 [i18n] Russian locale update 2025-05-27 16:28:09 +03:00
Tim Jaeryang Baek 100a764293 Merge pull request #14402 from torisetxd/parallelized-model-fetching
perf: Parallelize base model fetching
2025-05-27 16:56:44 +04:00
Timothy Jaeryang Baek 1d216b82ba refac 2025-05-27 16:48:17 +04:00
toriset 9eccce2444 Added proper type hints to new functions
Forgot about that...
2025-05-27 15:44:20 +03:00
toriset 27de981246 Parallelize base model fetching 2025-05-27 15:35:16 +03:00
Timothy Jaeryang Baek 40bea00e3d refac 2025-05-27 16:06:00 +04:00
Timothy Jaeryang Baek 8609471eeb Revert "Merge pull request #14379 from aleixdorca/dev"
This reverts commit cb135c370a, reversing
changes made to 8cf578d9fd.
2025-05-27 16:00:19 +04:00
Tim Jaeryang Baek cb135c370a Merge pull request #14379 from aleixdorca/dev
i18n: Update Catalan translation.json
2025-05-27 15:59:37 +04:00
Tim Jaeryang Baek 8cf578d9fd Merge pull request #14368 from qingchunnh/Update_zh-CN-25527
i18n: Update & Improve zh-CN
2025-05-27 15:59:29 +04:00
Tim Jaeryang Baek 1aac41d6c3 Merge pull request #14400 from torisetxd/capabilities-fix
fix: Improve mobile styling for capabilities in workspace models
2025-05-27 15:59:16 +04:00
toriset 20403c9df7 Match capabilities styling to other settings in workspace models 2025-05-27 14:39:21 +03:00
Timothy Jaeryang Baek b944acd3ff refac: function cache 2025-05-27 14:39:35 +04:00
Tim Jaeryang Baek 1ec9e42c1e Merge pull request #14383 from TiancongLx/dev
i18n: complete and improve zh-TW
2025-05-27 13:56:51 +04:00
Tim Jaeryang Baek 5989e9bf75 Merge pull request #14387 from Zyfax/patch-1
fix: tts read aloud
2025-05-27 13:56:40 +04:00
Tim Jaeryang Baek 75b2e4a659 Merge pull request #14392 from hurxxxx/fix/improve-loading-functions
perf: Prevent duplicate function loads with caching helper and refactor
2025-05-27 13:56:13 +04:00
Gunwoo Hur 14c3d0c2d1 Prevent duplicate function module loads with caching helper and refactor 2025-05-27 18:08:58 +09:00
Tim Jaeryang Baek 737dc7797c Merge pull request #14390 from open-webui/dev
Dev
2025-05-27 12:59:28 +04:00
Timothy Jaeryang Baek eb14bfbcdf refac: bug report template 2025-05-27 12:59:01 +04:00
Zyfax 4995bfe571 fix: tts read aloud
Change: const content = removeAllDetails(content); to const content = removeAllDetails(message.content);

Error: Uncaught (in promise) ReferenceError: can't access lexical declaration 'z' before initialization.
ResponseMessage.svelte:215:42
2025-05-27 10:52:26 +02:00
Tiancong Li 098ec43413 i18n: update zh-TW 2025-05-27 16:16:09 +08:00
Aleix Dorca fcb03df9ce Update catalan translation.json 2025-05-27 09:02:42 +02:00
Hisma 19bb3589ee fix: add Datalab Marker API to Content Extraction Engine Dropdown 2025-05-27 02:24:53 -04:00
Hisma e12a79c0e2 fix: handle json output format correctly 2025-05-27 01:12:03 -04:00
Hisma a9405cc101 feat: Marker api content extraction support 2025-05-27 00:44:07 -04:00
qingchun 7988e0c6e2 i18n: Improve zh-CN 2025-05-27 11:25:12 +08:00
qingchun 14bf579e9c i18n: Fix zh-CN 2025-05-27 10:25:02 +08:00
qingchun fc2202ad76 i18n: Update zh-CN 2025-05-27 09:19:35 +08:00
Timothy Jaeryang Baek a2afd6f645 refac 2025-05-27 03:06:01 +04:00
Tim Jaeryang Baek b8e16211b9 Merge pull request #14364 from open-webui/dev
fix: image generation
2025-05-27 02:48:45 +04:00
Timothy Jaeryang Baek efb54aa2e4 fix: image generation 2025-05-27 02:48:22 +04:00
Tim Jaeryang Baek 9faa4c6a4c Merge pull request #14194 from open-webui/dev
0.6.11
2025-05-27 02:27:09 +04:00
Timothy Jaeryang Baek 5c74e56bd0 chore: format 2025-05-27 02:18:43 +04:00
Timothy Jaeryang Baek 8609fe7ab4 i18n: french 2025-05-27 02:18:02 +04:00
Tim Jaeryang Baek 1cb8fa0f03 Merge pull request #14362 from PVBLIC-F/fix/chat-engagement-critical
Fix/chat engagement critical
2025-05-27 02:17:34 +04:00
Timothy Jaeryang Baek a681d5c60e doc: changelog 2025-05-27 02:16:43 +04:00
cheadings71 256034e285 Update misc.py
Before fix: Chat engagement failed with TypeError and KeyError
After fix: Chat works smoothly with automatic title generation and proper history
2025-05-26 14:55:48 -07:00
cheadings71 d414662d23 fix: resolve chat engagement TypeError - Fix get_message_list() to return [] instead of None - Fix middleware to use correct metadata message_id - Add safe fallback for missing role field - Ensure assistant messages include role field 2025-05-26 14:35:09 -07:00
Timothy Jaeryang Baek 940a437631 refac 2025-05-27 01:16:11 +04:00
Timothy Jaeryang Baek aaff204e7b refac 2025-05-27 00:56:59 +04:00
Timothy Jaeryang Baek e68a3d28b5 refac 2025-05-27 00:40:55 +04:00
Timothy Jaeryang Baek 090619c27c refac 2025-05-27 00:29:16 +04:00
Timothy Jaeryang Baek 9c120affb6 chore: format 2025-05-27 00:21:23 +04:00
Timothy Jaeryang Baek 2c7ccc69fe enh: allow custom openapi json url 2025-05-27 00:20:47 +04:00
Timothy Jaeryang Baek a38e44e870 enh: external tool server custom name/description support 2025-05-27 00:10:33 +04:00
Timothy Jaeryang Baek 20f54616e7 chore: format 2025-05-26 23:55:39 +04:00
Timothy Jaeryang Baek b4caad928e feat: load function from url 2025-05-26 23:52:22 +04:00
Timothy Jaeryang Baek 7d756982c2 refac 2025-05-26 23:22:37 +04:00
Tim Jaeryang Baek 6062174602 Merge pull request #14228 from suleimanelkhoury/s3-tags-allowed-characters
fix: S3 allowed characters in Tags.
2025-05-26 22:43:21 +04:00
Timothy Jaeryang Baek 9a5febc2d1 feat: copy model link 2025-05-26 22:29:03 +04:00
Timothy Jaeryang Baek 2d5b82df8c enh: include sources field in non-streaming response 2025-05-26 22:22:37 +04:00
Timothy Jaeryang Baek ffa51ece0c refac: pinned chat endpoint 2025-05-26 22:15:21 +04:00
Timothy Jaeryang BaekandClassic298 a0d034fa7c feat: Bypass file creation with CTRL + SHIFT + V
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2025-05-26 22:10:35 +04:00
Timothy Jaeryang Baek fc5dfd3536 refac 2025-05-26 22:02:40 +04:00
Tim Jaeryang Baek 5d7c89964c Merge pull request #14314 from fl0w1nd/dev
fix: Correctly handle toggle filters to prevent unintended activation
2025-05-26 21:58:57 +04:00
Timothy Jaeryang Baek caa5ad44d4 refac 2025-05-26 19:53:29 +04:00
Tim Jaeryang Baek 040f29d058 Merge pull request #14316 from Yu-QX/main
fix: Handle special cases of LLM output not following markdown syntax
2025-05-26 19:30:48 +04:00
Yu QX bb62bd9c1f Extracting the traversal. 2025-05-26 21:04:08 +08:00
Yu QX b9f74a66b2 Seperate into processChineseContent 2025-05-26 20:43:21 +08:00
Yu QX 254dd4246e Integrate file into index.ts. 2025-05-26 19:35:28 +08:00
Timothy Jaeryang BaekandJason Baker 4da75a9e78 feat: GZip, Brotli, ZStd compression middleware support
Co-Authored-By: Jason Baker <jason.th.baker@gmail.com>
2025-05-26 14:18:29 +04:00
Yu QX 4483fa95b8 Perfect the logic of case 1.1.x 2025-05-26 17:25:33 +08:00
Tim Jaeryang Baek c157e74f0c Merge pull request #14335 from open-webui/main
dev
2025-05-26 13:02:08 +04:00
Tim Jaeryang Baek ada9655f33 Merge pull request #14317 from SadmL/patch-3
[i18n] Russian locale update
2025-05-26 13:01:39 +04:00
Tim Jaeryang Baek 9de92687f6 Merge pull request #14328 from ShirasawaSama/fix_path_leakage_caused_by_file_upload
fix: Fix path leakage caused by file upload
2025-05-26 12:57:21 +04:00
Tim Jaeryang Baek 31dbafd62e Merge pull request #14301 from qingchunnh/Update_zh-CN-25525
i18n: Update & Improve zh-CN
2025-05-26 12:56:14 +04:00
Shirasawa 0dc29a220f fix: Fix path leakage caused by file upload 2025-05-26 12:20:00 +08:00
Link [Связной] 7db9f7b8b6 Update translation.json 2025-05-25 14:53:03 +03:00
Link [Связной] dc50fa3744 [i18n] Russian locale update 2025-05-25 14:42:42 +03:00
YuQX 8ef7938c96 fix: Handle special cases of LLM output not following markdown syntax (20250525, formated) This commit introduces a utility function to address rendering issues in LLM output, particularly for Chinese characters and parentheses. The function ensures minimal modification of the original text while fixing markdown parsing problems. Changes include: - Added in for handling specific cases. - Updated in to incorporate the new utility. The fix ensures proper rendering of bold/italic text containing Chinese parentheses, improving readability for non-English content. 2025-05-25 19:37:43 +08:00
fl0w1nd 332043c38b fix: Correctly handle toggle filters to prevent unintended activation 2025-05-25 17:59:31 +08:00
YuQX 49fe137553 fix: Handle special cases of LLM output not following markdown syntax 20250525
This commit introduces a utility function  to address rendering issues in LLM output, particularly for Chinese characters and parentheses. The function ensures minimal modification of the original text while fixing markdown parsing problems.

Changes include:
- Added  in  for handling specific cases.
- Updated  in  to incorporate the new utility.

The fix ensures proper rendering of bold/italic text containing Chinese parentheses, improving readability for non-English content.
2025-05-25 13:58:32 +08:00
qingchun 6a93b54e8e i18n: Update & Improve zh-CN 2025-05-25 08:02:47 +08:00
Timothy Jaeryang Baek 8598fc9608 chore: format 2025-05-25 02:33:52 +04:00
Timothy Jaeryang Baek 5885d8e275 enh: ability to hide workspace models 2025-05-25 02:32:32 +04:00
Timothy Jaeryang Baek f9d374810d refac 2025-05-25 02:21:03 +04:00
Timothy Jaeryang Baek 358ad8ef20 enh: import/export default prompt suggestions 2025-05-25 02:17:51 +04:00
Timothy Jaeryang Baek 33f2fdb91e refac: styling 2025-05-25 02:07:13 +04:00
Timothy Jaeryang Baek e65ac97c45 refac 2025-05-25 01:59:48 +04:00
Timothy Jaeryang Baek b2b1a21299 refac 2025-05-25 01:48:37 +04:00
Timothy Jaeryang Baek 75208935d7 refac: user chat list modal 2025-05-25 01:44:53 +04:00
Timothy Jaeryang Baek 44e7e09784 refac 2025-05-25 01:28:35 +04:00
Timothy Jaeryang Baekandsilentoplayz 1d53754ea7 refac: chat list modal
Co-Authored-By: silentoplayz <50341825+silentoplayz@users.noreply.github.com>
2025-05-25 01:26:12 +04:00
Timothy Jaeryang Baek 6e8ca96799 enh: archived chats modal 2025-05-25 01:23:12 +04:00
Timothy Jaeryang Baek 5db7d8acd1 refac 2025-05-25 00:58:55 +04:00
Timothy Jaeryang Baek 0fe00ab2c3 refac 2025-05-25 00:52:53 +04:00
Timothy Jaeryang Baek 9b000d82ec refac 2025-05-25 00:51:44 +04:00
Timothy Jaeryang Baek 7e6f1f8848 enh: archived chats modal 2025-05-25 00:48:30 +04:00
Timothy Jaeryang Baek 31e2686ae6 feat: /sync functions endpoint 2025-05-24 23:39:19 +04:00
Timothy Jaeryang Baek cce5f024bd feat: WEBUI_AUTH_TRUSTED_GROUPS_HEADER 2025-05-24 23:17:12 +04:00
Tim Jaeryang Baek 17263a294f Merge pull request #14284 from diwakar-s-maurya/patch-7
feat: add support for table formatting in knowledge collection
2025-05-24 23:02:46 +04:00
Tim Jaeryang Baek 6dbab870e5 Merge pull request #14263 from qingchunnh/Update_zh-CN-25524
i18n: Update & Improve zh-CN
2025-05-24 22:59:47 +04:00
Timothy Jaeryang Baek 9598532270 enh/chore: bug report template 2025-05-24 22:56:54 +04:00
Diwakar Singh Maurya e31a7bcc4c feat: add support for table formatting in knowledge collection 2025-05-24 16:21:55 +00:00
Timothy Jaeryang Baek 261328077d refac 2025-05-24 17:20:40 +04:00
Timothy Jaeryang Baek aebbc2593b refac: scrollbar thumb styling 2025-05-24 17:19:47 +04:00
Tim Jaeryang Baek 8261f0e17e Merge pull request #14276 from Classic298/memories
feat: Make Edit/Add Memory Modal larger, and the text input box size-adjustable
2025-05-24 16:01:07 +04:00
Tim Jaeryang Baek 37d2aed26e Merge pull request #14268 from TiancongLx/dev
i18n: update zh-TW
2025-05-24 15:42:50 +04:00
Classic298 c377ff80c9 Update EditMemoryModal.svelte 2025-05-24 13:33:56 +02:00
Classic298 9bd031dae3 Update AddMemoryModal.svelte 2025-05-24 13:33:49 +02:00
Tiancong Li bc7853d6a4 i18n: update zh-TW 2025-05-24 13:38:32 +08:00
qingchun adefc811c7 i18n: Update & Improve zh-CN 2025-05-24 09:18:18 +08:00
qingchun 3c828f7828 i18n: Update & Improve zh-CN 2025-05-24 08:30:09 +08:00
Timothy Jaeryang Baek da75d0ca1e chore: format 2025-05-24 02:13:54 +04:00
Timothy Jaeryang Baek 51ab02f3af chore: format 2025-05-24 02:13:46 +04:00
Timothy Jaeryang Baek 29d08c2a16 refac: styling 2025-05-24 02:12:44 +04:00
Timothy Jaeryang Baek abcf59ab2b refac: styling 2025-05-24 01:56:02 +04:00
Timothy Jaeryang Baek ffa5db4fec refac: styling 2025-05-24 01:45:55 +04:00
Timothy Jaeryang Baek f95e52a6a6 refac: wording 2025-05-24 01:40:01 +04:00
Timothy Jaeryang Baek 17aca908c5 refac: styling 2025-05-24 01:36:53 +04:00
Tim Jaeryang Baek e663b90a9f Merge pull request #14069 from Ithanil/bm25_weight
feat: Configurable weight for BM25Retriever during hybrid search
2025-05-24 01:13:03 +04:00
Timothy Jaeryang Baek bf97dbe052 refac 2025-05-24 01:12:11 +04:00
Timothy Jaeryang Baek 80eaa24ca8 enh: provide source name in context if available 2025-05-24 00:52:09 +04:00
Timothy Jaeryang Baek 8b5e89eada chore: format 2025-05-24 00:43:38 +04:00
Timothy Jaeryang Baek a46765fd0e refac 2025-05-24 00:43:02 +04:00
Timothy Jaeryang Baek baaa285534 feat: user stt language 2025-05-24 00:36:30 +04:00
Jan Kessler a90d3f3268 add missing locale string 2025-05-23 22:30:04 +02:00
Jan Kessler e70dd33233 rename BM25_WEIGHT -> HYBRID_BM25_WEIGHT 2025-05-23 22:06:44 +02:00
Tim Jaeryang Baek 9946dc7b5f Merge pull request #14252 from Kylapaallikko/dev
i18n: Update fi-FI translation
2025-05-23 23:53:40 +04:00
Timothy Jaeryang Baek 88a296b989 refac: chat reset input behaviour 2025-05-23 23:52:10 +04:00
Kylapaallikko bf12946ac9 Update fi-FI translation.json 2025-05-23 22:12:00 +03:00
Tim Jaeryang Baek 99f88082ca Merge pull request #14248 from diwakar-s-maurya/patch-3
feat: Show tool call parameters along tool ouput
2025-05-23 22:55:08 +04:00
Timothy Jaeryang Baek 6636207e0c refac 2025-05-23 22:10:57 +04:00
Tim Jaeryang Baek c8f1bdf928 Merge pull request #14245 from PVBLIC-F/dev
perf Update mistral.py
2025-05-23 21:57:16 +04:00
Timothy Jaeryang Baek 3f2025dc6e enh: always process file with external document loader 2025-05-23 21:55:09 +04:00
Diwakar Singh Maurya 549237fb05 Show tool call parameters along tool ouput 2025-05-23 17:42:05 +00:00
Timothy Jaeryang Baek 797c7d4e56 refac: allow min_p for openai requests 2025-05-23 21:28:02 +04:00
PVBLIC Foundation bf193dfb5d Update mistral.py 2025-05-23 10:00:19 -07:00
Timothy Jaeryang Baek 30d15c1b4b chore: format 2025-05-23 20:44:41 +04:00
Timothy Jaeryang Baek b9afef85c4 refac: advanced params 2025-05-23 20:44:19 +04:00
Timothy Jaeryang Baek ed0bebc7ef enh: granular audio playback speed 2025-05-23 20:10:16 +04:00
Timothy Jaeryang Baek f75ac5cc01 refac: styling 2025-05-23 20:06:05 +04:00
Timothy Jaeryang Baek 1cf21d3fa2 feat: ollama unload model 2025-05-23 19:45:29 +04:00
Timothy Jaeryang Baek 19b69fcb66 chore: format 2025-05-23 19:22:08 +04:00
Timothy Jaeryang Baek e15ff8d9f6 refac 2025-05-23 19:15:57 +04:00
Timothy Jaeryang Baek 0e6f09a0a9 enh: ollama loaded model display 2025-05-23 19:13:18 +04:00
Timothy Jaeryang Baek a50a8e2ef9 refac: ollama ps 2025-05-23 18:47:50 +04:00
Timothy Jaeryang Baek 65d997a6c5 refac 2025-05-23 18:22:57 +04:00
Timothy Jaeryang Baek 62fbd6c91c refac 2025-05-23 18:17:35 +04:00
Timothy Jaeryang Baek 03288d9fca refac: redesign 2025-05-23 17:30:17 +04:00
Tim Jaeryang Baek 06aa59e2a5 Merge pull request #14219 from Yu-QX/main
Fix: Solve equation problems with Chinese symbols
2025-05-23 17:19:29 +04:00
Timothy Jaeryang Baek 02000b1a6c refac: include help items in user menu 2025-05-23 16:49:11 +04:00
Timothy Jaeryang Baek dd8e742c2d refac: tts details tags 2025-05-23 13:40:33 +04:00
Tim Jaeryang Baek 55a6fae9db Merge pull request #14210 from ShrootBuck/main
refac: improve prompt formatting for selected text in FloatingButtons
2025-05-23 13:14:27 +04:00
Tim Jaeryang Baek bdcd04a78c Merge pull request #14212 from qingchunnh/Update_zh-CN-25523
i18n: Update & Improve zh-CN
2025-05-23 13:13:19 +04:00
Suleiman Elkhoury e10d6ad79c Fix S3 allowed characters in Tags.
In Amazon S3 storage, only the following charaters allowed in Tagging "letters (a-z, A-Z), numbers (0-9), and spaces representable in UTF-8, and the following characters: + - = . _ : / @". Added a sanitizer function to clear tags before the put request.
2025-05-23 11:09:40 +02:00
Tim Jaeryang Baek 9c4ce0273f Merge pull request #14215 from qingchunnh/Update_zh-TW-25523
i18n: Update zh-TW
2025-05-23 13:09:23 +04:00
qingchun 3d2a26fe0b i18n: Update & Improve zh-CN 2025-05-23 16:54:20 +08:00
qingchun aa6648ba8a i18n: Update & Improve zh-CN 2025-05-23 15:46:03 +08:00
YuQX ec97fdf5d6 Fix: Solve equation problems with Chinese symbols by adding more symbols to the ALLOWED_SURROUNDING_CHARS 2025-05-23 15:06:37 +08:00
qingchun 4025c96d0f i18n: Update zh-TW 2025-05-23 12:39:45 +08:00
qingchun ed9a2c295c i18n: Update & Improve zh-CN 2025-05-23 12:34:30 +08:00
qingchun 28e44452dd i18n: Update & Improve zh-CN 2025-05-23 09:51:05 +08:00
Zayd Krunz 1d382b83c8 refac: improve prompt formatting for selected text in FloatingButtons 2025-05-23 01:16:47 +00:00
Timothy Jaeryang Baek a2f12db8d9 refac: memories 2025-05-23 03:26:14 +04:00
Timothy Jaeryang Baek 7071a03419 chore: format 2025-05-23 02:54:44 +04:00
Timothy Jaeryang Baek 7a593b63b2 fix: image generation with allowed file extensions 2025-05-23 02:53:08 +04:00
Timothy Jaeryang Baek 4c55ad71a7 fix: local/external models 2025-05-23 02:48:31 +04:00
Timothy Jaeryang Baek 1f632d3570 fix: remove leading dot for file extension check 2025-05-23 02:39:19 +04:00
2eca6f6414 feat: bypass web loader in web search
Co-Authored-By: Perry Li <peiyaoli@mail.nankai.edu.cn>
Co-Authored-By: WilliamGates <3852641+williamgateszhao@users.noreply.github.com>
2025-05-23 02:30:35 +04:00
Timothy Jaeryang Baek 0b7f927983 enh: sortable banners 2025-05-23 02:02:11 +04:00
Timothy Jaeryang Baek a68ec8822a enh: filter md image for task generations 2025-05-23 01:33:08 +04:00
Timothy Jaeryang BaekandTornike Gurgenidze aac25eac9e refac: reranker
Co-Authored-By: Tornike Gurgenidze <togurg14@freeuni.edu.ge>
2025-05-23 01:29:48 +04:00
Timothy Jaeryang Baek f2d2cae8fb enh: help menu 2025-05-23 01:16:08 +04:00
Timothy Jaeryang Baek 71288ea3f2 refac 2025-05-23 01:11:44 +04:00
Timothy Jaeryang Baek cbe84d7139 enh: token expiry buffer 2025-05-23 01:08:28 +04:00
Timothy Jaeryang Baek d4c3cfbaba refac: oauth redirect url to use WEBUI_URL 2025-05-23 01:03:28 +04:00
Tim Jaeryang Baek 336d0b45ad Merge pull request #14165 from Zyfax/patch-1
fix: image model list
2025-05-22 15:37:00 +04:00
Tim Jaeryang Baek da4aa5f08b Merge pull request #14152 from U8F69/fix_user_auth
fix(auth): correctly use password hash when duplicate email records exist
2025-05-22 14:58:10 +04:00
Zyfax 7489bc6126 fix: image model list
OpenAI image model added:
gpt-image-1

Gemini image model renamed:
id: imagen-3-0-generate-002 to imagen-3.0-generate-002
2025-05-22 11:07:46 +02:00
Tim Jaeryang Baek 4519e4eed1 Merge pull request #14158 from itk-dev/feature/4347-danishtranslations
i18n: Updated danish translations
2025-05-22 12:47:22 +04:00
Timothy Jaeryang Baek 787987d2f0 refac/fix: optional tool param type 2025-05-22 12:41:00 +04:00
Tim Jaeryang Baek 17ed331a0a Merge pull request #14126 from SadmL/patch-2
[i18n] Russian locale update
2025-05-22 12:38:01 +04:00
Tim Jaeryang Baek b339fd6fed Merge pull request #14136 from Vombit/main
[i18n] Fixed a typo in the translation
2025-05-22 12:37:31 +04:00
Tim Jaeryang Baek 0eda03bd3c Merge pull request #14147 from PVBLIC-F/dev
perf Update pinecone.py
2025-05-22 12:19:14 +04:00
Tim Jaeryang Baek a935e375f3 Merge pull request #14149 from silentoplayz/i18n-fix
fix: $i18n.t fix
2025-05-22 11:49:09 +04:00
U8F69 dd6124a84f fix(auth): fix invalid password use in auth 2025-05-22 11:03:43 +08:00
Silentoplayz 558b2b1016 Update General.svelte 2025-05-21 20:41:19 -04:00
PVBLIC Foundation 86e24bb4aa Update pinecone.py
I've improved the pinecone.py file by:
Updated from the deprecated PineconeGRPC client to the newer Pinecone client
Modified the client initialization code to match the new API requirements
Added better response handling with getattr() to safely access attributes from response objects
Removed the streaming_upsert method which is not available in the newer client
Added safer attribute access with fallbacks throughout the code
Updated the close method to reflect that the newer client doesn't need explicit closing
These changes ensure the code is compatible with the latest Pinecone Python SDK and will be more robust against future changes. The key improvement is migrating away from the deprecated gRPC client which will eventually stop working.
2025-05-21 15:28:42 -07:00
Vombit ff46fa5b33 fix: fixed a typo in the translation 2025-05-21 21:15:42 +05:00
lilosti bd40cceb2d Updated danish translations 2025-05-21 14:52:33 +02:00
Link [Связной] 9201fd4652 [i18n] Russian locale update 2025-05-21 14:48:57 +03:00
Tim Jaeryang Baek 65a44a3900 Merge pull request #14099 from Classic298/ShiftHoldingFix
fix: Holding Shift Bug fixed
2025-05-21 13:46:58 +04:00
Tim Jaeryang Baek d2e426990b Merge pull request #14110 from qingchunnh/Update_zh-CN-25521
i18n: update and improve zh-CN
2025-05-21 13:43:00 +04:00
qingchun ed0538e643 i18n: update and improve zh-CN 2025-05-21 07:34:07 +08:00
Timothy Jaeryang Baek e3e7eb96f6 refac 2025-05-20 23:55:11 +04:00
Tim Jaeryang Baek a45f2cd0a9 Merge pull request #14104 from bassettb/bryan/parse-db-user-chars
fix: handle escaped characters in DATABASE_URL username
2025-05-20 23:51:41 +04:00
Timothy Jaeryang Baek 6121d4696a enh: search input arrow controls 2025-05-20 23:47:41 +04:00
Timothy Jaeryang Baek b78354100f refac 2025-05-20 23:33:41 +04:00
Timothy Jaeryang Baek 512345f40e refac: chat transition 2025-05-20 23:33:23 +04:00
Bryan Bassett 12896fb728 use unquote_user in peewee 3.17.10 2025-05-20 15:18:32 -04:00
DrMelone 8b80555adf fix attempt #1 2025-05-20 20:03:21 +02:00
Tim Jaeryang Baek 4e4b5ab83b Merge pull request #14096 from silentoplayz/add-clear-button-to-remaining-pages
feat: Add Clear button to Functions and Models pages search bar
2025-05-20 21:30:07 +04:00
Silentoplayz 03eeb75bfe feat: Add Clear button for Function and admin Model pages 2025-05-20 12:21:32 -04:00
Timothy Jaeryang Baek 74ace200fe fix/refac: functions multi-replica issue 2025-05-20 20:20:27 +04:00
Tim Jaeryang Baek 2b5c015bcb Merge pull request #14093 from open-webui/main
dev
2025-05-20 19:48:26 +04:00
Timothy Jaeryang Baek 82716f3789 refac 2025-05-20 19:39:18 +04:00
Timothy Jaeryang Baek a8a9b35153 refac: file item modal 2025-05-20 19:29:31 +04:00
Tim Jaeryang Baek d3c7628092 Merge pull request #14059 from sreesdas/main
fix: resolve issue where external document loader was not invoked
2025-05-20 17:43:06 +04:00
Tim Jaeryang Baek 86f1aee20b Merge pull request #14054 from silentoplayz/delete-lightblub
fix: Correct typo in LightBulb component name and imports & delete lightblub.svelte (duplicate of lightbulb.svelte)
2025-05-20 17:25:42 +04:00
Tim Jaeryang Baek f9a66afe76 Merge pull request #14072 from torisetxd/alignment-fix
fix: Alignment issues with Text in Buttons in UI
2025-05-20 17:24:49 +04:00
Tim Jaeryang Baek fac5884d8c Merge pull request #14073 from tth37/fix_default_web_loader_verify_ssl
fix: Default web loader fail silently when `verify_ssl=False`
2025-05-20 17:24:22 +04:00
Tim Jaeryang Baek 6035664048 Merge pull request #14051 from Classic298/german-translation
i18n: German translation overhaul
2025-05-20 17:23:55 +04:00
Tim Jaeryang Baek 75a31e116a Merge pull request #14049 from mjmendo/web_search_optional_fields
fix: web search optional fields
2025-05-20 17:22:52 +04:00
tth37 78befd5a2f fix: Default web loader fail when verify_ssl=False 2025-05-20 19:44:18 +08:00
toriset b735042ecc Fix alignment issues with Buttons in UI 2025-05-20 14:33:04 +03:00
Jan Kessler 308d8ac04a make bm25_weight a regular parameter of query_doc.. / get_sources_from_files functions 2025-05-20 11:46:32 +02:00
Jan Kessler b5ddaf6417 make weight for bm25 retriever in hybrid search ui-configurable 2025-05-20 10:39:31 +02:00
Classic298 8d94a59f27 task models 2025-05-20 08:39:36 +02:00
Classic298 479b5ad820 fixes after feedback 2025-05-20 08:31:49 +02:00
sree f408b08965 minor bug fix for external document loader not working 2025-05-20 11:10:23 +05:30
Derek Wischusen 42be1f956a Add Azure OpenAI embedding support 2025-05-19 22:58:04 -04:00
Silentoplayz ab9486603c chore: fix on-hover visual 2025-05-19 22:42:15 -04:00
Silentoplayz 58b309149a chore: adjustment 2025-05-19 17:21:43 -04:00
Silentoplayz 83ed2d0661 chore: delete lightblub.svelte 2025-05-19 17:12:36 -04:00
DrMelone 4b538047dd various i18n fixes
typos, incomplete sentences fixed, wrong translations, inconsistencies fixed
2025-05-19 19:35:08 +02:00
DrMelone fd2ff5e777 i18n german update 1 2025-05-19 19:28:12 +02:00
Marcelo Mendoza 897727c7eb Merge branch 'open-webui:main' into web_search_optional_fields 2025-05-19 17:25:14 +02:00
Marcelo Mendoza d6ad96affb fix: use get method for title and snippet in search results 2025-05-19 17:24:47 +02:00
Tim Jaeryang Baek 5613cda5cf Merge pull request #14048 from open-webui/main
dev
2025-05-19 19:17:39 +04:00
Timothy Jaeryang Baek ed0659aca6 refac 2025-05-19 19:17:08 +04:00
Timothy Jaeryang Baek fd91b277a4 refac: pypi 2025-05-19 14:48:55 +04:00
Timothy Jaeryang Baek d2758c7fa2 refac: pypi 2025-05-19 14:35:51 +04:00
Tim Jaeryang Baek ef3232a24f Merge pull request #14023 from TiancongLx/dev
i18n: update zh-TW
2025-05-19 14:22:31 +04:00
Tim Jaeryang Baek 438eb90671 Merge pull request #14024 from hurxxxx/i18n/update-ko-KR
i18n: update ko-KR
2025-05-19 14:22:13 +04:00
Tim Jaeryang Baek c9bbfaee9d Merge pull request #14033 from SadmL/patch-2
[i18n] Russian locale update
2025-05-19 14:22:03 +04:00
Link [Связной] 1d0921de0d [i18n] Russian locale update 2025-05-19 11:24:08 +03:00
hurxxxx c1066d7e08 i18n: update ko-KR 2025-05-19 13:28:45 +09:00
Tiancong Li 23991c32c5 i18n: update zh-TW 2025-05-19 12:09:07 +08:00
Tim Jaeryang Baek e6afa69f59 Merge pull request #13887 from open-webui/dev
0.6.10
2025-05-19 05:34:23 +04:00
Timothy Jaeryang Baek 2b1c3b5208 Update CHANGELOG.md 2025-05-19 05:27:29 +04:00
Timothy Jaeryang Baek b905b73935 Update CHANGELOG.md 2025-05-19 05:24:03 +04:00
Timothy Jaeryang Baek fa5cd59ef8 doc: changelog 2025-05-19 05:20:57 +04:00
Timothy Jaeryang Baek 030c20feff refac 2025-05-19 05:12:53 +04:00
Tim Jaeryang Baek 8fb7b980a2 Merge pull request #14019 from silentoplayz/clear
feat: Improve remaining export buttons with total item count
2025-05-19 04:33:34 +04:00
Timothy Jaeryang Baek eafb8e014a chore: format 2025-05-19 04:32:00 +04:00
Timothy Jaeryang Baek 2ab5aa4d34 refac: azure openai 2025-05-19 04:31:04 +04:00
Silentoplayz 5cb1475e29 fix: apply change to functions and models pages 2025-05-18 20:12:53 -04:00
Timothy Jaeryang Baek 2f2a7ee0b0 chore: format 2025-05-19 03:58:42 +04:00
Timothy Jaeryang Baek 2e56b1f13d refac 2025-05-19 03:55:56 +04:00
Timothy Jaeryang Baek caeb822cdc feat: azure openai support 2025-05-19 03:40:32 +04:00
Timothy Jaeryang Baek 47f8b3500b feat: azure openai connections ui 2025-05-19 03:29:14 +04:00
Timothy Jaeryang Baek 73e64fe7fb refac: audio upload handling 2025-05-19 02:52:48 +04:00
Timothy Jaeryang Baekandsilentoplayz aee9d05916 enh: workspace search clear button
Co-Authored-By: silentoplayz <50341825+silentoplayz@users.noreply.github.com>
2025-05-19 02:36:15 +04:00
Timothy Jaeryang Baek 3af289d196 refac 2025-05-19 02:32:06 +04:00
Timothy Jaeryang Baek 9f9fb3403d refac 2025-05-19 02:07:41 +04:00
Timothy Jaeryang Baek c6580a2a02 refac 2025-05-19 02:01:07 +04:00
Timothy Jaeryang Baek 417ba4980a chore: format 2025-05-19 01:55:02 +04:00
Timothy Jaeryang Baek 0c6f22b20b refac 2025-05-19 01:54:26 +04:00
Timothy Jaeryang Baek 96c7a0ac3c refac 2025-05-19 01:52:40 +04:00
Timothy Jaeryang Baek e895c33baa refac 2025-05-19 01:47:12 +04:00
Timothy Jaeryang Baek fbfd3ff134 refac 2025-05-19 01:44:27 +04:00
Timothy Jaeryang Baek 5a40feea1c enh: search shortcut 2025-05-19 01:40:51 +04:00
Timothy Jaeryang Baek 1f5860c4ab refac 2025-05-19 01:40:10 +04:00
Timothy Jaeryang Baek d069d1264d feat: search modal 2025-05-19 01:39:33 +04:00
Tim Jaeryang Baek 4f4811adb1 Merge pull request #14017 from athoik/title_tags
feat: read max_tokens from model config with fallback to 1000 for title and tag generation
2025-05-19 00:43:05 +04:00
Timothy Jaeryang Baek 05b79ce221 refac: model selection 2025-05-19 00:41:49 +04:00
Timothy Jaeryang Baekandsilentoplayz 18d6311951 feat: export count
Co-Authored-By: silentoplayz <50341825+silentoplayz@users.noreply.github.com>
2025-05-19 00:15:07 +04:00
Timothy Jaeryang Baek 8f4104fb7a refac 2025-05-19 00:13:03 +04:00
Tim Jaeryang Baek 349b0ca0b7 Merge pull request #14002 from silentoplayz/search-by-user-or-email
feat: enhance search with username/email search across workspace pages
2025-05-19 00:12:11 +04:00
Tim Jaeryang Baek 2dde4102ad Merge pull request #14009 from TiancongLx/dev
i18n: update and improve zh-TW
2025-05-19 00:11:32 +04:00
Athanasios Oikonomou eabdd4a140 feat: read max_tokens from model config with fallback to 1000 for title and tag generation
Improves title and tag generation by using the max_tokens value from the model configuration when available, with a fallback to the previous default of 1000.

This change is necessary for models like Gemini Pro that generate longer responses and require a higher token limit to successfully generate titles or tags.
2025-05-18 22:45:33 +03:00
Tiancong Li 4667328d40 i18n: update zh-TW 2025-05-18 21:16:16 +08:00
Tim Jaeryang Baek 56740ab8d6 Merge pull request #13996 from Davixk/update-IT-translation
i18n: add and improve Italian translations
2025-05-18 15:04:54 +04:00
Silentoplayz 95badda1c2 feat: username and email search in workspace pages 2025-05-18 03:23:38 -04:00
Dave ebf2e4141e Merge remote-tracking branch 'upstream/dev' into update-IT-translation 2025-05-18 04:46:41 +02:00
Dave a8813c1a69 i18n: Update and Improve Italian translations 2025-05-18 04:36:12 +02:00
Tim Jaeryang Baek fbcc80485b Merge pull request #13972 from jarrod-lowe/otel-attrs
feat: Add user details to opentelemetry spans
2025-05-17 21:54:56 +04:00
Timothy Jaeryang Baek 765acc4cad chore: dep bump 2025-05-17 21:53:31 +04:00
Timothy Jaeryang Baek 9801a2d23f refac 2025-05-17 03:14:26 +04:00
Timothy Jaeryang Baek 424f6f71cf chore: duckduckgo-search bump 2025-05-17 03:03:07 +04:00
Timothy Jaeryang Baek b280f828b0 enh: very long audio transcription 2025-05-17 02:51:28 +04:00
Timothy Jaeryang Baek d54e588ec3 refac 2025-05-17 02:26:28 +04:00
Timothy Jaeryang Baek 08e4c163ea feat: local/external connections 2025-05-17 01:47:48 +04:00
Timothy Jaeryang Baek 000906cd99 chore: format 2025-05-17 01:13:36 +04:00
Timothy Jaeryang Baek c3acb30bb1 feat: model file upload capability 2025-05-17 01:13:13 +04:00
Jarrod Lowe df853246f3 Add user details to otel span 2025-05-17 09:11:26 +12:00
Timothy Jaeryang Baek 6692fb2181 chore: format 2025-05-17 01:00:37 +04:00
Timothy Jaeryang Baek b14398481d enh: model capabilities 2025-05-17 00:59:00 +04:00
Timothy Jaeryang Baek 7df6d7f325 refac/fix: signout redirect flow 2025-05-17 00:38:39 +04:00
Timothy Jaeryang Baek fcb93f3155 feat: artifacts download button 2025-05-17 00:27:20 +04:00
Timothy Jaeryang Baek 02e5bbc903 refac 2025-05-17 00:19:08 +04:00
Timothy Jaeryang Baek 359bcb837d refac 2025-05-17 00:08:03 +04:00
Timothy Jaeryang Baek ecae944fd0 refac 2025-05-16 23:59:24 +04:00
Timothy Jaeryang Baek 05f284d4a6 refac 2025-05-16 23:41:51 +04:00
Timothy Jaeryang Baek ea723374c7 refac 2025-05-16 23:36:33 +04:00
Timothy Jaeryang Baek 1f38350128 feat: toggle filter middleware 2025-05-16 23:33:02 +04:00
Timothy Jaeryang Baek e37433f2b1 refac 2025-05-16 23:21:08 +04:00
Timothy Jaeryang Baek b61efcf54a feat: toggle filter 2025-05-16 22:59:02 +04:00
Timothy Jaeryang Baek 32b386e8d0 refac 2025-05-16 22:44:47 +04:00
Timothy Jaeryang Baek 7bbeaec93b revert 2025-05-16 22:43:42 +04:00
Timothy Jaeryang Baek 34ef249a10 revert: remove reasoning capability 2025-05-16 22:24:23 +04:00
Timothy Jaeryang Baek 363ac35c09 refac 2025-05-16 22:17:47 +04:00
Tim Jaeryang Baek 50bc6d9b12 Merge pull request #13919 from dongfangzan/main
feat: add switching thinking/non-thinking modes to models
2025-05-16 22:08:04 +04:00
Timothy Jaeryang Baek 27bb5f5369 fix/refac: tts remove formattings 2025-05-16 21:51:29 +04:00
Timothy Jaeryang Baek 54dc24986f feat: preview html 2025-05-16 21:47:43 +04:00
Timothy Jaeryang Baek 07b5e84221 refac 2025-05-16 21:29:50 +04:00
Timothy Jaeryang Baek 2bd7db12a2 enh: ALLOWED_FILE_EXTENSIONS ui 2025-05-16 21:05:52 +04:00
Timothy Jaeryang Baek 528a2cf96a refac 2025-05-16 18:19:28 +04:00
Timothy Jaeryang Baek 2aa5e73728 refac: filter details 2025-05-16 18:01:55 +04:00
Timothy Jaeryang Baek dec9dd1ac0 refac: web search queries 2025-05-16 17:11:42 +04:00
Timothy Jaeryang Baek bb06df80a2 refac 2025-05-16 17:07:52 +04:00
Timothy Jaeryang Baek 0b761776c4 enh: high contrast mode 2025-05-16 17:06:21 +04:00
Timothy Jaeryang Baek 2450f2c5d8 chore: format 2025-05-16 16:57:30 +04:00
Timothy Jaeryang Baek 62180b15e1 refac: command arrow up/down behaviour 2025-05-16 15:39:26 +04:00
Tim Jaeryang Baek c201b4d689 Merge pull request #13931 from itk-dev/feature/better-frontend-error
WIP: Minor clean up of console log usage in the front end
2025-05-16 13:33:16 +04:00
Tim Jaeryang Baek bc66755fb0 Merge pull request #13950 from uryukikutani/feature/screen-reader
fix: made the chat response messages readable with a screen reader
2025-05-16 13:32:40 +04:00
Tim Jaeryang Baek e74c06d2f5 Merge pull request #13929 from mini-777/feat--Korean-translations-for-clarity-and-accuracy
i18n : korean translations for clarity and accuracy
2025-05-16 13:32:10 +04:00
uryukikutani 53dcaa63d4 made the chat response messages readable with a screen reader 2025-05-16 17:41:56 +09:00
Tim Jaeryang Baek 186a89bcba Merge pull request #13937 from tanerim/patch-1
Update translation.json
2025-05-16 10:23:47 +04:00
dongfangzan 351f0186b9 fix: typo 2025-05-16 08:05:49 +08:00
Taner Sezer 21e96247a9 Update translation.json
Added new translations
2025-05-16 01:19:53 +03:00
Jesper Kristensen 84e0605835 Cleaning up usage of console log in front end 2025-05-15 21:53:07 +02:00
Jesper Kristensen 20c1c3f66b Changed console logging of errors to use console error logging 2025-05-15 21:35:07 +02:00
mini-777 2a4fe1335e i18n: update Korean translations with new entries and corrections 2025-05-16 02:45:19 +09:00
mini-777 e24e22debe Merge commit 'e4179aa114fecc8d35ba48f091ce7c9abcc96065' into HEAD 2025-05-16 02:10:01 +09:00
mini-777 208b86ae53 feat: add new notes feature with editor and list components, and update routing for notes management 2025-05-16 02:06:36 +09:00
Tim Jaeryang Baek e4179aa114 Merge pull request #13922 from TiancongLx/dev
i18n: update zh-TW
2025-05-15 19:02:37 +04:00
Tiancong Li df5ef8e553 i18n: update zh-TW 2025-05-15 21:15:22 +08:00
dongfangzan 9b93b81f84 feat: add switching thinking/non-thinking modes to models 2025-05-15 19:56:10 +08:00
dongfangzan a7c90925ef test contribute 2025-05-15 19:31:40 +08:00
Tim Jaeryang Baek ca2cfb5bad Merge pull request #13899 from loitragg/apply-multitenant-for-qdrant
perf: implement multi-tenancy for Qdrant vector store client
2025-05-15 13:37:36 +04:00
Kiet Trinh 418ac1a8da refac: Rename Qdrant multi-tenancy variable for improved clarity and consistency 2025-05-15 09:09:24 +00:00
Tim Jaeryang Baek db24af6356 Merge pull request #13902 from yo-nagase/dev
i18n: Update Japanese translation
2025-05-15 12:59:17 +04:00
Timothy Jaeryang Baek 72b2555953 refac 2025-05-15 12:58:44 +04:00
Kiet Trinh 485bd7666c fix: Update Qdrant multi-tenancy variable name for consistency in configuration 2025-05-15 08:02:58 +00:00
Nagase Yoshimasa 6eb772c741 i18n: Update Japanese translation 2025-05-15 15:41:04 +09:00
LoiTra 184d8dfd7e feat: Implement Qdrant multi-tenancy support with collection management and tenant isolation 2025-05-15 11:28:06 +07:00
Timothy Jaeryang Baek 0a8cecfbfa feat: response watermark 2025-05-14 23:53:28 +04:00
Timothy Jaeryang Baek b143c71da2 refac: AIOHTTP_CLIENT_SESSION_SSL 2025-05-14 23:33:52 +04:00
Timothy Jaeryang Baek 04287eb6d6 refac 2025-05-14 23:27:34 +04:00
Timothy Jaeryang Baek 39b06dc3b6 refac 2025-05-14 23:13:06 +04:00
Tim Jaeryang Baek e358a66fb1 Merge pull request #13852 from fizyr-forks/self-signed-cert
feat: Add option to disable certificate validation for LDAP certificates.
2025-05-14 23:12:24 +04:00
Timothy Jaeryang Baek a6624a4b16 feat: RAG_ALLOWED_FILE_EXTENSIONS 2025-05-14 23:06:33 +04:00
Timothy Jaeryang Baek 7e241a3904 refac 2025-05-14 22:48:23 +04:00
Timothy Jaeryang Baek 42382b5167 fix 2025-05-14 22:46:01 +04:00
Timothy Jaeryang Baek 8732b64b6b feat: external document loader support 2025-05-14 22:28:40 +04:00
Timothy Jaeryang Baek 32ea31144e refac: file upload handling behaviour 2025-05-14 22:00:42 +04:00
Timothy Jaeryang Baek b80f0751f6 refac 2025-05-14 21:59:17 +04:00
Timothy Jaeryang Baek 0280ee30c7 chore: format 2025-05-14 21:56:22 +04:00
Timothy Jaeryang Baek 8fe4f789d4 fix 2025-05-14 21:53:46 +04:00
Timothy Jaeryang Baek dacf7e015d i18n: revert 2025-05-14 21:52:42 +04:00
Timothy Jaeryang Baek ac83568194 chore: format 2025-05-14 21:40:51 +04:00
Timothy Jaeryang Baek 10108fcd04 chore: format 2025-05-14 21:39:50 +04:00
Timothy Jaeryang Baek 74ccec2812 refac 2025-05-14 21:39:17 +04:00
Tim Jaeryang Baek 29ae231ee0 Merge pull request #13889 from Classic298/pendingtext
feat: Custom Title and Text for Account Activation Screen for pending users
2025-05-14 21:28:16 +04:00
Timothy Jaeryang Baek de70d0cb64 feat: docling do picture description support 2025-05-14 21:26:49 +04:00
DrMelone 3974e53cc3 i18n 2025-05-14 19:11:53 +02:00
DrMelone 92140118f2 revert i18n 2025-05-14 19:07:45 +02:00
DrMelone e9eb661b83 fix + i18n 2025-05-14 18:58:54 +02:00
DrMelone bdb3778f8b fix 2025-05-14 18:10:23 +02:00
Timothy Jaeryang Baek 6e8481e157 fix: evaluation feedback endpoints 2025-05-14 20:08:49 +04:00
DrMelone 143cb4cc34 feat custom text and title 2025-05-14 17:58:20 +02:00
Tim Jaeryang Baek 2217fc4fd9 Merge pull request #13883 from alpha-pet/fix-workspace-model-unintended-toggle
Fix: Unintentional toggling of model status on the workspace page.
2025-05-14 18:09:16 +04:00
Tim Jaeryang Baek 8029bf0dca Merge pull request #13881 from hwzhuhao/feat/vector-db
feat: Add vector type and vector factory class for vector database integration
2025-05-14 17:41:39 +04:00
Thomas Rehn 6b6ce36c96 fix: add unique key to model list to avoid state confusion when filtering 2025-05-14 15:33:32 +02:00
hwzhuhao 6f869ded43 feat:Add vector type and vector factory class for vector database integration 2025-05-14 21:30:50 +08:00
Tim Jaeryang Baek 4959e91fa7 Merge pull request #13832 from Ithanil/svelte_versioning
feat: Handle frontend updates according to Svelte docs
2025-05-14 17:19:32 +04:00
Tim Jaeryang Baek f77a076c16 Merge pull request #13882 from itk-dev/feature/enable_direct_connections
chore: Added missing "enable_direct_connections" to config type
2025-05-14 17:18:04 +04:00
Jesper Kristensen 8b194bc781 Added missing "enable_direct_connections" to config type 2025-05-14 14:53:36 +02:00
Tim Jaeryang Baek bd7d2978f1 Merge pull request #13846 from hary777/patch-2
i18n update translation.json fix typo "amin" in cs-CZ
2025-05-14 15:27:40 +04:00
Tim Jaeryang Baek c7e3ae9f39 Merge pull request #13812 from hwzhuhao/refactor/log
refactor: replace print statements with logging
2025-05-14 15:27:13 +04:00
Timothy Jaeryang Baek 92a8ea743a refac: form data edge case 2025-05-14 15:22:38 +04:00
Timothy Jaeryang Baek 3c4accaeb3 refac/security: ollama model upload path traversal 2025-05-14 15:17:07 +04:00
Timothy Jaeryang Baek a9c22bffc9 refac/security: pipelines upload path traversal 2025-05-14 15:15:05 +04:00
Tim Jaeryang Baek d89e37cc7c Merge pull request #13866 from itk-dev/feature/contrast-chat-input
feat: Contrast enhancements
2025-05-14 15:12:00 +04:00
Sine Jespersen 8eacd26573 add placeholder dark mode 2025-05-14 12:50:47 +02:00
Sine Jespersen 90bcf45b7b placeholder contrast changed 2025-05-14 10:24:09 +02:00
Yeyeong Cho bbb4e75df8 Merge pull request #2 from mini-777/i18n-ko-rewriting
feat(openwebui) : improve Korean translations for better readability Fixes mini-777/8dobibim#2
2025-05-14 17:23:48 +09:00
Sine Jespersen b052b5cced remove whitespace 2025-05-14 10:15:31 +02:00
Sine Jespersen 5db783d67b improve suggestions contrast 2025-05-14 09:52:44 +02:00
Sine Jespersen 43d0db5eea model selector contrast 2025-05-14 09:52:10 +02:00
Sine Jespersen 41f64da0d6 update message input contrast 2025-05-14 09:51:38 +02:00
Sine Jespersen 5cd0b99ae0 update toast contrast 2025-05-14 09:51:07 +02:00
mini-777 571d078a82 fix: improve Korean translations for better readability (fixes mini-777/8dobibim#2) 2025-05-14 15:37:59 +09:00
mini-777 f72fa9b269 fix: update Korean translations for clarity and accuracy 2025-05-14 15:28:46 +09:00
Hans Gaiser f63e76e653 Add option to disable certificate validation. 2025-05-13 17:38:06 +02:00
hary777 66d73cb1de Update translation.json fix typo
Fix typo "amin". I can not look at this anymore.
2025-05-13 15:24:48 +02:00
Tim Jaeryang Baek 950f3cdcc0 Merge pull request #13838 from TiancongLx/dev
i18n: update, complete, and optimize zh-TW
2025-05-13 16:53:52 +04:00
Tim Jaeryang Baek f907e7c979 Merge pull request #13836 from itk-dev/feature/build-watch
build: Added build:watch command to package.json
2025-05-13 16:53:24 +04:00
Jesper Kristensen 24c1b8911c Added build:watch script/command to package.json
Should make it eaiser to automatically build the front-end when
doing local development in docker node containers.
2025-05-13 10:36:48 +02:00
Tiancong Li b41fc1d2f4 i18n: update zh-TW 2025-05-13 16:30:59 +08:00
Jan Kessler dfd4037132 better handling of frontend updates, following svelte docs 2025-05-13 09:51:51 +02:00
Timothy Jaeryang Baek a904f874ee fix: admin signup logic 2025-05-12 20:03:40 +04:00
hwzhuhao fb2950201a refactor: replace print statements with logging 2025-05-12 22:18:47 +08:00
Timothy Jaeryang Baek e9b799676b chore: format 2025-05-12 17:34:33 +04:00
Tim Jaeryang Baek c7f743ddb7 Merge pull request #13810 from phpia/phpia-patch-1
i18n es
2025-05-12 17:31:18 +04:00
phpia 16a9210321 Update translation for mantain prompt
This pull request updates the Spanish localization by replacing all instances of "indicador"/"indicadores" with the more accurate terms "prompt"/"prompts" throughout the UI, documentation, and messages. 

Reason and Rationale: 

In the context of AI and Large Language Models (LLMs), the words "prompt" and "prompts" have become standardized terms worldwide, including within the Spanish-speaking technical community. While "indicador" is a valid literal translation of "prompt" in general Spanish, in generative AI contexts it is potentially confusing, since "indicador" more commonly refers to a gauge, dashboard indicator, or measuring device. This can cause misunderstanding for users, especially those familiar with AI concepts, who expect the term "prompt" to refer to the input text given to a model.
2025-05-12 14:59:15 +02:00
Sung Min c049837a11 Merge pull request #1 from mini-777/i18n-ko-missing
feat(openwebui) : add missing Korean translation strings Fixes mini-777/8dobibim#1
2025-05-12 20:55:14 +09:00
Tim Jaeryang Baek ca34ba318e Merge pull request #13801 from aleixdorca/dev
i18n: Update catalan translation.json
2025-05-12 14:56:19 +04:00
Tim Jaeryang Baek 0d8e24320e Merge pull request #13794 from Lutherwaves/dev
i18n: Bulgarian translations
2025-05-12 14:55:10 +04:00
Aleix Dorca b7ef9a03ac Update catalan translation.json 2025-05-12 08:56:52 +02:00
Tim Jaeryang Baek 4ce1e88750 Merge pull request #13795 from open-webui/dev
refac
2025-05-12 01:10:34 +04:00
Timothy Jaeryang Baek f32b042a00 refac 2025-05-12 01:08:49 +04:00
lutherwaves 792beb095e i18n: Bulgarian translations 2025-05-11 23:58:31 +03:00
Timothy Jaeryang Baek 4c06683e95 refac: Vazirmatn 2025-05-12 00:05:04 +04:00
Tim Jaeryang Baek 0cef844168 Merge pull request #13758 from open-webui/dev
0.6.9
2025-05-10 23:04:48 +04:00
Timothy Jaeryang Baek c56d6963b0 doc: changelog 2025-05-10 23:01:48 +04:00
Timothy Jaeryang Baek 92e2cfdd2f refac 2025-05-10 23:00:53 +04:00
Timothy Jaeryang Baek 3ad5a7d97c chore: bump 2025-05-10 22:48:26 +04:00
Timothy Jaeryang Baek 867fddb821 chore: format 2025-05-10 22:45:58 +04:00
Timothy Jaeryang Baek de69c08a1d enh: alert private task model 2025-05-10 22:43:53 +04:00
Timothy Jaeryang Baek 30cc51ec82 feat: edit message files 2025-05-10 22:27:39 +04:00
Timothy Jaeryang Baek ba69c751ca refac: temporary chat behaviour 2025-05-10 22:12:36 +04:00
Timothy Jaeryang Baek be85f7cc35 fix: confirm dialog focus 2025-05-10 22:09:05 +04:00
Tim Jaeryang Baek 4191ec692e Merge pull request #13751 from open-webui/dev
fix: external reranker
2025-05-10 19:33:59 +04:00
Timothy Jaeryang Baek 6b5f99bf66 fix: external reranker 2025-05-10 19:33:34 +04:00
Tim Jaeryang Baek ef301aa16b Merge pull request #13707 from open-webui/dev
0.6.8
2025-05-10 19:31:52 +04:00
Timothy Jaeryang Baek 32b27749ef Update CHANGELOG.md 2025-05-10 19:29:02 +04:00
Timothy Jaeryang Baek b81a5833c7 doc: changelog 2025-05-10 19:25:42 +04:00
Timothy Jaeryang Baek 6f635d8b7d refac 2025-05-10 19:16:09 +04:00
Timothy Jaeryang Baek 549989e9ec refac 2025-05-10 19:04:40 +04:00
Timothy Jaeryang Baek 1c665592e4 chore: bump 2025-05-10 19:01:12 +04:00
Timothy Jaeryang Baek c61790b355 chore: format 2025-05-10 19:00:01 +04:00
Timothy Jaeryang Baek 3a79840a87 feat: unstylized pdf export 2025-05-10 18:59:04 +04:00
Timothy Jaeryang Baek a515a5df1a refac 2025-05-10 18:38:30 +04:00
Timothy Jaeryang Baek ba72d4625f refac 2025-05-10 18:36:45 +04:00
Timothy Jaeryang Baek 3dc34c2402 feat: external reranker settings ui 2025-05-10 18:33:52 +04:00
Timothy Jaeryang Baek be912f1529 refac 2025-05-10 18:29:04 +04:00
Timothy Jaeryang BaekandBrendan Campbell d5fd3b3600 feat: external reranker
Co-Authored-By: Brendan Campbell <20541191+bcambs09@users.noreply.github.com>
2025-05-10 18:25:20 +04:00
Timothy Jaeryang BaekandMabeck 34ec10a78c refac: web search performance
Co-Authored-By: Mabeck <64421281+mmabeck@users.noreply.github.com>
2025-05-10 17:54:41 +04:00
Timothy Jaeryang Baek 015ac2f532 refac: user list loading indicator 2025-05-10 17:23:17 +04:00
Tim Jaeryang Baek 030b6b26af Merge pull request #13735 from Classic298/dev
Fix: Markdown in Knowledgebases rendered correctly now
2025-05-10 17:13:16 +04:00
Tim Jaeryang Baek 6d1c18e118 Merge pull request #13736 from taylorwilsdon/ideas/13702
enh: Improve visibility of overflow / hidden tools when more than 3 are present
2025-05-10 17:10:45 +04:00
Tim Jaeryang Baek 54dda08d39 Merge pull request #13712 from PVBLIC-F/dev
perf - Pinecone.py
2025-05-10 17:10:08 +04:00
Tim Jaeryang Baek 7bfe4c6a43 Merge pull request #13738 from dataway/dev
fix: allow non-root jupyter base_url
2025-05-10 17:09:09 +04:00
Timothy Jaeryang Baek cc1dc6cb78 refac: config 2025-05-10 17:08:09 +04:00
PVBLIC Foundation 3f58a17e47 Update pinecone.py
•	Removed the unused Pinecone REST‐client import; we now only import ServerlessSpec and the gRPC client.
	•	Enhanced close()
	•	Call self.client.close() to explicitly shut down the underlying gRPC channel.
	•	Log success or a warning on failure.
	•	Still tear down the thread‐pool executor afterward.
	•	Context‐manager support
	•	Added __enter__()/__exit__() so you can do:

with PineconeClient() as client:
    client.insert(...)
# automatically calls client.close()
2025-05-10 06:07:27 -07:00
Tim Jaeryang Baek 3e1be8b958 Merge pull request #13746 from ghscience/dev
i18n Updated Italian Language Translation Added missing translations
2025-05-10 17:05:49 +04:00
gh d704b8b686 i18n Updated Italian Language Translation 2025-05-10 09:53:52 +00:00
PVBLIC Foundation 12c2138982 Update pinecone.py
Refactor and added debug
2025-05-09 18:15:22 -07:00
Anthony Uk 337ece4d50 code_interpreter.py: allow non-root jupyter base_url 2025-05-09 20:32:01 +02:00
Taylor Wilsdon 1299c11650 only show tools toggle if overflow present 2025-05-09 10:30:45 -04:00
Taylor Wilsdon 628a10e34a make the collapse toggle more subtle & compact 2025-05-09 10:26:48 -04:00
Classic298 513895c2d5 Update KnowledgeBase.svelte 2025-05-09 16:24:07 +02:00
Taylor Wilsdon ccbc46fbbe implement visible scrollbar for tools overflow, add show all toggle 2025-05-09 10:20:17 -04:00
Tim Jaeryang Baek 8acc1ab425 Merge pull request #13731 from tth37/fix_duplicate_web_search_urls
fix: Duplicate web search urls
2025-05-09 16:43:11 +04:00
Tim Jaeryang Baek 616e9e2760 Merge pull request #13730 from Classic298/dev
feat: Increase default users per page in admin panel to 30
2025-05-09 16:42:29 +04:00
tth37 c95a65a4bd fix: Duplicate web search urls 2025-05-09 20:06:35 +08:00
Classic298 220d35ebcb Increase users shown per page to 30 2025-05-09 13:00:42 +02:00
Classic298 947f5e840e Increased PAGE_ITEM_COUNT to 30 2025-05-09 12:59:50 +02:00
Timothy Jaeryang Baek ebf5cb22ab refac: socket 2025-05-09 14:23:16 +04:00
Timothy Jaeryang Baek d84a3c0bf4 revert: websocket 2025-05-09 14:20:43 +04:00
Timothy Jaeryang Baek 9143716463 refac: chat pdf rendering 2025-05-09 13:56:53 +04:00
Yeyeong Cho 13e7a3dee9 fix:add missing Korean translation strings Fixes mini-777/8dobibim#1 2025-05-09 17:54:50 +09:00
Timothy Jaeryang Baek 9657474a8d refac: websocket 2025-05-09 12:45:53 +04:00
PVBLIC Foundation b38711a581 Update pinecone.py 2025-05-08 16:02:47 -07:00
PVBLIC Foundation 04b9065f08 Update pinecone.py
Now supports batched insert, upsert, and delete operations using a default batch size of 100, reducing API strain and improving throughput. All blocking calls to the Pinecone API are wrapped in asyncio.to_thread(...), ensuring async safety and preventing event loop blocking. The implementation includes zero-vector handling for efficient metadata-only queries, normalized cosine distance scores for accurate ranking, and protections against empty input operations. Logs for batch durations have been streamlined to minimize noise, while preserving key info-level success logs.
2025-05-08 15:53:30 -07:00
Timothy Jaeryang Baek 827326e1a2 refac: audio transcription issue 2025-05-08 22:57:48 +04:00
Timothy Jaeryang Baek bfa5550cc3 refac: openai already supports webm audio 2025-05-08 22:44:32 +04:00
Tim Jaeryang Baek e60fa7927d Merge pull request #13604 from Acters/dev
feat: Improve Default Code Interpreter Prompt
2025-05-08 22:14:29 +04:00
Tim Jaeryang Baek 1fea4f794f Merge pull request #13670 from HarrisonConsulting/fix/milvus-standalone-index
fix: enhance MilvusClient with dynamic index type and improved logging
2025-05-08 22:08:23 +04:00
Timothy Jaeryang Baek 7c8597765f refac/fix: ldap issue 2025-05-08 22:06:44 +04:00
Timothy Jaeryang Baek 08b6ea0e17 refac 2025-05-08 20:47:41 +04:00
Tim Jaeryang Baek 68f5ff540c Merge pull request #13695 from itk-dev/8908-focus-trap-modal
feat: focus trap modal
2025-05-08 20:44:36 +04:00
Tim Jaeryang Baek 9509528deb Merge pull request #13703 from hurxxxx/fix/onedrive-org-picker-error
fix: Add SharePoint tenant ID to OneDrive settings
2025-05-08 20:41:06 +04:00
hurxxxx 4ca0ea1e55 Add SharePoint tenant ID to OneDrive settings 2025-05-09 00:35:23 +09:00
Sine Jespersen 3886dee1ce add whitespace and semi colon 2025-05-08 13:29:24 +02:00
Sine Jespersen 305435b4ef use npm package focus-trap instead of npm package focus-trap-svelte 2025-05-08 13:22:47 +02:00
Matt Harrison 4abe2cfa7b fix: Specified "HNSW" for backwards compatibility prefering cluster environments over standalone. 2025-05-08 07:21:34 -04:00
Tim Jaeryang Baek a6d6c928d9 Merge pull request #13567 from tth37/fix_katex_chinese_delimiters
fix(katex): Allow CJK characters adjacent to math delimiters
2025-05-08 14:41:16 +04:00
Timothy Jaeryang Baek af518cacbf refac: filter out images 2025-05-08 12:43:13 +04:00
Tim Jaeryang Baek 54e4c1e0cc Merge pull request #13675 from feddersen-group/task/remove_files_from_tasks
perf: remove files from tasks
2025-05-08 12:38:39 +04:00
Tim Jaeryang Baek f6b20ca109 Merge pull request #13677 from silentoplayz/dev
refactor: Improve Default Title Generation Prompt Template for robust JSON output
2025-05-08 12:38:05 +04:00
silentoplayz 994f17b430 Update config.py
Update DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE
2025-05-08 07:31:51 +00:00
Alexander Grimm a655bb5a63 skip checks in title gen 2025-05-08 06:55:43 +00:00
Alexander Grimm cf5201aada remove files from message for tasks 2025-05-08 06:50:21 +00:00
Matt Harrison 2df9f7fb4d fix: remove import for os module in milvus.py 2025-05-08 00:28:24 -04:00
Matt Harrison 731251d11a refac: streamline Milvus index type handling using configuration options 2025-05-07 23:39:56 -04:00
Matt Harrison 2b8be86f23 feat: add Milvus configuration options for index and metric types 2025-05-07 23:32:58 -04:00
Matt Harrison 5e46c27806 refac: enhance MilvusClient with dynamic index type and improved logging 2025-05-07 21:51:28 -04:00
Timothy Jaeryang Baek b34401a087 refac/fix: notes hide hidden models 2025-05-07 23:55:20 +04:00
Timothy Jaeryang Baek 61a9d9927d refac/fix: message input rtl issue 2025-05-07 23:14:42 +04:00
Timothy Jaeryang Baek f761d2cf90 refac: allow html video controls 2025-05-07 23:06:00 +04:00
Timothy Jaeryang Baek 0f40cb0de4 feat: Vazirmatn font for Persian and Arabic languages 2025-05-07 22:31:59 +04:00
Timothy Jaeryang Baek b50dcb1862 refac: remove duplicate urls 2025-05-07 22:25:18 +04:00
Tim Jaeryang Baek b9f55a44b7 Merge pull request #13346 from dh4340/patch-1
Fix: Use dynamic Python command to run uvicorn and support pyenv setups.
2025-05-07 22:08:35 +04:00
Tim Jaeryang Baek f1bb43707e Merge pull request #13566 from itk-dev/8908-accessibility-enhancements
feat: Accessibility enhancements
2025-05-07 22:07:57 +04:00
Tim Jaeryang Baek 519bb64238 Merge pull request #13623 from panda44312/patch-15
i18n: zh-cn
2025-05-07 22:06:54 +04:00
Tim Jaeryang Baek 410af53eca Merge pull request #13581 from kaytwo/refreshOauthPfp
feat: refresh oauth profile picture
2025-05-07 22:06:43 +04:00
Tim Jaeryang Baek c2de7efa2f Merge pull request #13637 from mMabeck/my-dev
i18n: Update Danish translations (da-DK)
2025-05-07 22:02:29 +04:00
Tim Jaeryang Baek 6424c8b644 Merge pull request #13643 from suleimanelkhoury/S3-tagging-boto3
fix: Add S3_ENABLE_TAGGING env variable and fix S3_USE_ACCELERATE_ENDPOINT Type Error
2025-05-07 22:02:14 +04:00
Sine Jespersen bd2021e8e1 add aria modal true and role dialog to modal 2025-05-07 15:17:11 +02:00
Suleiman Elkhoury ccc64ac6b7 Update provider.py
Add S3_ENABLE_TAGGING to add tags optionally based on the env file.
2025-05-07 14:48:49 +02:00
Suleiman Elkhoury 7620a6bf8c Update config.py
Fixed S3_USE_ACCELERATE_ENDPOINT variable from "False" to "false" since boto3 expects a Python bool, not a string.
Added S3_ENABLE_TAGGING env variable to avoid incompatiblilty with Storages that does not support tagging (like Cloudflare R2)
2025-05-07 14:46:58 +02:00
Sine Jespersen 54b30e0b7f use svelte-focus-trap in modal 2025-05-07 14:21:56 +02:00
Sine Jespersen f56983632f add npm package svelte-focus-trap 2025-05-07 14:20:06 +02:00
Mabeck 3a553f7208 Updated danish translations 2025-05-07 14:06:30 +02:00
Panda 571e4c9600 Merge branch 'dev' into patch-15 2025-05-07 11:18:13 +02:00
Timothy Jaeryang Baek a38f3c7617 refac 2025-05-07 13:10:24 +04:00
Timothy Jaeryang Baek b1fb298bee enh: contributions stats script 2025-05-07 13:09:41 +04:00
Timothy Jaeryang Baek 176a7fc986 refac 2025-05-07 12:52:55 +04:00
Tim Jaeryang Baek efd50c3c5a Merge pull request #13607 from ssakar/dev
feat: add presence_penalty parameter to openai (Qwen3)
2025-05-07 12:48:23 +04:00
Sine Jespersen 263086aa91 break and indent divs 2025-05-07 09:21:04 +02:00
Tim Jaeryang Baek a3bb7df610 Merge pull request #13530 from open-webui/dev
0.6.7
2025-05-07 03:08:25 +04:00
Timothy Jaeryang Baek 3a6a05cef1 doc: changelog 2025-05-07 03:05:49 +04:00
Serkan Sakar d62353c140 enh: add presence_penalty parameter to openai 2025-05-07 00:56:20 +02:00
Timothy Jaeryang Baek 803b39b00c refac 2025-05-07 02:45:00 +04:00
Timothy Jaeryang Baek ed5de96d1d fix: tool servers ui settings can be bypassed with search 2025-05-07 02:41:33 +04:00
Timothy Jaeryang Baek c667d27c38 refac 2025-05-07 02:37:23 +04:00
acters e088f1ef23 Merge branch 'open-webui:dev' into dev 2025-05-06 16:17:54 -06:00
Timothy Jaeryang Baek ad1486c8bd refac 2025-05-07 02:06:18 +04:00
Timothy Jaeryang Baek 6359cb55fe chore: format 2025-05-07 02:01:03 +04:00
Timothy Jaeryang Baek 96618967bd refac 2025-05-07 02:00:10 +04:00
Tim Jaeryang Baek 19bf7289c1 Merge pull request #13603 from TiancongLx/dev
i18n: complete empty values and **fix a typo** in zh-TW
2025-05-07 01:53:16 +04:00
Timothy Jaeryang Baek 369f461691 refac 2025-05-07 01:48:54 +04:00
acters 9c760ace80 Improve Default Code Interpreter Prompt
The change to the default "Code Interpreter" prompt makes it more clear to the LLM to not wrap the code in markdown's code block formatting. This improves the reliability of having code generated in the XML tags that will run properly.
2025-05-06 15:36:17 -06:00
Tiancong Li 45e9d05240 i18n: update zh-TW 2025-05-07 05:16:44 +08:00
Timothy Jaeryang Baek ace3303ff5 enh: TOOL_SERVER_CONNECTIONS env var support 2025-05-07 01:11:28 +04:00
Timothy Jaeryang Baek 62e57a4cf5 refac: auth endpoint 2025-05-07 00:54:53 +04:00
Tim Jaeryang Baek 2a4dfc02a2 Merge pull request #13540 from NoMoreFood/dev
feat: Azure TTS Allow Base URL
2025-05-07 00:49:57 +04:00
Tim Jaeryang Baek e0e151f4f4 Merge pull request #13583 from torisetxd/torisetxd-image-patch
Fix: Fix Image parameter for Automatic1111 `/generations` endpoint
2025-05-07 00:45:08 +04:00
Tim Jaeryang Baek ea07e242f5 Merge pull request #13528 from Classic298/dev
feat: Enhance YouTube Transcription Loader for multi-language support
2025-05-07 00:44:45 +04:00
Timothy Jaeryang Baek 53f2ab1676 chore: wording 2025-05-06 20:15:09 +04:00
Chris Kanich 500f4d73e1 refresh oauth profile picture 2025-05-06 11:00:35 -05:00
toriset 6c957f2407 Fixed mis-use of set_image_model in automatic1111 Image generation
A custom model parameter would always throw an error.. :/
2025-05-06 18:17:52 +03:00
Classic298 1dcbec71ec Update youtube.py 2025-05-06 17:14:00 +02:00
Classic298 87dcbd198c Update youtube.py 2025-05-06 17:11:03 +02:00
Classic298 d7927506f1 Update youtube.py 2025-05-06 17:06:21 +02:00
Classic298 f65dc715f9 Update youtube.py 2025-05-06 16:30:18 +02:00
Classic298 c69278c13c Update youtube.py 2025-05-06 16:24:27 +02:00
Classic298 a129e0954e Update youtube.py 2025-05-06 16:22:40 +02:00
Classic298 5e1cb76b93 Update youtube.py 2025-05-06 16:16:58 +02:00
tth37 8e14372dd8 enh: Support more languages 2025-05-06 21:26:09 +08:00
Sine Jespersen 9d8d4393a6 remove alt text from logo, as this is decorative and provides no value for assistive tech users 2025-05-06 14:37:35 +02:00
Sine Jespersen c4a3a9c616 fix text-contrast from 3.71 to 7.55 on dark mode 2025-05-06 14:37:05 +02:00
Sine Jespersen 8c6f4f6579 change divs to labels, as this is what they semantically are 2025-05-06 14:33:26 +02:00
Sine Jespersen 86ce111edd Add aria-labelledby to icon-button to make assistive tech able to give info on what user can expect from button 2025-05-06 14:30:01 +02:00
Sine Jespersen 0889dd3a51 add close label to close button in changelog modal, only accessible for screen readers 2025-05-06 14:24:19 +02:00
tth37 de182ddec2 fix(katex): Allow Chinese characters adjacent to math delimiters 2025-05-06 18:38:42 +08:00
Panda bfdcc12cdc i18n: zh-cn 2025-05-06 08:30:28 +02:00
Tim Jaeryang Baek ed210517c2 Merge pull request #13533 from TheRauch1/main
feat: handle user OAuth groups as list or string
2025-05-06 09:57:14 +04:00
Tim Jaeryang Baek 7b5247810c Merge pull request #13542 from tuzkiyoung/main
fix: support gpt-image-1 with correct parameter
2025-05-06 09:56:48 +04:00
杨方亮 9cc00afc6b fix: support gpt-image-1 with correct parameter 2025-05-06 10:17:22 +08:00
Bryan Berns 5c055b73a3 Add Custom Azure TTS URL - Formatting 2025-05-05 22:17:18 -04:00
Bryan Berns 853adad894 Add Custom Azure TTS URL - Fix Spaces 2025-05-05 22:12:32 -04:00
Bryan Berns 5aabe21cbe Add Custom Azure TTS URL 2025-05-05 22:08:48 -04:00
Timothy Jaeryang Baek e63b8b3879 refac 2025-05-06 00:46:32 +04:00
Tim Jaeryang Baek 0a8199a056 Merge pull request #13522 from salbahra/bug/fix-missing-api-config
fix: Correct missing `api_config` for embedding Ollama proxy endpoints
2025-05-05 23:49:40 +04:00
Timothy Jaeryang Baek 7b36466c1c refac: audio transcribe supported filetype 2025-05-05 23:42:56 +04:00
Timothy Jaeryang Baek 27da31dc83 fix: tikaloader extract images 2025-05-05 23:40:34 +04:00
therauch1 c36acd46cc enh: handle user OAuth groups as list or string 2025-05-05 21:38:31 +02:00
Tim Jaeryang Baek 413297b5c7 Merge pull request #13532 from Classic298/main
i18n: Update de-DE translation
2025-05-05 23:37:54 +04:00
Classic298 f94965909c Update translation.json 2025-05-05 21:33:45 +02:00
Timothy Jaeryang Baek 982c84c8ac fix: non admin user notes 2025-05-05 23:31:37 +04:00
Timothy Jaeryang Baek 1f45b9df2e fix: notes non admin issue 2025-05-05 23:29:40 +04:00
Tim Jaeryang Baek 3dd2e80dec Merge pull request #13525 from Kylapaallikko/dev
i18n: Update fi-FI translation
2025-05-05 23:28:04 +04:00
Classic298 67a612fe24 Update youtube.py 2025-05-05 20:40:48 +02:00
Classic298 791dd24ace Update youtube.py 2025-05-05 20:08:25 +02:00
Classic298 9cf3381381 Update youtube.py 2025-05-05 20:07:52 +02:00
Classic298 b0d74a59f1 Update youtube.py 2025-05-05 20:07:37 +02:00
Classic298 1a30b3746e Update youtube.py 2025-05-05 20:03:00 +02:00
Classic298 0a3817ed86 Update youtube.py 2025-05-05 20:00:10 +02:00
Classic298 0a845db8ec Update youtube.py 2025-05-05 19:57:21 +02:00
Classic298 7680ac2517 Update youtube.py 2025-05-05 19:57:06 +02:00
Kylapaallikko b3eb3b3c05 Update fi-FI translation.json 2025-05-05 20:21:23 +03:00
Samer Albahra d5e866d839 Fix missing api_config for embedding Ollama proxy endpoints 2025-05-05 12:33:07 -04:00
Timothy Jaeryang Baek db0a0b395f fix: oauth 2025-05-05 19:38:36 +04:00
Tim Jaeryang Baek 23b9354cf6 Merge pull request #12932 from open-webui/dev
0.6.6
2025-05-05 17:58:52 +04:00
Timothy Jaeryang Baek 4fa2e136bf refac: styling 2025-05-05 17:58:40 +04:00
Timothy Jaeryang Baek 512d475ecc refac: api usage 2025-05-05 17:43:51 +04:00
Timothy Jaeryang Baek 56cefde161 doc: changelog 2025-05-05 17:13:09 +04:00
Timothy Jaeryang Baek f78811b253 chore: bump 2025-05-05 16:49:21 +04:00
Timothy Jaeryang Baek e6ca7a414b refac 2025-05-05 16:41:51 +04:00
Tim Jaeryang Baek 367f6fb8f1 Merge pull request #13498 from omahs/main
fix: typos
2025-05-05 16:27:33 +04:00
omahs 37e0313c83 Merge branch 'dev' into main 2025-05-05 14:26:40 +02:00
Timothy Jaeryang Baek 267889809d chore: format 2025-05-05 16:19:05 +04:00
Timothy Jaeryang Baek 33c206c15c enh: include profile image during user import 2025-05-05 16:17:20 +04:00
omahs 863f227be9 fix: typos 2025-05-05 14:14:59 +02:00
Timothy Jaeryang Baek abdde10b74 refac 2025-05-05 16:12:41 +04:00
Timothy Jaeryang Baek 26e9cd0b4b enh: user role update confirm dialog 2025-05-05 16:10:36 +04:00
Timothy Jaeryang Baek 457a0f318a refac 2025-05-05 16:05:39 +04:00
Timothy Jaeryang Baek 3771d27a52 refac 2025-05-05 15:50:09 +04:00
Timothy Jaeryang Baek d16ff54895 refac 2025-05-05 15:46:55 +04:00
Timothy Jaeryang Baek de0f52b686 doc: readme 2025-05-05 15:42:14 +04:00
Timothy Jaeryang Baek eec7a2afa1 refac 2025-05-05 11:25:00 +04:00
Timothy Jaeryang Baek 9e1400f055 enh: generate title 2025-05-05 10:36:01 +04:00
Timothy Jaeryang Baek aa374823ce refac 2025-05-05 10:16:34 +04:00
Timothy Jaeryang Baek a20a920ca4 refac: styling 2025-05-05 09:41:25 +04:00
Timothy Jaeryang Baek 827fd6606c refac 2025-05-05 09:36:34 +04:00
Timothy Jaeryang Baek deb1f6b6d7 feat: ai enhanced notes 2025-05-05 09:22:54 +04:00
Tim Jaeryang Baek 0f1df43bb2 Merge pull request #13485 from TiancongLx/dev
i18n: update zh-TW
2025-05-05 08:25:24 +04:00
Tiancong Li 62a5b14642 i18n: update zh-TW 2025-05-05 10:15:20 +08:00
Timothy Jaeryang Baek 9faf2d9849 refac: styling 2025-05-05 01:51:51 +04:00
Timothy Jaeryang Baek f24d0085c2 refac 2025-05-05 01:45:55 +04:00
Timothy Jaeryang Baek 463ac99e39 enh: note versioning 2025-05-05 01:35:43 +04:00
Timothy Jaeryang Baek e8b23ef0c1 refac 2025-05-05 00:05:06 +04:00
Timothy Jaeryang Baek a6a7ce6b68 enh: import md notes 2025-05-04 23:04:15 +04:00
Timothy Jaeryang Baek 8514669053 refac 2025-05-04 18:59:36 +04:00
Timothy Jaeryang Baek 291834ecbe chore: format 2025-05-04 18:32:56 +04:00
Timothy Jaeryang Baek 360b5b303a enh: note editor 2025-05-04 18:32:21 +04:00
Timothy Jaeryang Baek d8e9b000e3 enh: upload audio 2025-05-04 18:26:33 +04:00
Timothy Jaeryang Baek 62366ad020 refac: audio recording 2025-05-04 18:17:35 +04:00
Timothy Jaeryang Baek 2946f5fcce refac: styling 2025-05-04 17:33:15 +04:00
Timothy Jaeryang Baek f2cf21d64c enh: file item display audio 2025-05-04 17:29:59 +04:00
Timothy Jaeryang Baek 6475ae3b11 refac 2025-05-04 17:23:42 +04:00
Timothy Jaeryang Baek 84a05bec7b enh: notes user group permission 2025-05-04 17:22:51 +04:00
Timothy Jaeryang Baek f716968009 enh: delete notes 2025-05-04 17:01:12 +04:00
Timothy Jaeryang Baek 18149483f2 refac: note pdf export 2025-05-04 16:59:41 +04:00
Timothy Jaeryang Baek 175bbe27e9 refac 2025-05-04 16:53:48 +04:00
Tim Jaeryang Baek d1eaa9d4ee Merge pull request #13478 from SadmL/dev
[i18n] Russian locale update
2025-05-04 02:24:38 -07:00
SadmL 9b133a2245 misspell fix 2025-05-04 12:05:21 +03:00
SadmL 0e51b75654 Russian locale update 2025-05-04 11:59:13 +03:00
Timothy Jaeryang Baek 1cf9843468 refac 2025-05-04 12:02:10 +04:00
Timothy Jaeryang Baek dd42a10aa1 refac 2025-05-04 12:01:20 +04:00
Tim Jaeryang Baek 60c0aa36a2 Merge pull request #13470 from TiancongLx/dev
i18n: update zh-TW
2025-05-04 00:41:02 -07:00
Tiancong Li a7cbfeec9f i18n: update zh-TW 2025-05-04 11:58:20 +08:00
Timothy Jaeryang Baek c5c98591f9 refac 2025-05-04 00:20:30 +04:00
Timothy Jaeryang Baek 59e52add63 chore: bump 2025-05-04 00:15:46 +04:00
Timothy Jaeryang Baek 9c4a931d22 feat: capture display media audio 2025-05-04 00:14:19 +04:00
Timothy Jaeryang Baek 4cfb99248d chore: format 2025-05-03 23:48:24 +04:00
Timothy Jaeryang Baek aefd5d9557 chore: format 2025-05-03 23:48:12 +04:00
Timothy Jaeryang Baek 0f14f34dbe refac 2025-05-03 23:47:41 +04:00
Timothy Jaeryang Baek d74809853d enh: note files 2025-05-03 23:29:19 +04:00
Timothy Jaeryang Baek 23da63b06a refac: styling 2025-05-03 23:08:28 +04:00
Timothy Jaeryang Baek 116b61c53b refac 2025-05-03 23:06:46 +04:00
Timothy Jaeryang Baek f43cb87556 refac 2025-05-03 23:05:25 +04:00
Tim Jaeryang Baek 1af45435eb Merge pull request #13459 from open-webui/notes
feat: notes
2025-05-03 11:54:04 -07:00
Timothy Jaeryang Baek 4acd278624 refac 2025-05-03 22:53:23 +04:00
Timothy Jaeryang Baek 1d82a1c8c4 feat: note editor 2025-05-03 21:18:19 +04:00
Timothy Jaeryang Baek 52d32e8bf2 refac: styling 2025-05-03 21:02:21 +04:00
Timothy Jaeryang Baek a74297ed47 refac: styling 2025-05-03 18:56:27 +04:00
Timothy Jaeryang Baek 7de6112c5b feat: note list ui 2025-05-03 18:52:13 +04:00
Tim Jaeryang Baek bfacab5a0c Merge pull request #13447 from athoik/dev
feat(ocr): add support for Docling OCR engine and language configuration
2025-05-03 07:18:05 -07:00
Tim Jaeryang Baek bbc1ae0942 Merge pull request #13441 from salbahra/bug/fix-prefix-for-embedding-model
fix: Incorrect model reference with Ollama proxy when prefixes are used
2025-05-03 07:17:40 -07:00
Timothy Jaeryang Baek 7fee84c06e feat: notes 2025-05-03 18:16:32 +04:00
Athanasios Oikonomou 437804a2f8 fix: update validation logic for Docling OCR engine and language requirements
Both Docling OCR Engine and Language(s) must be provided or both left empty.
2025-05-03 08:12:58 +03:00
Athanasios Oikonomou 4801430ad2 fix: correct condition for Docling OCR engine and language validation
Both must have value or both must be empty.
2025-05-03 08:02:00 +03:00
Athanasios Oikonomou 657162e96d feat(ocr): add support for Docling OCR engine and language configuration
This commit adds support for configuring the OCR engine and language(s) for Docling.
Configuration can be set via the environment variables `DOCLING_OCR_ENGINE` and `DOCLING_OCR_LANG`, or through the UI.

Fixes #13133
2025-05-03 00:32:06 +03:00
Samer Albahra cafba3eaff Fix model reference with Ollama proxy when prefixes are used 2025-05-02 11:30:59 -04:00
Tim Jaeryang Baek 7d184c3a14 Merge pull request #13085 from ayan4m1/fix/tika-image-ocr
fix: pass extractInlineImages header to Tika if PDF_EXTRACT_IMAGES is true
2025-05-02 03:47:51 -07:00
Timothy Jaeryang Baek 754f631a07 feat: OAUTH_BLOCKED_GROUPS support 2025-05-02 14:47:02 +04:00
Timothy Jaeryang Baek c1ddb00377 refac: styling 2025-05-02 14:19:53 +04:00
Timothy Jaeryang Baek 74f160b849 refac 2025-05-02 14:16:11 +04:00
Tim Jaeryang Baek 11d599c55d Merge pull request #13111 from tth37/perf_multi_thread_web_searching
perf: Concurrent processing for web search queries
2025-05-02 03:15:49 -07:00
Tim Jaeryang Baek 7b014e44ee Merge pull request #13376 from Thaniel94/add-whisper-language-constraint
feat: Added WHISPER_LANGUAGE env variable
2025-05-02 03:08:00 -07:00
Timothy Jaeryang Baek 4a0b2a3afc chore: bump 2025-05-02 14:06:57 +04:00
Tim Jaeryang Baek 2d5643fb17 Merge pull request #13405 from yak1ex/fix_multiple_actions_in_an_action_module_do_not_work_as_intended
fix: Multiple actions in an action module do not work as intended.
2025-05-02 03:06:05 -07:00
Tim Jaeryang Baek d522914774 Merge pull request #13413 from hurxxxx/fix/eslint-version-mismatch
fix: eslint version mismatch
2025-05-02 03:05:30 -07:00
Tim Jaeryang Baek 3013ce2d1a Merge pull request #13415 from TiancongLx/dev
i18n: update zh-TW
2025-05-02 03:04:39 -07:00
Timothy Jaeryang Baek 6d81eef425 refac: reasoning detection 2025-05-02 14:04:12 +04:00
Timothy Jaeryang Baek c258272709 refac 2025-05-02 13:37:41 +04:00
Tim Jaeryang Baek a0981e01a0 Merge pull request #13417 from flefevre/dev-i8n-french
i18n french
2025-05-02 02:28:11 -07:00
hurxxxx 10243b0fac chore: bump @typescript-eslint/eslint-plugin to 8.31.1 2025-05-02 09:44:51 +09:00
nathaniel cc14aacaaa Improvements to parsing of WHISPER_LANGUAGE environment variable (Setting as empty string now equivalent to unsetting/removing WHISPER_LANGUAGE). Resolved crash caused when unset 2025-05-01 22:03:49 +01:00
nathaniel ef7acfbf3d WHISPER_LANGUAGE no longer a "PersistentConfig" variable (Was not appropriate with how WHISPER_LANGUAGE is currently configured). 2025-05-01 21:33:57 +01:00
flefevre fb8a7e8549 Merge branch 'dev' into dev-i8n-french 2025-05-01 20:17:50 +02:00
Francois LE FEVRE 754a3c4cb9 last elements to translate into french 2025-05-01 20:07:56 +02:00
Tiancong Li 725761ad6b i18n: update zh-TW 2025-05-01 22:56:47 +08:00
Francois LE FEVRE 57cb030a24 seconde review on french translation 2025-05-01 15:59:52 +02:00
Francois LE FEVRE 12626107b6 first elements translated in french 2025-05-01 15:46:58 +02:00
hurxxxx 72f985aba7 fix: align eslint plugin and parser versions 2025-05-01 22:39:29 +09:00
Timothy Jaeryang Baek 5d5e351937 feat: notes scaffolding 2025-05-01 16:39:36 +04:00
Tim Jaeryang Baek 2062d3434e Merge pull request #13385 from open-webui/dependabot/npm_and_yarn/dev/typescript-eslint/parser-8.31.1
build(deps-dev): bump @typescript-eslint/parser from 6.21.0 to 8.31.1
2025-05-01 04:59:00 -07:00
Timothy Jaeryang Baek f26cafd984 chore: langchain bump 2025-05-01 15:58:45 +04:00
Tim Jaeryang Baek aca82d64a0 Merge pull request #13391 from open-webui/dependabot/pip/backend/dev/rapidocr-onnxruntime-1.4.4
build(deps): bump rapidocr-onnxruntime from 1.3.24 to 1.4.4 in /backend
2025-05-01 04:56:32 -07:00
Tim Jaeryang Baek 3715919a6b Merge pull request #13389 from open-webui/dependabot/npm_and_yarn/dev/codemirror-lang-hcl-0.1.0
build(deps): bump codemirror-lang-hcl from 0.0.0-beta.2 to 0.1.0
2025-05-01 04:55:52 -07:00
Tim Jaeryang Baek 0e3b9f9da1 Merge pull request #13388 from open-webui/dependabot/npm_and_yarn/dev/prosemirror-schema-list-1.5.1
build(deps): bump prosemirror-schema-list from 1.4.1 to 1.5.1
2025-05-01 04:55:45 -07:00
Tim Jaeryang Baek f10e7ac81b Merge pull request #13387 from open-webui/dependabot/npm_and_yarn/dev/tiptap/core-2.11.9
build(deps): bump @tiptap/core from 2.10.0 to 2.11.9
2025-05-01 04:55:36 -07:00
Tim Jaeryang Baek 61580e9490 Merge pull request #13404 from NoMoreFood/dev
fix: Use SHA256 For Query Result Computation
2025-05-01 04:55:16 -07:00
Tim Jaeryang Baek 6f2d61dc4f Merge pull request #13398 from hurxxxx/i18n-korean
i18n: Update Korean translation.json and parsed messages
2025-05-01 04:48:56 -07:00
Bryan Berns 32257089f9 Use SHA256 For Query Result Computation 2025-05-01 03:56:20 -04:00
Yak! b5cfa2b992 fix: Multiple actions in an action module do not work as intended. 2025-05-01 16:41:06 +09:00
dependabot[bot] c826717343 build(deps): bump @tiptap/core from 2.10.0 to 2.11.9
Bumps [@tiptap/core](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/core) from 2.10.0 to 2.11.9.
- [Release notes](https://github.com/ueberdosis/tiptap/releases)
- [Changelog](https://github.com/ueberdosis/tiptap/blob/@tiptap/core@2.11.9/packages/core/CHANGELOG.md)
- [Commits](https://github.com/ueberdosis/tiptap/commits/@tiptap/core@2.11.9/packages/core)

---
updated-dependencies:
- dependency-name: "@tiptap/core"
  dependency-version: 2.11.9
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-01 05:44:35 +00:00
Tim Jaeryang Baek 35a64b55a1 Merge pull request #13386 from open-webui/dependabot/npm_and_yarn/dev/tiptap/extension-code-block-lowlight-2.11.9
build(deps): bump @tiptap/extension-code-block-lowlight from 2.10.0 to 2.11.9
2025-04-30 22:43:30 -07:00
Tim Jaeryang Baek 9695e08411 Merge pull request #13392 from open-webui/dependabot/pip/backend/dev/pytest-approx-eq-8.3.5
build(deps): update pytest requirement from ~=8.3.2 to ~=8.3.5 in /backend
2025-04-30 22:43:09 -07:00
Tim Jaeryang Baek c6d76acf76 Merge pull request #13393 from open-webui/dependabot/pip/backend/dev/elasticsearch-9.0.1
build(deps): bump elasticsearch from 8.17.1 to 9.0.1 in /backend
2025-04-30 22:42:55 -07:00
Tim Jaeryang Baek b6a76dc026 Merge pull request #13394 from open-webui/dependabot/pip/backend/dev/loguru-0.7.3
build(deps): bump loguru from 0.7.2 to 0.7.3 in /backend
2025-04-30 22:42:45 -07:00
Timothy Jaeryang Baek 079af5fffe fix: send webhook notification when user is not active 2025-05-01 09:41:07 +04:00
Tim Jaeryang Baek e57f2c928a Merge pull request #13379 from NoMoreFood/dev
feat: Azure STT Allow Base URL & Max Speaker Setting
2025-04-30 22:26:18 -07:00
hurxxxx 513d5c8bf5 Fix typos 2025-05-01 13:21:15 +09:00
hurxxxx d72e41c563 Consolidate duplicate messages into a single one 2025-05-01 13:16:47 +09:00
hurxxxx 1ca5d532b8 chore(i18n): update parsed i18n data and Korean translations 2025-05-01 13:00:27 +09:00
hurxxxx f46c7d53b7 update Korean translation 2025-05-01 12:55:07 +09:00
hurxxxx a7cbf62c68 update Korean translation 2025-05-01 12:43:19 +09:00
dependabot[bot] 4eaaeb1888 build(deps): bump loguru from 0.7.2 to 0.7.3 in /backend
Bumps [loguru](https://github.com/Delgan/loguru) from 0.7.2 to 0.7.3.
- [Release notes](https://github.com/Delgan/loguru/releases)
- [Changelog](https://github.com/Delgan/loguru/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/Delgan/loguru/compare/0.7.2...0.7.3)

---
updated-dependencies:
- dependency-name: loguru
  dependency-version: 0.7.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-01 02:54:59 +00:00
dependabot[bot] 753d530cd0 build(deps): bump elasticsearch from 8.17.1 to 9.0.1 in /backend
Bumps [elasticsearch](https://github.com/elastic/elasticsearch-py) from 8.17.1 to 9.0.1.
- [Release notes](https://github.com/elastic/elasticsearch-py/releases)
- [Commits](https://github.com/elastic/elasticsearch-py/compare/v8.17.1...v9.0.1)

---
updated-dependencies:
- dependency-name: elasticsearch
  dependency-version: 9.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-01 02:54:56 +00:00
dependabot[bot] 10f55d39d2 build(deps): update pytest requirement in /backend
Updates the requirements on [pytest](https://github.com/pytest-dev/pytest) to permit the latest version.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.3.2...8.3.5)

---
updated-dependencies:
- dependency-name: pytest
  dependency-version: 8.3.5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-01 02:54:52 +00:00
dependabot[bot] 0b84863d76 build(deps): bump rapidocr-onnxruntime from 1.3.24 to 1.4.4 in /backend
Bumps [rapidocr-onnxruntime](https://github.com/RapidAI/RapidOCR) from 1.3.24 to 1.4.4.
- [Release notes](https://github.com/RapidAI/RapidOCR/releases)
- [Commits](https://github.com/RapidAI/RapidOCR/compare/v1.3.24...v1.4.4)

---
updated-dependencies:
- dependency-name: rapidocr-onnxruntime
  dependency-version: 1.4.4
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-01 02:54:49 +00:00
dependabot[bot] efc163fede build(deps): bump codemirror-lang-hcl from 0.0.0-beta.2 to 0.1.0
Bumps codemirror-lang-hcl from 0.0.0-beta.2 to 0.1.0.

---
updated-dependencies:
- dependency-name: codemirror-lang-hcl
  dependency-version: 0.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-01 02:53:31 +00:00
dependabot[bot] dc5107a473 build(deps): bump prosemirror-schema-list from 1.4.1 to 1.5.1
Bumps [prosemirror-schema-list](https://github.com/prosemirror/prosemirror-schema-list) from 1.4.1 to 1.5.1.
- [Changelog](https://github.com/ProseMirror/prosemirror-schema-list/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prosemirror/prosemirror-schema-list/compare/1.4.1...1.5.1)

---
updated-dependencies:
- dependency-name: prosemirror-schema-list
  dependency-version: 1.5.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-01 02:53:26 +00:00
dependabot[bot] c5fd40d951 build(deps): bump @tiptap/extension-code-block-lowlight
Bumps [@tiptap/extension-code-block-lowlight](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-code-block-lowlight) from 2.10.0 to 2.11.9.
- [Release notes](https://github.com/ueberdosis/tiptap/releases)
- [Changelog](https://github.com/ueberdosis/tiptap/blob/@tiptap/extension-code-block-lowlight@2.11.9/packages/extension-code-block-lowlight/CHANGELOG.md)
- [Commits](https://github.com/ueberdosis/tiptap/commits/@tiptap/extension-code-block-lowlight@2.11.9/packages/extension-code-block-lowlight)

---
updated-dependencies:
- dependency-name: "@tiptap/extension-code-block-lowlight"
  dependency-version: 2.11.9
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-01 02:53:05 +00:00
dependabot[bot] 317032aa5c build(deps-dev): bump @typescript-eslint/parser from 6.21.0 to 8.31.1
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 6.21.0 to 8.31.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.31.1/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.31.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-01 02:52:50 +00:00
Tim Jaeryang Baek f3b38e9777 Merge pull request #13372 from therealmichaelberna/main
**fix** convert_function_to_pydantic_model returning empty descriptions
2025-04-30 13:36:22 -07:00
Tim Jaeryang Baek 336d20fa33 Merge pull request #13364 from NovoNordisk-OpenSource/websocket-behind-auth
refactor: hiding websocket initalization behind authentication
2025-04-30 13:35:27 -07:00
Timothy Jaeryang Baek dadfa5f043 refac: HTML_FILE_ID handling 2025-05-01 00:32:29 +04:00
Timothy Jaeryang Baek e52e45d613 refac: collapsible 2025-04-30 22:26:07 +04:00
Michael Berna 4a6b964a51 Fixed convert_function_to_pydantic_model
convert_function_to_pydantic_model was returning empty function descriptions. I have renamed the variables more precisely which helps to avoid confusion and also resolved this issue which was caused by a variable naming conflict.
2025-04-30 14:22:38 -04:00
Timothy Jaeryang Baek 41904f21e4 refac 2025-04-30 19:41:16 +04:00
Timothy Jaeryang Baek ef2aeb7c0e fix: only allow admin uploaded html to be rendered as html 2025-04-30 19:34:53 +04:00
shcodenick 4c81b6ebc5 hiding websocket initalization behind authentication 2025-04-30 15:05:57 +02:00
Timothy Jaeryang Baek 783d409b1d refac 2025-04-30 16:58:38 +04:00
Timothy Jaeryang Baek c1fa989cb8 refac: styling 2025-04-30 16:54:51 +04:00
Bryan Berns 6c8a9d000e Azure STT Allow Base URL & Max Speaker Setting 2025-04-30 08:51:01 -04:00
Timothy Jaeryang Baek f9d238e850 refac: user list sub-standard code 2025-04-30 16:49:41 +04:00
Tim Jaeryang Baek db06a925fe Merge pull request #13267 from AnapatChaiwongse/dev
feat: implement pagination for /admin/users
2025-04-30 05:04:14 -07:00
Timothy Jaeryang Baek eb68b21e0f refac 2025-04-30 16:01:23 +04:00
Tim Jaeryang Baek e822984162 Merge pull request #13358 from feddersen-group/fix/load_ext_dependencies_on_startup
fix: install external requirements of tools and functions to prevent deactivation
2025-04-30 04:59:56 -07:00
Alexander Grimm e80ed32aeb fix: install external requirements of admin functions and tools on startups 2025-04-30 11:14:38 +00:00
Tim Jaeryang Baek c9d3bb35c3 Merge pull request #13342 from feddersen-group/feat/embedding_dim
feat: allow pgvector to use large embedding models
2025-04-30 02:41:26 -07:00
Alexander Grimm da9966aca1 ~ truncate vectors for pgvector if too big 2025-04-30 05:35:17 +00:00
Tim Jaeryang Baek 4ee5dd58b7 Merge pull request #13177 from tth37/fix_firecrawl_loader_default_mode
fix: FireCrawlLoader default mode to scrape
2025-04-29 08:39:06 -07:00
Tim Jaeryang Baek e87f2669fa Merge pull request #13191 from tth37/feat_firecrawl_search_engine
feat: Add Firecrawl search engine
2025-04-29 08:38:28 -07:00
Tim Jaeryang Baek 390263742c Merge pull request #13273 from spammenotinoz/dev
**FIX** Update Knowledge ReIndexing to continue upon missing\corrupt collections
2025-04-29 08:37:55 -07:00
Tim Jaeryang Baek 8f4be09dd2 Merge pull request #13315 from aleixdorca/dev
i18n: Update Catalan translation.json
2025-04-29 08:35:28 -07:00
Tim Jaeryang Baek 7b863465a9 Merge pull request #13311 from stephen304/yacy-support
feat: Yacy search support
2025-04-29 08:35:10 -07:00
Timothy Jaeryang Baek 6ae2d2c4ea refac: gpt-image-1 support 2025-04-29 19:34:00 +04:00
Timothy Jaeryang Baek addf44b21f refac 2025-04-29 19:27:54 +04:00
D 8f31b6d74a Fix: Use dynamic Python command to run uvicorn and support pyenv setups.
Updated the uvicorn startup command to dynamically detect and use either 'python3' or 'python',
ensuring compatibility across different environments and pyenv setups.
This avoids issues where only one of the commands is available.
2025-04-29 09:14:23 +01:00
Aleix Dorca bc88a9bc8b Update Catalan translation.json 2025-04-29 07:04:16 +02:00
Tim Jaeryang Baek f7f3ae7cc9 Merge pull request #13306 from tarmst/remove-blocking-websocket-awaits
Perf: Chat Websocket Sequential Awaits
2025-04-28 11:17:50 -07:00
Tim Jaeryang Baek 5a1c4dbe1b Merge pull request #13279 from panda44312/patch-14
i18n: zh-cn
2025-04-28 11:17:24 -07:00
Panda 63e4d1ea41 i18n: fix 2025-04-28 18:22:36 +02:00
tth37 cf2d7de873 refac: Implicit asyncio create_task 2025-04-29 00:20:55 +08:00
tarmst c9a9e05bb4 Fix websocket sequential awaits 2025-04-28 15:17:34 +00:00
Timothy Jaeryang Baek 3ac74a4fcb refac: styling 2025-04-28 18:21:48 +04:00
Timothy Jaeryang Baek 63fe426a52 chore: format 2025-04-28 17:46:05 +04:00
Timothy Jaeryang Baek ee8de1ee0b refac: disable jwt display for non-admin users 2025-04-28 17:45:17 +04:00
Timothy Jaeryang Baek 791c19acf7 enh: unicode char support for user import 2025-04-28 17:26:19 +04:00
Tim Jaeryang Baek a9999833d2 Merge pull request #13185 from Chayud-Ac/feature-thai-i18n
fix(i18n): improve Thai translations
2025-04-28 06:16:20 -07:00
Tim Jaeryang Baek c3da9f3026 Merge pull request #13246 from jmtatsch/fix-video-treated-as-text
Fix uploading video files
2025-04-28 06:15:40 -07:00
Tim Jaeryang Baek 2f5abf7374 Merge pull request #13187 from Mister-Hope/patch-1
feat: update default CUDA version to 12.8, close #13186
2025-04-28 06:15:01 -07:00
Tim Jaeryang Baek dedc45183f Merge pull request #13258 from choket/dev
Fix: Fixed a bug that caused user notification webhooks to never be sent
2025-04-28 06:14:24 -07:00
Tim Jaeryang Baek 6d59a5f809 Merge pull request #13253 from noexcs/dev
fix: The value of the 'Access-Control-Allow-Origin' header in the res…
2025-04-28 06:12:53 -07:00
Timothy Jaeryang Baek 10855cf5ed refac 2025-04-28 17:11:28 +04:00
Timothy Jaeryang Baek 9398c843d3 refac 2025-04-28 17:08:07 +04:00
Timothy Jaeryang Baek 45ad6776de fix: token cookie issue 2025-04-28 17:04:13 +04:00
Tim Jaeryang Baek fb1c101dc6 Merge pull request #13269 from jarrod-lowe/s3-tagging
feat: Add tags to S3 uploaded files
2025-04-28 05:57:08 -07:00
Tim Jaeryang Baek a0273aaea8 Merge pull request #13271 from schulti/patch-1
Update de-DE translation.json
2025-04-28 05:56:22 -07:00
Tim Jaeryang Baek 146a08c68f Merge branch 'dev' into patch-1 2025-04-28 05:49:50 -07:00
Timothy Jaeryang Baek 7d0a78a43a refac: aiohttp trust_env=True 2025-04-28 16:47:34 +04:00
Timothy Jaeryang Baek 9bfb4ae838 fix: ollama proxy "format" 2025-04-28 16:45:23 +04:00
Panda ec496de593 i18n: zh-cn 2025-04-28 11:05:06 +02:00
spammenotinoz 12fd7c8cb9 Update knowledge.py
Improve Error Handling ReIndexing Knowledge Database
2025-04-28 16:39:20 +10:00
Stefan Schultheis, OE1SCS d9186e138b Update de-DE translation.json
added a few translations and corrected some typos
2025-04-28 08:21:21 +02:00
Jarrod Lowe 141810039f Add tags to S3 uploaded files 2025-04-28 16:56:56 +12:00
KaeOhhhh 2112067054 Support pagination in /admin/users API
Update Users.svelte

Update index.ts

Update users.py

Update users.py

Update UserList.svelte
2025-04-28 11:29:42 +07:00
root 8a9de0a62f Fix bug that never triggers user notification webhooks 2025-04-27 21:31:34 +02:00
noexcs 4b9b24ae78 fix: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. 2025-04-27 21:29:13 +08:00
jmtatsch 056a042ef2 Update files.py
fix formatting
2025-04-27 11:18:27 +02:00
jmtatsch c51a91583e Dont attempt to infer charset for video files 2025-04-27 10:59:35 +02:00
nathaniel 1efa708f83 Added WHISPER_LANGUAGE env variable. If set to a country's ISO2, constrains Whisper's stt to that language. Detects language as normal if unset 2025-04-27 05:58:06 +01:00
Stephen Smith ea16426a8d Remove unused kwargs in yacy, update comments. 2025-04-27 00:41:46 -04:00
Stephen Smith f9b9217e98 Set Yacy search to text 2025-04-26 23:13:31 -04:00
Stephen Smith e6d43d70f3 Don't request nav and pass count to Yacy 2025-04-26 23:08:16 -04:00
Stephen Smith 240d91d38d Add yacy config for user/pass, automatically add yacy json api path 2025-04-26 22:28:30 -04:00
Stephen Smith 0f73b96616 first pass at yacy support copied from searxng 2025-04-26 14:07:13 -04:00
Timothy Jaeryang Baek 1349bc47b1 fix: chat message input ui styling 2025-04-25 14:21:58 +09:00
tth37 92dbeb1939 feat: Add Firecrawl search engine 2025-04-24 14:57:28 +08:00
Mister-Hope 5f69424215 Update Dockerfile 2025-04-24 13:59:57 +08:00
Chayud 86b9f252c6 fix(i18n): improve Thai translations 2025-04-24 10:35:02 +07:00
Timothy Jaeryang Baek 0b5514f376 refac 2025-04-24 03:16:46 +09:00
Timothy Jaeryang Baek 28ec3069de enh: exp token check 2025-04-24 03:12:22 +09:00
tth37 8f7195ceda fix: FireCrawlLoader default mode to scrape 2025-04-24 01:17:35 +08:00
Tim Jaeryang Baek 91e758f3ec Merge pull request #13165 from feddersen-group/perf/parallel_knowledge_search
perf: all knowledge searches in parallel in non-hybrid mode
2025-04-23 10:01:06 -07:00
Tim Jaeryang Baek d0a1a0f16b Merge pull request #13119 from jarrod-lowe/suggestions
feat: Set default prompt suggestions from environment variable DEFAULT_PROMPT_SUGGESTIONS
2025-04-23 09:55:57 -07:00
Timothy Jaeryang BaekandDrZoidberg09 732d7aee70 enh: sentence transformers env vars
Co-Authored-By: DrZoidberg09 <96449693+drzoidberg09@users.noreply.github.com>
2025-04-24 01:55:18 +09:00
Timothy Jaeryang Baek 3ec6652f99 chore: dep bump 2025-04-24 01:48:07 +09:00
Tim Jaeryang Baek cad9b292bf Merge pull request #13175 from PeterDaveHello/Improve-zh-TW-locale
i18n: improve zh-TW Traditional Chinese locale
2025-04-23 09:41:23 -07:00
Timothy Jaeryang Baek 09874ab83d fix: FireCrawlLoader 2025-04-24 01:40:34 +09:00
Peter Dave Hello c798adb04c i18n: improve zh-TW Traditional Chinese locale 2025-04-24 00:37:15 +08:00
Timothy Jaeryang Baek 7baca2b483 fix: ollama url model download issue 2025-04-24 01:26:19 +09:00
Tim Jaeryang Baek 2b10b378b6 Merge pull request #13170 from TiancongLx/dev
i18n: update zh-TW
2025-04-23 08:22:27 -07:00
Tim Jaeryang Baek 2652b91f5a Merge pull request #13166 from oleguldberg/update-danish-translation
i18n - Add to Danish Translation
2025-04-23 08:22:17 -07:00
Tiancong Li afdba443f4 i18n: update zh-TW 2025-04-23 21:28:46 +08:00
Ole Guldberg ba4f399ace Merge branch 'update-danish-translation' of https://github.com/oleguldberg/open-webui into update-danish-translation 2025-04-23 14:51:01 +02:00
Ole Guldberg 176ff56e85 Merge branch 'update-danish-translation' of https://github.com/oleguldberg/open-webui into update-danish-translation 2025-04-23 12:30:19 +02:00
Ole Guldberg f2765bee72 More Danish Translation
i18n - add to Danish translation. Made agains -dev.
2025-04-23 12:25:32 +02:00
Ole Guldberg 497a7b4ded More Danish Translation
i18n - add to Danish translation. Made agains -dev.
2025-04-23 12:06:17 +02:00
Alexander Grimm d182155fac ~ call knowledge searches in parallel in non-hybrid mode 2025-04-23 09:20:51 +00:00
Timothy Jaeryang Baek 5030041683 chore: format 2025-04-23 16:05:15 +09:00
Tim Jaeryang Baek faa3cac0e4 Merge pull request #13107 from tth37/fix_tavily_max_results
fix: `max_results` in Tavily search handler
2025-04-22 23:47:36 -07:00
Tim Jaeryang Baek d3e516934c Merge pull request #13098 from athoik/dev
feat: Add abstract base class for vector database integration
2025-04-22 23:24:08 -07:00
Timothy Jaeryang Baek 2f7b5acdf8 enh: chat share & export permissions 2025-04-23 14:43:33 +09:00
Tim Jaeryang Baek 7937ed3ee1 Merge pull request #13144 from PeterDaveHello/FixI18nInMultiResponseMessages
i18n: Add missing i18n locale for "Merged Response" string
2025-04-22 20:38:37 -07:00
Tim Jaeryang Baek f0a129482e Merge pull request #13145 from PeterDaveHello/Update-zh-TW-locale
i18n: improve zh-TW Traditional Chinese locale
2025-04-22 20:37:47 -07:00
Ole Guldberg 0e0ee3ca9c More Danish Translation 2025-04-22 21:32:21 +02:00
Ole Guldberg 26a2187e6e More Danish translation 2025-04-22 21:30:05 +02:00
Ole Guldberg 2b4e946f70 More danish 2025-04-22 21:10:51 +02:00
Ole Guldberg 17a9d11435 More dansh 2025-04-22 21:00:36 +02:00
Ole Guldberg a21ac3cc25 More danish 2025-04-22 20:54:33 +02:00
Peter Dave Hello 4eb6985c1b i18n: improve zh-TW Traditional Chinese locale 2025-04-23 02:03:07 +08:00
Peter Dave Hello 5f81755b86 i18n: Add missing i18n locale for "Merged Response" string 2025-04-23 02:01:18 +08:00
Ole Guldberg 4340431753 More danish translation 2025-04-22 18:10:43 +02:00
Timothy Jaeryang Baek f2314596ba enh: failed login attempts audit log 2025-04-23 00:06:44 +09:00
Timothy Jaeryang Baek 43efff0fe6 refac 2025-04-22 23:22:50 +09:00
Ole Guldberg 4e8cd688bf beginning of update 2025-04-22 16:05:13 +02:00
Jarrod Lowe af5efe1580 Set default prompt suggestions from environment variable 2025-04-22 11:39:01 +12:00
tth37 5b9c1de3d1 fix: Translatable 'Searching the web' 2025-04-22 01:24:30 +08:00
tth37 4b451b984b perf: Multi-thread web searching 2025-04-21 22:18:05 +08:00
tth37 bc315bd530 fix: max_results in Tavily search api 2025-04-21 20:59:47 +08:00
Athanasios Oikonomou 1e291aff25 feat: Add abstract base class for vector database integration
- Created `VectorDBBase` as an abstract base class to standardize vector database operations.
- Added required methods for common vector database operations: `has_collection`, `delete_collection`, `insert`, `upsert`, `search`, `query`, `get`, `delete`, `reset`.
- The base class can now be extended by any vector database implementation (e.g., Qdrant, Pinecone) to ensure a consistent API across different database systems.
2025-04-21 08:27:27 +03:00
Tim Jaeryang Baek 913f8a15f9 Merge pull request #13073 from athoik/dev
feat(vector-db): add support for Pinecone client
2025-04-20 14:42:36 -07:00
Tim Jaeryang Baek 7f85436a94 Merge pull request #13089 from izumi0004/fix/tool_calls
fix: handling tool calls in streamed responses
2025-04-20 14:08:09 -07:00
izumi0004 99ce85f20f fix: handling tool calls in streamed responses 2025-04-21 00:29:52 +08:00
ayan4m1 039dec6820 fix: pass header to Tika if PDF_EXTRACT_IMAGES is true 2025-04-20 17:36:40 +02:00
Athanasios Oikonomou e000c56ef7 feat(vector-db): add support for Pinecone client
Adds Pinecone as a supported vector database option.

- Implements `PineconeClient` with support for common operations: `add`, `query`, `delete`, `reset`.
- Emulates namespace support using metadata filtering (`collection_name` prefix).
- Dynamically configures Pinecone via the following env vars:
  - `PINECONE_API_KEY`
  - `PINECONE_ENVIRONMENT`
  - `PINECONE_INDEX_NAME`
  - `PINECONE_DIMENSION`
  - `PINECONE_METRIC`
  - `PINECONE_CLOUD`
- Integrates cleanly with the vector DB abstraction layer.
- Includes markdown documentation under `docs/getting-started/env-configuration.md`.

BREAKING CHANGE: None
2025-04-20 11:08:51 +03:00
Tim Jaeryang Baek af02708b4c Merge pull request #13072 from grandtele/fix/openai_o_series_handler
* fix: Refactor openai handler to support all o-series models universally
2025-04-20 00:42:39 -07:00
grand 5a8e417590 * refactor openai handler to support all o-series models universally 2025-04-20 09:14:27 +02:00
Timothy Jaeryang Baek 1a6e4c2948 refac 2025-04-19 23:40:31 -07:00
Timothy Jaeryang Baek 6fd082d55f enh: html token (allow yt embed) 2025-04-19 23:38:08 -07:00
Timothy Jaeryang Baek f4de3b4c45 enh: notificationSoundAlways option 2025-04-19 23:13:19 -07:00
Tim Jaeryang Baek 8fcad1df4e Merge pull request #13062 from open-webui/cla
chore: cla
2025-04-19 11:33:02 -07:00
Tim Jaeryang Baek 08a35417f2 Merge branch 'dev' into cla 2025-04-19 11:32:49 -07:00
Timothy Jaeryang Baek f7906790e1 refac 2025-04-19 11:32:16 -07:00
Timothy Jaeryang Baek 852d9dcbe9 chore: cla 2025-04-19 11:30:59 -07:00
Timothy Jaeryang Baek 48637893b1 fix: scroll on branch change w/ multi response chat 2025-04-19 04:09:46 -07:00
Timothy Jaeryang Baek 463d7fb628 refac: openapi to tool spec 2025-04-19 03:46:06 -07:00
Timothy Jaeryang Baek 332f8579d7 doc: readme 2025-04-19 02:55:02 -07:00
Timothy Jaeryang Baek ad8c8da438 refac 2025-04-19 02:47:48 -07:00
Timothy Jaeryang Baek f0447b24ab refac: wording 2025-04-19 01:51:40 -07:00
Timothy Jaeryang Baek 63a887151f chore: pr template 2025-04-19 00:47:52 -07:00
Timothy Jaeryang Baek 0aea894e2d Create CONTRIBUTOR_LICENSE_AGREEMENT.md 2025-04-19 00:41:08 -07:00
Timothy Jaeryang Baek 8507c47459 refac: WEBUI_AUTH_SIGNOUT_REDIRECT_URL 2025-04-18 23:58:37 -07:00
Tim Jaeryang Baek 221303eec5 Merge pull request #12592 from jarrod-lowe/main
feat: Redirect on sign out
2025-04-18 23:56:38 -07:00
Tim Jaeryang Baek 87844a8042 Merge pull request #12822 from tth37/feat_external_search_loader
feat: Support for Self-Hosted/External Web Search/Loader Engines
2025-04-18 23:51:27 -07:00
Timothy Jaeryang Baek 1c18282d19 fix 2025-04-18 18:21:49 -07:00
Tim Jaeryang Baek 6101067ea4 Merge pull request #12974 from holmlund/fix/prompts-international
fix: improve international character handling in prompt commands
2025-04-18 18:19:12 -07:00
Tim Jaeryang Baek d2f3c82fd6 Merge pull request #13031 from gaby/anyio-workers
feat: Add support for configuring FastAPI/AnyIO Thread Pool
2025-04-18 17:53:35 -07:00
Tim Jaeryang Baek 27936014b0 Merge pull request #13039 from taylorwilsdon/jit_oauth_groups
feat: Just-in-Time Group Creation for OAuth Groups
2025-04-18 17:53:04 -07:00
Timothy Jaeryang Baek 8b9527d887 refac 2025-04-18 17:14:09 -07:00
Timothy Jaeryang Baek a5476ef7d8 refac 2025-04-18 17:10:44 -07:00
Juan Calderon-Perez 62f87c2e38 Use default AnyIO pool size 2025-04-18 19:21:54 -04:00
Timothy Jaeryang Baek 60d84a3aae chore: license "branding" clause 2025-04-18 14:49:20 -07:00
Tim Jaeryang Baek 3e7bce0921 Merge pull request #13036 from erandow/translate-persian-texts
feat: translate all texts to persian
2025-04-18 12:32:17 -07:00
Taylor Wilsdon 1dbf4d0461 Add ENABLE_OAUTH_GROUP_CREATION flag for JIT group creation with OAuth group updates enabled 2025-04-18 10:17:08 -07:00
Taylor Wilsdon a67e5f725f Merge branch 'dev' of github.com:taylorwilsdon/open-webui into dev 2025-04-18 10:04:42 -07:00
Erfan Asadi 527c5e73dc fix: revert اولاما to ollama after feedback 2025-04-18 18:56:01 +03:30
Erfan Asadi 2ecfd3dbed Merge branch 'dev' of https://github.com/erandow/open-webui into translate-persian-texts 2025-04-18 18:55:14 +03:30
Erfan Asadi 5989b6ca12 Merge branch 'main' of https://github.com/erandow/open-webui into translate-persian-texts 2025-04-18 18:54:35 +03:30
Erfan Asadi e3af831db1 feat: translate the remaining texts in persian 2025-04-18 18:38:06 +03:30
Juan Calderon-Perez f77f6cd1e2 Update imports to match anyio docs 2025-04-18 09:30:34 -04:00
Juan Calderon-Perez e19554e9a8 Simplify import 2025-04-18 09:26:57 -04:00
Juan Calderon-Perez 19b3d88737 Add support for configuring FastAPI/AnyIO Thread Pool Size 2025-04-18 09:22:23 -04:00
Timothy Jaeryang Baek 0d09e46281 refac 2025-04-18 05:32:11 -07:00
Timothy Jaeryang Baek 961869a60c refac 2025-04-18 05:30:50 -07:00
Timothy Jaeryang Baek b8fb4e528d enh: toast close button 2025-04-18 04:33:18 -07:00
Tim Jaeryang Baek 794360addc Merge pull request #12997 from Elmolesto/feat/knowledge-lazy-load-content
feat: Lazy load file content on click
2025-04-18 04:04:29 -07:00
Tim Jaeryang Baek fa8132903e Merge pull request #12904 from hurxxxx/feat/onedrive-orgs
feat: support Organization and School accounts in OneDrive
2025-04-18 04:03:54 -07:00
Timothy Jaeryang Baek a86a932b46 refac: deprecate event dispatcher 2025-04-18 02:38:53 -07:00
Felipe Hernandez 14fa7d16f9 feat: Cache file content 2025-04-18 11:38:15 +02:00
Timothy Jaeryang Baek 64a98b2e0e refac: manifest orientation "any" 2025-04-18 02:24:44 -07:00
Timothy Jaeryang Baek 8f67ffabbd fix: model editor system not being preserved 2025-04-18 02:23:25 -07:00
Timothy Jaeryang Baek a22be555cb refac 2025-04-18 01:26:42 -07:00
Timothy Jaeryang Baek eb80719cf0 feat: AIOHTTP_CLIENT_SESSION_SSL 2025-04-17 22:11:42 -07:00
Tim Jaeryang Baek 840dd9c89a Merge pull request #12971 from tth37/fix_source_indexing
fix: Align backend <source> tag indexing with frontend citation grouping
2025-04-17 22:08:07 -07:00
Tim Jaeryang Baek 30e84adb51 Merge pull request #12883 from Classic298/patch-1
fix: UVICORN_WORKERS __init__.py pass application as string
2025-04-17 22:07:21 -07:00
Tim Jaeryang Baek 414ebc87ee Merge pull request #12894 from Classic298/patch-2
security/fix: prevent email and password changes to the primary admin account
2025-04-17 22:06:54 -07:00
Tim Jaeryang Baek 165eb50c22 Merge pull request #12958 from Youggls/fix_search_web
fix: use run_in_threadpool for search_web to prevent blocking
2025-04-17 22:03:31 -07:00
Tim Jaeryang Baek 477ac7de86 Merge pull request #12968 from alpha-pet/feat-openapi-parameter-description
feat: support OpenAPI parameter description and enum values in tool spec
2025-04-17 22:02:50 -07:00
Timothy Jaeryang Baek 824238a620 enh: AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL 2025-04-17 22:02:16 -07:00
Felipe Hernandez 88f97372d9 fix: Show content on upload file 2025-04-18 02:01:47 +02:00
Felipe Hernandez 0577dc2990 feat: Lazy load file content on click 2025-04-18 01:40:30 +02:00
Tim Jaeryang Baek 3b20c57ea8 Merge pull request #12956 from lukasz-pekala/feat/improve-polish-translation
fix: update Polish translations for clarity and consistency
2025-04-17 12:38:14 -07:00
Tim Jaeryang Baek 43b9bf17dc Merge pull request #12966 from damianmr/dev
i18n: Update es-ES (Spanish) translation.json
2025-04-17 12:38:03 -07:00
Tim Jaeryang Baek 246ff83864 Merge pull request #12981 from knight-upstage/dev
i18n: Update ko-KR (Korean) translation.json
2025-04-17 12:37:41 -07:00
knight-upstage cdf26d8189 i18n: Update ko-KR (Korean) translation.json 2025-04-17 09:57:22 -07:00
tth37 79bde6fa47 fix: Align backend <source> tag indexing with frontend citation grouping 2025-04-17 22:27:22 +08:00
Thomas Rehn 3dcbf1acf5 feat: support OpenAPI parameter description and enum values in tool spec 2025-04-17 15:48:55 +02:00
Damian Mendez Romera b1629d8660 i18n: Update es-ES (Spanish) translation.json 2025-04-17 13:58:00 +01:00
David Holmlund 9f8b94e45f fix: improve international character handling in prompt commands 2025-04-17 14:56:57 +02:00
Youggls 9669cd3454 fix: use run_in_threadpool for search_web to prevent blocking
Used fastapi's run_in_threadpool function to execute the search_web function,
preventing the synchronous function from blocking the entire web search process.
2025-04-17 17:23:20 +08:00
Łukasz Pękala c4edcdb12a fix: update Polish translations for clarity and consistency 2025-04-17 10:56:23 +02:00
Timothy Jaeryang Baek a6ccc48d91 refac: textarea component 2025-04-17 00:35:39 -07:00
Tim Jaeryang Baek b6caa95fac Merge pull request #12947 from gaby/qdrant-grpc
feat: Add gRPC support to Qdrant
2025-04-16 23:08:50 -07:00
Juan Calderon-Perez 6188c0c5b7 Add suport for Qdrant GRPC 2025-04-17 01:13:49 -04:00
Jarrod Lowe dfe50b4e56 Merge branch 'open-webui:main' into main 2025-04-17 15:46:24 +12:00
Taylor Wilsdon 54c6e64eec Merge branch 'dev' of github.com:taylorwilsdon/open-webui into dev 2025-04-16 13:01:04 -07:00
Tim Jaeryang Baek 465e879a78 Merge pull request #12917 from rimvel/patch-1
Update translation.json
2025-04-16 08:05:44 -07:00
Tim Jaeryang Baek 93fd5185f9 Merge pull request #12921 from aleixdorca/dev
i18n: Update catalan translation.json
2025-04-16 08:05:14 -07:00
Tim Jaeryang Baek 4ebee592f2 Merge pull request #12925 from JaworIwanowRoosi/main
fix: typos in German translation
2025-04-16 08:05:05 -07:00
JaworIwanowRoosi 6076476a74 fixed: typos in German translation 2025-04-16 14:47:15 +02:00
Aleix Dorca 59918b0f54 Update catalan translation.json 2025-04-16 11:20:03 +02:00
Rimvydas bb1dea72d4 Update translation.json
Fixed typo from "Tęstti" to "Tęsti" (continue).
2025-04-16 09:59:20 +03:00
Tim Jaeryang Baek 264a87f467 Merge pull request #12901 from Kylapaallikko/dev
i18n: Update Finnish Translation
2025-04-15 19:03:52 -07:00
Tim Jaeryang Baek 07347f518a Merge pull request #12896 from gaby/fix-12893
fix: Optional content from files routes
2025-04-15 15:54:29 -07:00
hurxxxx 84f5a52923 chore: clean up unnecessary code 2025-04-16 02:38:20 +09:00
Kylapaallikko 8a50d50038 Update translation.json 2025-04-15 19:44:20 +03:00
hurxxxx 41ef596472 Merge remote-tracking branch 'upstream/main' into feat/onedrive-orgs 2025-04-16 01:41:24 +09:00
Juan Calderon-Perez b4d0d840d1 Fix formatting of qdrant.py 2025-04-15 08:56:51 -04:00
Juan Calderon-Perez b1ef53873f Check if content is present before removing 2025-04-15 08:53:05 -04:00
Classic298 4f14b17c34 Update users.py 2025-04-15 13:50:52 +02:00
Classic298 36b2052fb0 Update __init__.py 2025-04-15 09:55:35 +02:00
Timothy Jaeryang Baek 12c7ecf0f4 refac: styling 2025-04-14 22:34:38 -07:00
Tim Jaeryang Baek e5cbef0baa Merge pull request #12868 from athoik/dev
feat: add QDRANT_ON_DISK configuration option for Qdrant integration
2025-04-14 21:31:46 -07:00
Tim Jaeryang Baek 44e2785495 Merge pull request #12859 from aindriu80/main
i18n: Updated Irish translation
2025-04-14 21:30:21 -07:00
Tim Jaeryang Baek f96c610e21 Merge pull request #12847 from panda44312/patch-13
i18n: zh-cn fix
2025-04-14 21:30:17 -07:00
Taylor Wilsdon 0fc5699533 Merge branch 'dev' of github.com:taylorwilsdon/open-webui into dev 2025-04-14 19:34:12 -04:00
Athanasios Oikonomou 575c12f80c feat: add QDRANT_ON_DISK configuration option for Qdrant integration
This commit will allow configuring the on_disk client parameter, to reduce the memory usage.
https://qdrant.tech/documentation/concepts/storage/?q=mmap#configuring-memmap-storage
Default is false, keeping vectors in memory.
2025-04-15 01:40:57 +03:00
Tim Jaeryang Baek 7e0e6177e9 Merge pull request #12865 from TiancongLx/dev
i18n: update zh-TW
2025-04-14 13:53:24 -07:00
Tiancong Li 0d388b4e54 i18n: update zh-TW 2025-04-15 03:33:30 +08:00
Aindriú Mac Giolla Eoin a3d950872c Updated Irish translations - April 2025-04-14 17:14:02 +01:00
hurxxxx 55d077b52a refactor 2025-04-15 00:35:18 +09:00
hurxxxx 2d7062fc99 fix: onedrive orgs selection 2025-04-15 00:27:59 +09:00
hurxxxx 5fd794612e add onedrive sub menu 2025-04-14 22:57:32 +09:00
Panda 21206bf4b9 i18n: zh-cn fix 2025-04-14 14:26:10 +02:00
tth37 dcb3f18e1e refac: styling 2025-04-14 18:56:47 +08:00
tth37 85f8e91288 feat: Allow admin editing external search/loader settings 2025-04-14 18:19:26 +08:00
tth37 008fec80c1 fix: Update external search/loader method to POST 2025-04-14 18:17:27 +08:00
Tim Jaeryang Baek 07d8460126 Merge pull request #12809 from open-webui/dev
0.6.5
2025-04-14 02:13:21 -07:00
Timothy Jaeryang Baek e4c7417522 refac 2025-04-14 02:12:58 -07:00
Timothy Jaeryang Baek 91523bbb07 chore: bump 2025-04-14 02:08:11 -07:00
Timothy Jaeryang Baek 49bf3207e8 chore: format 2025-04-14 02:00:52 -07:00
Timothy Jaeryang Baek ec730b08c5 chore: format 2025-04-14 02:00:46 -07:00
Timothy Jaeryang Baek 815d18b57e revert 2025-04-14 01:57:23 -07:00
Timothy Jaeryang Baek 8d33b5e063 refac 2025-04-14 01:56:52 -07:00
Timothy Jaeryang BaekandSebastian Whincop aa8db40376 enh: copy formatted option
Co-Authored-By: Sebastian Whincop <123417897+macjedi42@users.noreply.github.com>
2025-04-14 01:56:15 -07:00
Timothy Jaeryang Baek 95fca7b700 refac 2025-04-14 01:41:58 -07:00
Timothy Jaeryang Baek 87aa01a5c2 refac: styling 2025-04-14 01:40:29 -07:00
Timothy Jaeryang Baek d932fc555d enh: tts/stt user permissions 2025-04-14 01:40:22 -07:00
Timothy Jaeryang Baek ec3b8fab5b refac 2025-04-14 01:08:16 -07:00
Timothy Jaeryang Baek 403295600f refac: audio tts voice list 2025-04-14 00:20:51 -07:00
Tim Jaeryang Baek ceffac87b6 Merge pull request #12835 from weisser-dev/dev
Fix: Robust Handling of LDAP Email Attribute Types
2025-04-14 00:15:51 -07:00
Timothy Jaeryang Baek e7332fd6fe refac 2025-04-13 23:39:38 -07:00
Tim Jaeryang Baek 60596c362a Merge pull request #12815 from TomBayne/enh/vad_filter
feat/Enable vad_filter to improve quality of transcription in faster-whisper
2025-04-13 23:27:03 -07:00
Erik c749083935 Update auths.py 2025-04-14 08:12:37 +02:00
Timothy Jaeryang Baek 70718dda90 refac 2025-04-13 22:31:43 -07:00
Timothy Jaeryang Baek 2fcf5d8578 refac 2025-04-13 17:28:09 -07:00
Tim Jaeryang Baek bc2b5d3cc8 Merge pull request #12816 from SadmL/dev
[i18n] Russian locale update
2025-04-13 14:11:16 -07:00
Tim Jaeryang Baek 370f934158 Merge pull request #12819 from athoik/dev
fix: Properly handle ldap3.abstract.attribute.Attribute value
2025-04-13 14:09:56 -07:00
tth37 22f0365cef format 2025-04-14 02:05:58 +08:00
tth37 839ba22c90 feat: Backend for Self-Hosted/External Web Search/Loader Engines 2025-04-14 01:49:05 +08:00
Athanasios Oikonomou c70fedb626 Properly handle ldap3.abstract.attribute.Attribute value
This commit fixes an issue created on commit 2e7a01f30a.

Ldap object is type <class 'ldap3.abstract.attribute.Attribute'>.
So we need to return the value and verify if it's null, string or list.

Fixes #12813
2025-04-13 18:00:42 +03:00
SadmL 08bcfdb8cc [i18n] Russian locale update 2025-04-13 15:37:18 +03:00
Tom 24367d459b Enable vad_filter to improve quality of transcription in faster-whisper model. 2025-04-13 13:03:57 +01:00
Tim Jaeryang Baek 8158609c93 Merge pull request #12810 from tth37/i18n_small_fix
i18n: update zh-CN
2025-04-12 23:15:54 -07:00
tth37 70d9405e64 i18n: update zh-CN 2025-04-13 14:14:34 +08:00
Timothy Jaeryang Baek aaefba2777 chore: bump 2025-04-12 23:06:40 -07:00
Tim Jaeryang Baek aca37f592d Merge pull request #12807 from open-webui/dev
0.6.4
2025-04-12 23:01:19 -07:00
Timothy Jaeryang Baek 888b468576 fix 2025-04-12 23:00:34 -07:00
Timothy Jaeryang Baek 9348c05f1a doc: changelog 2025-04-12 22:57:13 -07:00
Timothy Jaeryang Baek 4dafbbccfc fix: rag template display issue 2025-04-12 22:55:24 -07:00
Tim Jaeryang Baek 8b0e565e2c Merge pull request #12715 from open-webui/dev
0.6.3
2025-04-12 22:44:24 -07:00
Timothy Jaeryang Baek 2766d7df6a doc: changelog 2025-04-12 22:44:00 -07:00
Tim Jaeryang Baek 8e810e758d Merge pull request #12806 from tth37/fix_web_rag_settings
fix: Resolve small bugs on updated web/rag settings
2025-04-12 22:26:04 -07:00
Timothy Jaeryang Baek e7821288f5 doc: changelog 2025-04-12 22:25:02 -07:00
tth37 db9d4ead59 format 2025-04-13 13:10:39 +08:00
Timothy Jaeryang Baek 1a6eaf6445 chore: dependency bump 2025-04-12 22:07:23 -07:00
tth37 8d53f1e770 fix: small bugs on updated web/rag settings 2025-04-13 12:55:50 +08:00
Timothy Jaeryang Baek c1444cbbba refac 2025-04-12 21:47:46 -07:00
Timothy Jaeryang Baek 40f9747ae4 refac 2025-04-12 21:44:47 -07:00
Timothy Jaeryang Baek ac8e81834c refac 2025-04-12 21:44:15 -07:00
Timothy Jaeryang Baek 8d62cc1935 refac 2025-04-12 21:09:26 -07:00
Timothy Jaeryang Baek f3fe82da80 fix: ongoing chat stop issue 2025-04-12 20:51:02 -07:00
Timothy Jaeryang Baek fa61065c1e refac 2025-04-12 20:15:00 -07:00
Timothy Jaeryang Baek 5844779384 refac: styling 2025-04-12 20:14:32 -07:00
Timothy Jaeryang Baek 451465151e refac 2025-04-12 19:31:02 -07:00
Tim Jaeryang Baek 85ef912f6a Merge pull request #12794 from TiancongLx/dev
i18n: update zh-TW
2025-04-12 19:27:26 -07:00
Tiancong Li 2b8cd4dff4 i18n: update zh-TW 2025-04-13 10:00:58 +08:00
Timothy Jaeryang Baek f6c8184a74 fix: model hide issue 2025-04-12 18:49:49 -07:00
Timothy Jaeryang Baek bde89fd29e refac: audio 2025-04-12 18:40:09 -07:00
Timothy Jaeryang Baek daabc188e8 fix: model list hide issue 2025-04-12 18:34:10 -07:00
Timothy Jaeryang Baek d5ee39de21 fix: katex display overflow issue 2025-04-12 18:27:23 -07:00
Timothy Jaeryang Baek cdb4904999 refac 2025-04-12 18:11:22 -07:00
Tim Jaeryang Baek 577406e00b Merge pull request #12788 from TiancongLx/dev
i18n: update zh-TW
2025-04-12 17:58:01 -07:00
Timothy Jaeryang Baek 175919c787 enh: widescreen mode in shared chats 2025-04-12 17:53:23 -07:00
Tiancong Li 03e2e5279f i18n: update zh-TW 2025-04-13 08:50:22 +08:00
Timothy Jaeryang Baek 4ca6247496 chore: format 2025-04-12 17:45:33 -07:00
Timothy Jaeryang Baek 40a3e7786b refac: voice recording 2025-04-12 17:41:53 -07:00
Timothy Jaeryang Baek 79dff685ea feat: multiple models chat user permissions 2025-04-12 17:37:30 -07:00
Timothy Jaeryang Baek 881d81d87f refac: default tool calling behaviour 2025-04-12 17:26:35 -07:00
Timothy Jaeryang Baek db8780fd9c refac: default tool boilerplate 2025-04-12 17:16:27 -07:00
Timothy Jaeryang Baek 0a69db291d enh: iframe sandbox policy setting 2025-04-12 17:09:39 -07:00
Timothy Jaeryang Baek af0d99b2a3 refac: artifacts styling 2025-04-12 17:01:45 -07:00
Tim Jaeryang Baek 540c27c589 Merge pull request #12784 from rgaricano/dev
UPD: Spanish Translation (es-ES)  Rev.0.6.3
2025-04-12 16:51:47 -07:00
_00_ 4106681889 UPD: Spanish Translation (es-ES) Rev.0.6.3
Revision of Spanish Translation (es-ES)
Adapted to Open-WebUI v.0.6.3

Has been done

Revision & Complete.
2025-04-13 01:42:28 +02:00
Timothy Jaeryang Baek 91a455a284 chore: format 2025-04-12 16:35:11 -07:00
Timothy Jaeryang Baek 77b25ae36a refac 2025-04-12 16:34:20 -07:00
Timothy Jaeryang Baek 48a23ce3fe refac: web/rag config 2025-04-12 16:33:36 -07:00
Timothy Jaeryang Baek c3497da5dd enh: only copy text message content 2025-04-12 15:11:03 -07:00
Timothy Jaeryang Baek bdef1001ac refac 2025-04-12 15:10:43 -07:00
Tim Jaeryang Baek 6862d8fba8 Merge pull request #12769 from tth37/feat_frontend_web_loader
feat: Add Frontend Configuration for `RAG_WEB_LOADER_ENGINE`
2025-04-12 14:56:47 -07:00
Tim Jaeryang Baek 02ba7aeb95 Merge pull request #12366 from Ithanil/redis_sentinel_2
chore: adapt websocket redis sentinel code to new native support in python-socketio
2025-04-12 11:41:30 -07:00
Jan Kessler c6755f9151 bump python-socketio to 5.13.0 (to support Redis Sentinel natively) 2025-04-12 19:22:10 +02:00
tth37 5eac5960ef feat: Add frontend configuration for web loader 2025-04-12 17:13:30 +08:00
Timothy Jaeryang Baek 30d02c638c refac: banners 2025-04-11 15:34:21 -07:00
Timothy Jaeryang Baek c5636ff68c refac 2025-04-11 15:27:25 -07:00
Timothy Jaeryang Baek c846a550fa refac 2025-04-11 15:19:47 -07:00
Timothy Jaeryang Baek 59dd798156 refac/security: set ac to private by default 2025-04-11 14:46:02 -07:00
Jarrod Lowe a8bbaa61bd Merge branch 'dev' into main 2025-04-11 22:14:41 +12:00
Timothy Jaeryang Baek 5fea0a2221 refac: styling 2025-04-10 21:52:03 -07:00
Timothy Jaeryang Baek 6d63337ee9 refac 2025-04-10 21:22:14 -07:00
Timothy Jaeryang Baek b97ea72616 refac: tool call result display 2025-04-10 20:06:00 -07:00
Timothy Jaeryang Baekanddulicon 1921420319 refac
Co-Authored-By: dulicon <57261058+dulicon@users.noreply.github.com>
2025-04-10 20:01:07 -07:00
Timothy Jaeryang Baek a47a958edd refac: rename functions 2025-04-10 19:43:26 -07:00
Timothy Jaeryang Baek 53df5d3946 refac: rename hard to understand cryptic function names 2025-04-10 19:41:17 -07:00
Timothy Jaeryang Baek 0f27968ceb fix: tool server session auth 2025-04-10 17:47:07 -07:00
Tim Jaeryang Baek 36ac81b229 Merge pull request #12727 from decent-engineer-decent-datascientist/main
feat: add Azure AI Speech STT provider
2025-04-10 16:50:40 -07:00
priten 9a55257c5b feat: add Azure AI Speech STT provider
- Add Azure STT configuration variables for API key, region and locales
- Implement Azure STT transcription endpoint with 200MB file size limit
- Update Audio settings UI to include Azure STT configuration fields
- Handle Azure API responses and error cases consistently
2025-04-10 15:38:59 -05:00
Timothy Jaeryang Baek 05aa9c6d9c refac 2025-04-10 12:27:11 -07:00
Tim Jaeryang Baek f0efee5de4 Merge pull request #12603 from alpha-pet/fix-convert-ogg-container-openai-transcription
fix: Convert ogg to wav for OpenAI transcription endpoint
2025-04-10 12:25:46 -07:00
Timothy Jaeryang Baek bf0716e85e refac: styling 2025-04-10 12:05:01 -07:00
Timothy Jaeryang Baek f9bd49bf56 refac 2025-04-10 11:18:53 -07:00
Tim Jaeryang Baek 4cbf16c456 Merge pull request #12717 from Kheiden/progressive-webapp-enhancements
feat: Add PWA external manifest URL env var
2025-04-10 10:55:54 -07:00
Timothy Jaeryang BaekandRobert Piosik 1eba6a812e enh: smaller code text size
Co-Authored-By: Robert Piosik <robertpiosik@gmail.com>
2025-04-10 10:50:46 -07:00
Timothy Jaeryang Baek 3f71929a69 refac: styling 2025-04-10 10:46:44 -07:00
Timothy Jaeryang Baek d8780e4de2 feat: detect artifacts toggle 2025-04-10 10:36:57 -07:00
Kurt Heiden c12f8d28b9 Add PWA external manifest URL env var 2025-04-10 11:35:43 -06:00
Timothy Jaeryang Baek d4297a3e3e refac: styling 2025-04-10 10:31:38 -07:00
Timothy Jaeryang Baek fb0ee1329d enh: tool server data timeout 2025-04-10 10:23:14 -07:00
Timothy Jaeryang Baek 23dcae7054 refac: selector 2025-04-10 10:18:56 -07:00
Tim Jaeryang Baek 7aab8b9307 Merge pull request #12567 from aleprj/fix/ms-sso
fix: Make Microsoft SSO work even when custom signing keys. #12475
2025-04-10 10:11:49 -07:00
Timothy Jaeryang BaekandAnthony Durussel f10566f3de feat: allow events from pipelines
Co-Authored-By: Anthony Durussel <87324020+anthonydurussel@users.noreply.github.com>
2025-04-10 09:20:18 -07:00
Timothy Jaeryang Baek 0611eb8ac7 refac: layout load 2025-04-10 09:15:08 -07:00
Tim Jaeryang Baek a3e477e100 Merge pull request #12602 from alpha-pet/fix-tool-server-indexing-mismatch
fix: mismatch between TOOL_SERVERS / TOOL_SERVER_CONNECTIONS indexing
2025-04-10 09:06:30 -07:00
Tim Jaeryang Baek b40b50fb2d Merge pull request #12563 from IONOS-Productivity/oidc-add-support-for-pkce
OIDC add support for PKCE
2025-04-10 09:03:31 -07:00
Tim Jaeryang Baek e065073bea Merge pull request #12689 from asncx/fix-folder-chat-rename
fix: Chat rename not updating inside folders
2025-04-10 08:51:22 -07:00
Tim Jaeryang Baek 33b35b797c Merge pull request #12594 from gaby/search-files
feat: Add support for searching files by name
2025-04-10 08:50:13 -07:00
Tim Jaeryang Baek ef14d0b46d Merge pull request #12562 from almajo/fix/agrimm/correct_knowledge_base_citations
fix: Correct Citations for knowledge base models
2025-04-10 08:48:47 -07:00
Tim Jaeryang Baek 62ef0bad6f Merge pull request #12680 from lucyknada/patch-1
fix #12678
2025-04-10 08:46:41 -07:00
Timothy Jaeryang Baek 63e5200e2f refac 2025-04-10 08:46:12 -07:00
Tim Jaeryang Baek 1865e29bfc Merge pull request #12683 from Youggls/dev
feat: Add sougou web search engine.
2025-04-10 08:45:15 -07:00
Tim Jaeryang Baek f7dc6474ee Merge pull request #12694 from SadmL/dev
[i18n] Russian locale misspelling fix
2025-04-10 08:43:51 -07:00
Timothy Jaeryang Baek 536bc36e36 chore: bump duckduckgo 2025-04-10 08:42:03 -07:00
Taylor Wilsdon 708fcdc6b7 Merge branch 'dev' of github.com:taylorwilsdon/open-webui into dev 2025-04-10 06:32:17 -04:00
SadmL de926a060d [i18n] Russian locale misspelling fix 2025-04-10 13:17:52 +03:00
Thomas Rehn 4731e0d0e3 fix: convert webm to wav for OpenAI transcription endpoint 2025-04-10 09:00:51 +02:00
Youggls 3e2a6df1fb feat: Add sougou web search API for backend, add config panel in for frontend. 2025-04-10 14:51:44 +08:00
lucy bc295546cd fix #12678 2025-04-10 07:23:34 +02:00
Tim Jaeryang Baek 6c9a4d6ce6 Merge pull request #12606 from alpha-pet/feat-openapi-yaml-support
Feat: openapi yaml support
2025-04-09 18:37:09 -07:00
Tim Jaeryang Baek e27c5989bc Merge pull request #12657 from noamtamim/patch-1
docs: Set FastAPI app title to "Open WebUI"
2025-04-09 18:32:26 -07:00
Tim Jaeryang Baek 5f116d69c2 Merge pull request #12664 from Beni34/main
Updated Hungarian language to full. translation.json
2025-04-09 16:43:08 -07:00
Tim Jaeryang Baek a27dcc84a3 Merge pull request #12654 from valckmir/main
feat: disable persistent config
2025-04-09 16:16:14 -07:00
Beni34 7cee0073cf Update translation.json 2025-04-09 21:40:23 +02:00
Miroslav Valcicak a5faaf388e refactor: rename persistent config variable 2025-04-09 21:22:25 +02:00
asncx 87af2692e5 fix: folder chat rename 2025-04-09 20:56:49 +02:00
Noam Tamim 15fd822916 Set FastAPI title to Open WebUI
This affects the generated OpenAPI (`/docs` and `/openapi.json`).
2025-04-09 19:45:17 +03:00
Miroslav Valcicak 68aea6bf10 feat: disable persistent config 2025-04-09 16:18:58 +02:00
Tim Jaeryang Baek 38d5b03685 Merge pull request #12647 from behrmann/multimail
fix: choose the first mail if multiple are returned from LDAP
2025-04-09 03:47:49 -07:00
Jörg Behrmann 2e7a01f30a fix: choose the first mail if multiple are returned from LDAP 2025-04-09 10:53:02 +02:00
Tim Jaeryang Baek a363c0619a Merge pull request #12638 from panda44312/patch-12
i18n: zh-cn
2025-04-09 01:45:41 -07:00
Tim Jaeryang Baek f49d3d09f5 Merge pull request #12637 from TiancongLx/dev
i18n: update zh-TW
2025-04-09 01:45:31 -07:00
Thomas Rehn 17ea20d3ad feat: OpenAPI YAML support for frontend toolserver 2025-04-09 09:46:44 +02:00
Panda 92611b588c i18n: zh-cn 2025-04-09 09:44:10 +02:00
Tiancong Li de1e9f3c0f i18n: update zh-TW 2025-04-09 15:25:37 +08:00
Tim Jaeryang Baek d8c0a1227a Merge pull request #12552 from tth37/dev
i18n: update zh-CN
2025-04-08 22:17:57 -07:00
Tim Jaeryang Baek 84ea9ea011 Merge pull request #12555 from SadmL/dev
[i18n] Russian locale update
2025-04-08 22:17:50 -07:00
Tim Jaeryang Baek b130b55f5b Merge pull request #12571 from hurxxxx/feat/reindex-knowledge-files
feat: reindex knowledge files
2025-04-08 22:10:35 -07:00
Tim Jaeryang Baek 2ff1657a7a Merge pull request #12573 from gem85247/dev
fix: manifest.json cross-origin issues
2025-04-08 22:07:43 -07:00
Tim Jaeryang Baek 2575dac4ed Merge pull request #12604 from maurerle/ddg_improve_stacktrace
**fix** improve stack trace of duckduckgo exception
2025-04-08 13:03:57 -07:00
Tim Jaeryang Baek 8c3146f6f8 Merge pull request #12615 from robert-norberg/additional-RAG-logging
chore: Add debug logging to RAG utils
2025-04-08 13:03:38 -07:00
Tim Jaeryang Baek 1f12023d85 Merge pull request #12617 from Classic298/dev
feat: UVICORN_WORKERS variable and docs
2025-04-08 12:52:47 -07:00
Timothy Jaeryang Baek 63eab42208 refac 2025-04-08 12:51:48 -07:00
Timothy Jaeryang Baek 60d11c1f6f enh: password max length verification 2025-04-08 12:50:25 -07:00
DrMelone f17befc853 Update __init__.py 2025-04-08 18:55:07 +02:00
DrMelone 65b5c6c0c7 Update start_windows.bat 2025-04-08 18:52:12 +02:00
DrMelone 2c1d0e3857 Update start.sh 2025-04-08 18:50:07 +02:00
DrMelone 4c9a791b06 Update env.py 2025-04-08 18:47:48 +02:00
DrMelone f822d89350 Update env.py 2025-04-08 18:38:00 +02:00
Robert Norberg 2337b36609 add debug logging to RAG utils 2025-04-08 12:08:32 -04:00
Thomas Rehn 81af20aa9b refac: use logging instead of print 2025-04-08 15:16:55 +02:00
Thomas Rehn 27e8c6fce4 feat: add support for OpenAPI spec in YAML format 2025-04-08 15:16:52 +02:00
Thomas Rehn d99a883867 fix: convert ogg to wav for OpenAI transcription endpoint 2025-04-08 15:04:04 +02:00
Thomas Rehn f5b2867e45 fix: mismatch between TOOL_SERVERS / TOOL_SERVER_CONNECTIONS indexing 2025-04-08 14:08:08 +02:00
Florian Maurer 760ea3f4af duckduckgo: backend api has been deprecated since december
also increase duckduckgo-search version

see https://github.com/deedy5/duckduckgo_search/commit/3ee8e08b1c8efc9b7356f406db299f1114dc4860
2025-04-08 14:02:06 +02:00
Florian Maurer 337c7caafa improve stack trace of duckduckgo exception
* fix search_results out of scope
* ddgs.text does already always return a list
2025-04-08 13:52:23 +02:00
Haodong Tian d50ca0aa43 Update zh-CN translation.json 2025-04-08 15:49:20 +08:00
Haodong Tian 399e7b1670 Update zh-CN translation.json 2025-04-08 15:47:24 +08:00
Haodong Tian e92d3073bb Update zh-CN translation.json
- Minor fix
2025-04-08 15:37:50 +08:00
Haodong Tian 00f58658f7 i18n: Apply i18n to Web Search proxy tooltips 2025-04-08 07:01:56 +00:00
Haodong Tian b15adc63fe Update zh-CN translation.json
- Standardize Chinese translations for "autocomplete"
2025-04-08 14:24:39 +08:00
Juan Calderon-Perez fed47f2e2b Add content param to /search route 2025-04-08 01:00:06 -04:00
Juan Calderon-Perez 1c60b8d543 Rewrite logic 2025-04-08 00:56:21 -04:00
Juan Calderon-Perez 8609ca3657 Fix formatting again 2025-04-08 00:49:55 -04:00
Juan Calderon-Perez e06ff17a70 Fix formatting 2025-04-08 00:48:54 -04:00
Juan Calderon-Perez 7c1b004687 simplify logic 2025-04-08 00:44:47 -04:00
Juan Calderon-Perez 353b104c77 add support for searching files 2025-04-08 00:42:37 -04:00
Jarrod Lowe b4465987f9 Environment variable to redirect on logout 2025-04-08 13:39:00 +12:00
Tim Jaeryang Baek 66322727e3 Merge pull request #12580 from lainedfles/opentelemetry-requirements-fix
fix: Upgrade OpenTelemetry instrumentation packages to 0.52b1
2025-04-07 14:19:17 -07:00
Self Denial 0e71b96597 fix: Upgrade OpenTelemetry instrumentation packages to 0.52b1
This commit updates all OpenTelemetry instrumentation dependencies (FastAPI, SQLAlchemy, Redis, Requests, Logging, HTTPX, aiohttp-client) from version 0.52b0 to 0.52b1 to satisfy opentelemetry-sdk==1.31.1 dependencies.
2025-04-07 14:49:47 -06:00
Gilles M f3f45209e9 fix: manifest.json cross-origin
When utilizing Cloudflare Zero Trust, fetching the manifest.json file may encounter issues because the request is redirected through the Zero Trust authentication page.
2025-04-07 19:01:18 +02:00
hurxxxx e7715f3760 i18n: add reindex knowledge files dialog 2025-04-08 01:37:50 +09:00
hurxxxx 7c828015d3 fix: ReindexKnowledgeFilesConfirmDialog 2025-04-08 00:53:11 +09:00
hurxxxx 4e545d432b feat: add new admin func - reindex knowledge files 2025-04-08 00:44:10 +09:00
Taylor Wilsdon 66bf6d88bc Merge branch 'dev' of github.com:taylorwilsdon/open-webui into dev 2025-04-07 10:17:57 -04:00
aleprj e8f02328f4 Merge branch 'open-webui:main' into fix/ms-sso 2025-04-07 11:16:21 -03:00
aleprj 818d24e672 fix: add appid to the URL to ensure we fetch the correct parameters in all scenarios 2025-04-07 11:15:47 -03:00
Alexander Grimm 1e7a36478b ~ update to latest change 2025-04-07 15:16:47 +02:00
Alexander Grimm d8a7e9c660 fix: correct document citation 2025-04-07 15:16:47 +02:00
Thomas Lehmann 5c658a4879 feat(config): add config OAUTH_CODE_CHALLENGE_METHOD
Add support to enable OIDC code challenge method (PKCE).
2025-04-07 14:03:52 +02:00
SadmL b81789190e [i18n] Russian locale update 2025-04-07 12:04:58 +03:00
Haodong Tian f172403049 Update zh-CN translation.json 2025-04-07 16:11:54 +08:00
Timothy Jaeryang Baek 9cb1104f09 refac 2025-04-07 00:58:32 -07:00
Timothy Jaeryang Baek d1a77a9110 Merge pull request #12549 from ulagbulag/fix/500-when-unknown-models
fix: internal server error when calling completions API with non-existent model names
2025-04-07 00:58:02 -07:00
Ho Kim 1efae95a36 fix: internal server error when calling completions API with non-existent model names
Signed-off-by: Ho Kim <ho.kim@ulagbulag.io>
2025-04-07 06:48:39 +00:00
Timothy Jaeryang Baek 54a74b2c0a chore: bump 2025-04-06 23:46:59 -07:00
Timothy Jaeryang Baek 908492702a chore: bump 2025-04-06 23:46:40 -07:00
Timothy Jaeryang Baek 5553ec3e51 Merge pull request #12537 from open-webui/dependabot/npm_and_yarn/dev/tiptap/pm-2.11.7
build(deps): bump @tiptap/pm from 2.10.0 to 2.11.7
2025-04-06 23:45:59 -07:00
Timothy Jaeryang Baek ddec6cf0fa Merge pull request #12545 from mogorman/elixir_syntax_highlighting
feat: add Elixir highlighting
2025-04-06 23:45:30 -07:00
Timothy Jaeryang Baek 1857f152c2 Merge pull request #12548 from aleixdorca/dev
i18n: Update Catalan translation.json
2025-04-06 23:45:07 -07:00
Timothy Jaeryang Baek c5deb5cda4 Merge pull request #12541 from open-webui/dependabot/uv/dev/opentelemetry-instrumentation-logging-0.52b0
build(deps): bump opentelemetry-instrumentation-logging from 0.51b0 to 0.52b0
2025-04-06 23:43:48 -07:00
Timothy Jaeryang Baek d3ed20e907 Merge pull request #12535 from open-webui/dependabot/pip/backend/dev/youtube-transcript-api-1.0.3
build(deps): bump youtube-transcript-api from 0.6.3 to 1.0.3 in /backend
2025-04-06 23:43:35 -07:00
Timothy Jaeryang Baek 82252cb760 Merge pull request #12532 from open-webui/dependabot/pip/backend/dev/chromadb-0.6.3
build(deps): bump chromadb from 0.6.2 to 0.6.3 in /backend
2025-04-06 23:43:08 -07:00
Timothy Jaeryang Baek 9e700dc9a2 Merge pull request #12533 from open-webui/dependabot/pip/backend/dev/pgvector-0.4.0
build(deps): bump pgvector from 0.3.5 to 0.4.0 in /backend
2025-04-06 23:42:30 -07:00
Timothy Jaeryang Baek e1f60feb7f Merge pull request #12529 from open-webui/dependabot/pip/backend/dev/azure-identity-1.21.0
build(deps): bump azure-identity from 1.20.0 to 1.21.0 in /backend
2025-04-06 23:41:53 -07:00
Timothy Jaeryang Baek aefb3117db fix: chat completion non-existent model issue 2025-04-06 23:40:48 -07:00
Aleix Dorca 76d8935d0b Update catalan translation.json 2025-04-07 08:31:44 +02:00
dependabot[bot] dbc06084a7 build(deps): bump opentelemetry-instrumentation-logging
Bumps [opentelemetry-instrumentation-logging](https://github.com/open-telemetry/opentelemetry-python-contrib) from 0.51b0 to 0.52b0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-python-contrib/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-python-contrib/commits)

---
updated-dependencies:
- dependency-name: opentelemetry-instrumentation-logging
  dependency-version: 0.52b0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 03:43:31 +00:00
Matthew O'Gorman 8a81f5f188 add support for elixir code syntax highlighting 2025-04-06 23:42:53 -04:00
dependabot[bot] ba66995532 build(deps): bump @tiptap/pm from 2.10.0 to 2.11.7
Bumps [@tiptap/pm](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/pm) from 2.10.0 to 2.11.7.
- [Release notes](https://github.com/ueberdosis/tiptap/releases)
- [Changelog](https://github.com/ueberdosis/tiptap/blob/@tiptap/pm@2.11.7/packages/pm/CHANGELOG.md)
- [Commits](https://github.com/ueberdosis/tiptap/commits/@tiptap/pm@2.11.7/packages/pm)

---
updated-dependencies:
- dependency-name: "@tiptap/pm"
  dependency-version: 2.11.7
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 03:42:44 +00:00
dependabot[bot] 96ecdc37b3 build(deps): bump youtube-transcript-api from 0.6.3 to 1.0.3 in /backend
Bumps [youtube-transcript-api](https://github.com/jdepoix/youtube-transcript-api) from 0.6.3 to 1.0.3.
- [Release notes](https://github.com/jdepoix/youtube-transcript-api/releases)
- [Commits](https://github.com/jdepoix/youtube-transcript-api/compare/v0.6.3...v1.0.3)

---
updated-dependencies:
- dependency-name: youtube-transcript-api
  dependency-version: 1.0.3
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 03:42:21 +00:00
dependabot[bot] f187a1c9ef build(deps): bump pgvector from 0.3.5 to 0.4.0 in /backend
Bumps [pgvector](https://github.com/pgvector/pgvector-python) from 0.3.5 to 0.4.0.
- [Changelog](https://github.com/pgvector/pgvector-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/pgvector/pgvector-python/compare/v0.3.5...v0.4.0)

---
updated-dependencies:
- dependency-name: pgvector
  dependency-version: 0.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 03:42:17 +00:00
dependabot[bot] 9e8b7ac611 build(deps): bump chromadb from 0.6.2 to 0.6.3 in /backend
Bumps [chromadb](https://github.com/chroma-core/chroma) from 0.6.2 to 0.6.3.
- [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/0.6.2...0.6.3)

---
updated-dependencies:
- dependency-name: chromadb
  dependency-version: 0.6.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 03:42:14 +00:00
dependabot[bot] 5379c08b9d build(deps): bump azure-identity from 1.20.0 to 1.21.0 in /backend
Bumps [azure-identity](https://github.com/Azure/azure-sdk-for-python) from 1.20.0 to 1.21.0.
- [Release notes](https://github.com/Azure/azure-sdk-for-python/releases)
- [Changelog](https://github.com/Azure/azure-sdk-for-python/blob/main/doc/esrp_release.md)
- [Commits](https://github.com/Azure/azure-sdk-for-python/compare/azure-identity_1.20.0...azure-identity_1.21.0)

---
updated-dependencies:
- dependency-name: azure-identity
  dependency-version: 1.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 03:42:11 +00:00
Taylor Wilsdon 0a2747bc6a Merge branch 'dev' of github.com:taylorwilsdon/open-webui into dev 2025-04-06 13:01:39 -04:00
Jan Kessler 257ca45456 leave out @ in redis+sentine url when no username/password is provided 2025-04-03 08:24:24 +02:00
Jan Kessler 35ea29b184 prepare websocket redis sentinel code for upcoming native support of sentinel in python-socketio 2025-04-02 21:50:00 +02:00
hdnh2006 0afe972bc6 embeddings function added 100% OpenAI compatible 2025-02-14 17:57:47 +01:00
515 changed files with 67915 additions and 25560 deletions
+9
View File
@@ -7,6 +7,15 @@ OPENAI_API_KEY=''
# AUTOMATIC1111_BASE_URL="http://localhost:7860"
# For production, you should only need one host as
# fastapi serves the svelte-kit built frontend and backend from the same host and port.
# To test with CORS locally, you can set something like
# CORS_ALLOW_ORIGIN='http://localhost:5173;http://localhost:8080'
CORS_ALLOW_ORIGIN='*'
# For production you should set this to match the proxy configuration (127.0.0.1)
FORWARDED_ALLOW_IPS='*'
# DO NOT TRACK
SCARF_NO_ANALYTICS=true
DO_NOT_TRACK=true
+49 -1
View File
@@ -1 +1,49 @@
*.sh text eol=lf
# TypeScript
*.ts text eol=lf
*.tsx text eol=lf
# JavaScript
*.js text eol=lf
*.jsx text eol=lf
*.mjs text eol=lf
*.cjs text eol=lf
# Svelte
*.svelte text eol=lf
# HTML/CSS
*.html text eol=lf
*.css text eol=lf
*.scss text eol=lf
*.less text eol=lf
# Config files and JSON
*.json text eol=lf
*.jsonc text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.toml text eol=lf
# Shell scripts
*.sh text eol=lf
# Markdown & docs
*.md text eol=lf
*.mdx text eol=lf
*.txt text eol=lf
# Git-related
.gitattributes text eol=lf
.gitignore text eol=lf
# Prettier and other dotfiles
.prettierrc text eol=lf
.prettierignore text eol=lf
.eslintrc text eol=lf
.eslintignore text eol=lf
.stylelintrc text eol=lf
.editorconfig text eol=lf
# Misc
*.env text eol=lf
*.lock text eol=lf
+29 -8
View File
@@ -89,9 +89,20 @@ body:
required: true
- label: I have included the Docker container logs.
required: true
- label: I have listed steps to reproduce the bug in detail.
- label: I have **provided every relevant configuration, setting, and environment variable used in my setup.**
required: true
- label: I have clearly **listed every relevant configuration, custom setting, environment variable, and command-line option that influences my setup** (such as Docker Compose overrides, .env values, browser settings, authentication configurations, etc).
required: true
- label: |
I have documented **step-by-step reproduction instructions that are precise, sequential, and leave nothing to interpretation**. My steps:
- Start with the initial platform/version/OS and dependencies used,
- Specify exact install/launch/configure commands,
- List URLs visited, user input (incl. example values/emails/passwords if needed),
- Describe all options and toggles enabled or changed,
- Include any files or environmental changes,
- Identify the expected and actual result at each stage,
- Ensure any reasonably skilled user can follow and hit the same issue.
required: true
- type: textarea
id: expected-behavior
attributes:
@@ -112,15 +123,25 @@ body:
id: reproduction-steps
attributes:
label: Steps to Reproduce
description: Providing clear, step-by-step instructions helps us reproduce and fix the issue faster. If we can't reproduce it, we can't fix it.
description: |
Please provide a **very detailed, step-by-step guide** to reproduce the issue. Your instructions should be so clear and precise that anyone can follow them without guesswork. Include every relevant detail—settings, configuration options, exact commands used, values entered, and any prerequisites or environment variables.
**If full reproduction steps and all relevant settings are not provided, your issue may not be addressed.**
placeholder: |
1. Go to '...'
2. Click on '...'
3. Scroll down to '...'
4. See the error message '...'
Example (include every detail):
1. Start with a clean Ubuntu 22.04 install.
2. Install Docker v24.0.5 and start the service.
3. Clone the Open WebUI repo (git clone ...).
4. Use the Docker Compose file without modifications.
5. Open browser Chrome 115.0 in incognito mode.
6. Go to http://localhost:8080 and log in with user "test@example.com".
7. Set the language to "English" and theme to "Dark".
8. Attempt to connect to Ollama at "http://localhost:11434".
9. Observe that the error message "Connection refused" appears at the top right.
Please list each step carefully and include all relevant configuration, settings, and options.
validations:
required: true
- type: textarea
id: logs-screenshots
attributes:
+4
View File
@@ -70,3 +70,7 @@
### Screenshots or Videos
- [Attach any relevant screenshots or videos demonstrating the changes]
### Contributor License Agreement
By submitting this pull request, I confirm that I have read and fully agree to the [Contributor License Agreement (CLA)](/CONTRIBUTOR_LICENSE_AGREEMENT), and I am providing my contributions under its terms.
+178 -13
View File
@@ -14,16 +14,18 @@ env:
jobs:
build-main-image:
runs-on: ${{ matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }}
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
include:
- platform: linux/amd64
runner: ubuntu-latest
- platform: linux/arm64
runner: ubuntu-24.04-arm
steps:
# GitHub Packages requires the entire repository name to be in lowercase
@@ -111,16 +113,18 @@ jobs:
retention-days: 1
build-cuda-image:
runs-on: ${{ matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }}
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
include:
- platform: linux/amd64
runner: ubuntu-latest
- platform: linux/arm64
runner: ubuntu-24.04-arm
steps:
# GitHub Packages requires the entire repository name to be in lowercase
@@ -210,17 +214,122 @@ jobs:
if-no-files-found: error
retention-days: 1
build-ollama-image:
runs-on: ${{ matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }}
build-cuda126-image:
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
include:
- platform: linux/amd64
runner: ubuntu-latest
- platform: linux/arm64
runner: ubuntu-24.04-arm
steps:
# GitHub Packages requires the entire repository name to be in lowercase
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
- name: Set repository and image name to lowercase
run: |
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
env:
IMAGE_NAME: '${{ github.repository }}'
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker images (cuda126 tag)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha,prefix=git-
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda126
flavor: |
latest=${{ github.ref == 'refs/heads/main' }}
suffix=-cuda126,onlatest=true
- name: Extract metadata for Docker cache
id: cache-meta
uses: docker/metadata-action@v5
with:
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
type=ref,event=branch
${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
flavor: |
prefix=cache-cuda126-${{ matrix.platform }}-
latest=false
- name: Build Docker image (cuda126)
uses: docker/build-push-action@v5
id: build
with:
context: .
push: true
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
build-args: |
BUILD_HASH=${{ github.sha }}
USE_CUDA=true
USE_CUDA_VER=cu126
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-cuda126-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
build-ollama-image:
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
- platform: linux/arm64
runner: ubuntu-24.04-arm
steps:
# GitHub Packages requires the entire repository name to be in lowercase
@@ -420,6 +529,62 @@ jobs:
run: |
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
merge-cuda126-images:
runs-on: ubuntu-latest
needs: [build-cuda126-image]
steps:
# GitHub Packages requires the entire repository name to be in lowercase
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
- name: Set repository and image name to lowercase
run: |
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
env:
IMAGE_NAME: '${{ github.repository }}'
- name: Download digests
uses: actions/download-artifact@v4
with:
pattern: digests-cuda126-*
path: /tmp/digests
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker images (default latest tag)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha,prefix=git-
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda126
flavor: |
latest=${{ github.ref == 'refs/heads/main' }}
suffix=-cuda126,onlatest=true
- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
merge-ollama-images:
runs-on: ubuntu-latest
needs: [build-ollama-image]
+2 -2
View File
@@ -32,7 +32,7 @@ jobs:
node-version: '22'
- name: Install Dependencies
run: npm install
run: npm install --force
- name: Format Frontend
run: npm run format
@@ -59,7 +59,7 @@ jobs:
node-version: '22'
- name: Install Dependencies
run: npm ci
run: npm ci --force
- name: Run vitest
run: npm run test:frontend
+4
View File
@@ -17,6 +17,10 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Git
run: sudo apt-get update && sudo apt-get install -y git
- uses: actions/setup-node@v4
with:
node-version: 22
+2 -1
View File
@@ -5,5 +5,6 @@
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"pluginSearchDirs": ["."],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }],
"endOfLine": "lf"
}
+478
View File
@@ -5,6 +5,484 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.6.18] - 2025-07-19
### Fixed
- 🚑 **Users Not Loading in Groups**: Resolved an issue where user list was not displaying within user groups, restoring full visibility and management of group memberships for teams and admins.
## [0.6.17] - 2025-07-19
### Added
- 📂 **Dedicated Folder View with Chat List**: Clicking a folder now reveals a brand-new landing page showcasing a list of all chats within that folder, making navigation simpler and giving teams immediate visibility into project-specific conversations.
- 🆕 **Streamlined Folder Creation Modal**: Creating a new folder is now a seamless, unified experience with a dedicated modal that visually and functionally matches the edit folder flow, making workspace organization more intuitive and error-free for all users.
- 🗃️ **Direct File Uploads to Folder Knowledge**: You can now upload files straight to a folder’s knowledge—empowering you to enrich project spaces by adding resources and documents directly, without the need to pre-create knowledge bases beforehand.
- 🔎 **Chat Preview in Search**: When searching chats, instantly preview results in context without having to open them—making discovery, auditing, and recall dramatically quicker, especially in large, active teams.
- 🖼️ **Image Upload and Inline Insertion in Notes**: Notes now support inserting images directly among your text, letting you create rich, visually structured documentation, brainstorms, or reports in a more natural and engaging way—no more images just as attachments.
- 📱 **Enhanced Note Selection Editing and Q&A**: Select any portion of your notes to either edit just the highlighted part or ask focused questions about that content—streamlining workflows, boosting productivity, and making reviews or AI-powered enhancements more targeted.
- 📝 **Copy Notes as Rich Text**: Copy entire notes—including all formatting, images, and structure—directly as rich text for seamless pasting into emails, reports, or other tools, maintaining clarity and consistency outside the WebUI.
- ⚡ **Fade-In Streaming Text Experience**: Live-generated responses now elegantly fade in as the AI streams them, creating a more natural and visually engaging reading experience; easily toggled off in Interface settings if you prefer static displays.
- 🔄 **Settings for Follow-Up Prompts**: Fine-tune your follow-up prompt experience—with new controls, you can choose to keep them visible or have them inserted directly into the message input instead of auto-submitting, giving you more flexibility and control over your workflow.
- 🔗 **Prompt Variable Documentation Quick Link**: Access documentation for prompt variables in one click from the prompt editor modal—shortening the learning curve and making advanced prompt-building more accessible.
- 📈 **Active and Total User Metrics for Telemetry**: Gain valuable insights into usage patterns and platform engagement with new metrics tracking active and total users—enhancing auditability and planning for large organizations.
- 🏷️ **Traceability with Log Trace and Span IDs**: Each log entry now carries detailed trace and span IDs, making it much easier for admins to pinpoint and resolve issues across distributed systems or in complex troubleshooting.
- 👥 **User Group Add/Remove Endpoints**: Effortlessly add or remove users from groups with new, improved endpoints—giving admins and team leads faster, clearer control over collaboration and permissions.
- ⚙️ **Note Settings and Controls Streamlined**: The main “Settings” for notes are now simply called “Controls”, and note files now reside in a dedicated controls section, decluttering navigation and making it easier to find and configure note-related options.
- 🚀 **Faster Admin User Page Loads**: The user list endpoint for admins has been optimized to exclude heavy profile images, speeding up load times for large teams and reducing waiting during administrative tasks.
- 📡 **Chat ID Header Forwarding**: Ollama and OpenAI router requests now include the chat ID in request headers, enabling better request correlation and debugging capabilities across AI model integrations.
- 🧠 **Enhanced Reasoning Tag Processing**: Improved and expanded reasoning tag parsing to handle various tag formats more robustly, including standard XML-style tags and custom delimiters, ensuring better AI reasoning transparency and debugging capabilities.
- 🔐 **OAuth Token Endpoint Authentication Method**: Added configurable OAuth token endpoint authentication method support, providing enhanced flexibility and security options for enterprise OAuth integrations and identity provider compatibility.
- 🛡️ **Redis Sentinel High Availability Support**: Comprehensive Redis Sentinel failover implementation with automatic master discovery, intelligent retry logic for connection failures, and seamless operation during master node outages—eliminating single points of failure and ensuring continuous service availability in production deployments.
- 🌐 **Localization & Internationalization Improvements**: Refined and expanded translations for Simplified Chinese, Traditional Chinese, French, German, Korean, and Polish, ensuring a more fluent and native experience for global users across all supported languages.
### Fixed
- 🏷️ **Hybrid Search Functionality Restored**: Hybrid search now works seamlessly again—enabling more accurate, relevant, and comprehensive knowledge discovery across all RAG-powered workflows.
- 🚦 **Note Chat - Edit Button Disabled During AI Generation**: The edit button when chatting with a note is now disabled while the AI is responding—preventing accidental edits and ensuring workflow clarity during chat sessions.
- 🧹 **Cleaner Database Credentials**: Database connection no longer duplicates ‘@’ in credentials, preventing potential connection issues and ensuring smoother, more reliable integrations.
- 🧑‍💻 **File Deletion Now Removes Related Vector Data**: When files are deleted from storage, they are now purged from the vector database as well, ensuring clean data management and preventing clutter or stale search results.
- 📁 **Files Modal Translation Issues Fixed**: All modal dialog strings—including “Using Entire Document” and “Using Focused Retrieval”—are now fully translated for a more consistent and localized UI experience.
- 🚫 **Drag-and-Drop File Upload Disabled for Unsupported Models**: File upload by drag-and-drop is disabled when using models that do not support attachments—removing confusion and preventing workflow interruptions.
- 🔑 **Ollama Tool Calls Now Reliable**: Fixed issues with Ollama-based tool calls, ensuring uninterrupted AI augmentation and tool use for every chat.
- 📄 **MIME Type Help String Correction**: Cleaned up mimetype help text by removing extraneous characters, providing clearer guidance for file upload configurations.
- 📝 **Note Editor Permission Fix**: Removed unnecessary admin-only restriction from note chat functionality, allowing all authorized users to access note editing features as intended.
- 📋 **Chat Sources Handling Improved**: Fixed sources handling logic to prevent duplicate source assignments in chat messages, ensuring cleaner and more accurate source attribution during conversations.
- 😀 **Emoji Generation Error Handling**: Improved error handling in audio router and fixed metadata structure for emoji generation tasks, preventing crashes and ensuring more reliable emoji generation functionality.
- 🔒 **Folder System Prompt Permission Enforcement**: System prompt fields in folder edit modal are now properly hidden for users without system prompt permissions, ensuring consistent security policy enforcement across all folder management interfaces.
- 🌐 **WebSocket Redis Lock Timeout Type Conversion**: Fixed proper integer type conversion for WebSocket Redis lock timeout configuration with robust error handling, preventing potential configuration errors and ensuring stable WebSocket connections.
- 📦 **PostHog Dependency Added**: Added PostHog 5.4.0 library to resolve ChromaDB compatibility issues, ensuring stable vector database operations and preventing library version conflicts during deployment.
### Changed
- 👀 **Tiptap Editor Upgraded to v3**: The underlying rich text editor has been updated for future-proofing, though some supporting libraries remain on v2 for compatibility. For now, please install dependencies using 'npm install --force' to avoid installation errors.
- 🚫 **Removed Redundant or Unused Strings and Elements**: Miscellaneous unused, duplicate, or obsolete code and translations have been cleaned up to maintain a streamlined and high-performance experience.
## [0.6.16] - 2025-07-14
### Added
- 🗂️ **Folders as Projects**: Organize your workflow with folder-based projects—set folder-level system prompts and associate custom knowledge, bringing seamless, context-rich management to teams and users handling multiple initiatives or clients.
- 📁 **Instant Folder-Based Chat Creation**: Start a new chat directly from any folder; just click and your new conversation is automatically embedded in the right project context—no more manual dragging or setup, saving time and eliminating mistakes.
- 🧩 **Prompt Variables with Automatic Input Modal**: Prompts containing variables now display a clean, auto-generated input modal that **autofocuses on the first field** for instant value entry—just select the prompt and fill in exactly what’s needed, reducing friction and guesswork.
- 🔡 **Variable Input Typing in Prompts**: Define input types for prompt variables (e.g., text, textarea, number, select, color, date, map and more), giving everyone a clearer and more precise prompt-building experience for advanced automation or workflows.
- 🚀 **Base Model List Caching**: Cache your base model list to speed up model selection and reduce repeated API calls; toggle this in Admin Settings > Connections for responsive model management even in large or multi-provider setups.
- ⏱️ **Configurable Model List Cache TTL**: Take control over model list caching with the new MODEL_LIST_CACHE_TTL environment variable. Set a custom cache duration in seconds to balance performance and freshness, reducing API requests in stable environments or ensuring rapid updates when models change frequently.
- 🔖 **Reference Notes as Knowledge or in Chats**: Use any note as knowledge for a model or folder, or reference it directly from chat—integrate living documentation into your Retrieval Augmented Generation workflows or discussions, bridging knowledge and action.
- 📝 **Chat Directly with Notes (Experimental)**: Ask questions about any note, and directly edit or update notes from within a chat—unlock direct AI-powered brainstorming, summarization, and cleanup, like having your own collaborative AI canvas.
- 🤝 **Collaborative Notes with Multi-User Editing**: Share notes with others and collaborate live—multiple users can edit a note in real-time, boosting cooperative knowledge building and workflow documentation.
- 🛡️ **Collaborative Note Permissions**: Control who can view or edit each note with robust sharing permissions, ensuring privacy or collaboration per your organizational needs.
- 🔗 **Copy Link to Notes**: Quickly copy and share direct links to notes for easier knowledge transfer within your team or external collaborators.
- 📋 **Task List Support in Notes**: Add, organize, and manage checklists or tasks inside your notes—plan projects, track to-dos, and keep everything actionable in a single space.
- 🧠 **AI-Generated Note Titles**: Instantly generate relevant and concise titles for your notes using AI—keep your knowledge library organized without tedious manual editing.
- 🔄 **Full Undo/Redo Support in Notes**: Effortlessly undo or redo your latest note changes—never fear mistakes or accidental edits while collaborating or writing.
- 📝 **Enhanced Note Word/Character Counter**: Always know the size of your notes with built-in counters, making it easier to adhere to length guidelines for shared or published content.
- 🖊️ **Floating & Bubble Formatting Menus in Note Editor**: Access text formatting tools through both a floating menu and an intuitive bubble menu directly in the note editor—making rich text editing faster, more discoverable, and easier than ever.
- ✍️ **Rich Text Prompt Insertion**: A new setting allows prompts to be inserted directly into the chat box as fully-formatted rich text, preserving Markdown elements like headings, lists, and bold text for a more intuitive and visually consistent editing experience.
- 🌐 **Configurable Database URL**: WebUI now supports more flexible database configuration via new environment variables—making deployment and scaling simpler across various infrastructure setups.
- 🎛️ **Completely Frontend-Handled File Upload in Temporary Chats**: When using temporary chats, file extraction now occurs fully in your browser with zero files sent to the backend, further strengthening privacy and giving you instant feedback.
- 🔄 **Enhanced Banner and Chat Command Visibility**: Banner handling and command feedback in chat are now clearer and more contextually visible, making alerts, suggestions, and automation easier to spot and interact with for all users.
- 📱 **Mobile Experience Polished**: The "new chat" button is back in mobile, plus core navigation and input controls have been smoothed out for better usability on phones and tablets.
- 📄 **OpenDocument Text (.odt) Support**: Seamlessly upload and process .odt files from open-source office suites like LibreOffice and OpenOffice, expanding your ability to build knowledge from a wider range of document formats.
- 📑 **Enhanced Markdown Document Splitting**: Improve knowledge retrieval from Markdown files with a new header-aware splitting strategy. This method intelligently chunks documents based on their header structure, preserving the original context and hierarchy for more accurate and relevant RAG results.
- 📚 **Full Context Mode for Knowledge Bases**: When adding a knowledge base to a folder or custom model, you can now toggle full context mode for the entire knowledge base. This bypasses the usual chunking and retrieval process, making it perfect for leaner knowledge bases.
- 🕰️ **Configurable OAuth Timeout**: Enhance login reliability by setting a custom timeout (OAUTH_TIMEOUT) for all OAuth providers (Google, Microsoft, GitHub, OIDC), preventing authentication failures on slow or restricted networks.
- 🎨 **Accessibility & High-Contrast Theme Enhancements**: Major accessibility overhaul with significant updates to the high-contrast theme. Improved focus visibility, ARIA labels, and semantic HTML ensure core components like the chat interface and model selector are fully compliant and readable for visually impaired users.
- ↕️ **Resizable System Prompt Fields**: Conveniently resize system prompt input fields to comfortably view and edit lengthy or complex instructions, improving the user experience for advanced model configuration.
- 🔧 **Granular Update Check Control**: Gain finer control over outbound connections with the new ENABLE_VERSION_UPDATE_CHECK flag. This allows administrators to disable version update checks independently of the full OFFLINE_MODE, perfect for environments with restricted internet access that still need to download embedding models.
- 🗃️ **Configurable Qdrant Collection Prefix**: Enhance scalability by setting a custom QDRANT_COLLECTION_PREFIX. This allows multiple Open WebUI instances to share a single Qdrant cluster safely, ensuring complete data isolation between separate deployments without conflicts.
- ⚙️ **Improved Default Database Performance**: Enhanced out-of-the-box performance by setting smarter database connection pooling defaults, reducing API response times for users on non-SQLite databases without requiring manual configuration.
- 🔧 **Configurable Redis Key Prefix**: Added support for the REDIS_KEY_PREFIX environment variable, allowing multiple Open WebUI instances to share a Redis cluster with isolated key namespaces for improved multi-tenancy.
- ➡️ **Forward User Context to Reranker**: For advanced RAG integrations, user information (ID, name, email, role) can now be forwarded as HTTP headers to external reranking services, enabling personalized results or per-user access control.
- ⚙️ **PGVector Connection Pooling**: Enhance performance and stability for PGVector-based RAG by enabling and configuring the database connection pool. New environment variables allow fine-tuning of pool size, timeout, and overflow settings to handle high-concurrency workloads efficiently.
- ⚙️ **General Backend Refactoring**: Extensive refactoring delivers a faster, more reliable, and robust backend experience—improving chat speed, model management, and day-to-day reliability.
- 🌍 **Expanded & Improved Translations**: Enjoy a more accessible and intuitive experience thanks to comprehensive updates and enhancements for Chinese (Simplified and Traditional), German, French, Catalan, Irish, and Spanish translations throughout the interface.
### Fixed
- 🛠️ **Rich Text Input Stability and Performance**: Multiple improvements ensure faster, cleaner text editing and rendering with reduced glitches—especially supporting links, color picking, checkbox controls, and code blocks in notes and chats.
- 📷 **Seamless iPhone Image Uploads**: Effortlessly upload photos from iPhones and other devices using HEIC format—images are now correctly recognized and processed, eliminating compatibility issues.
- 🔄 **Audio MIME Type Registration**: Issues with audio file content types have been resolved, guaranteeing smoother, error-free uploads and playback for transcription or note attachments.
- 🖍️ **Input Commands Now Always Visible**: Input commands (like prompts or knowledge) dynamically adjust their height on small screens, ensuring nothing is cut off and every tool remains easily accessible.
- 🛑 **Tool Result Rendering**: Fixed display problems with tool results, providing fast, clear feedback when using external or internal tools.
- 🗂️ **Table Alignment in Markdown**: Markdown tables are now rendered and aligned as expected, keeping reports and documentation readable.
- 🖼️ **Thread Image Handling**: Fixed an issue where messages containing only images in threads weren’t displayed correctly.
- 🗝️ **Note Access Control Security**: Tightened access control logic for notes to guarantee that shared or collaborative notes respect all user permissions and privacy safeguards.
- 🧾 **Ollama API Compatibility**: Fixed model parameter naming in the API to ensure uninterrupted compatibility for all Ollama endpoints.
- 🛠️ **Detection for 'text/html' Files**: Files loaded with docling/tika are now reliably detected as the correct type, improving knowledge ingestion and document parsing.
- 🔐 **OAuth Login Stability**: Resolved a critical OAuth bug that caused login failures on subsequent attempts after logging out. The user session is now completely cleared on logout, ensuring reliable and secure authentication across all supported providers (Google, Microsoft, GitHub, OIDC).
- 🚪 **OAuth Logout and Redirect Reliability**: The OAuth logout process has been made more robust. Logout requests now correctly use proxy environment variables, ensuring they succeed in corporate networks. Additionally, the custom WEBUI_AUTH_SIGNOUT_REDIRECT_URL is now properly respected for all OAuth/OIDC configurations, ensuring a seamless sign-out experience.
- 📜 **Banner Newline Rendering**: Banners now correctly render newline characters, ensuring that multi-line announcements and messages are displayed with their intended formatting.
- ℹ️ **Consistent Model Description Rendering**: Model descriptions now render Markdown correctly in the main chat interface, matching the formatting seen in the model selection dropdown for a consistent user experience.
- 🔄 **Offline Mode Update Check Display**: Corrected a UI bug where the "Checking for Updates..." message would display indefinitely when the application was set to offline mode.
- 🛠️ **Tool Result Encoding**: Fixed a bug where tool calls returning non-ASCII characters would fail, ensuring robust handling of international text and special characters in tool outputs.
## [0.6.15] - 2025-06-16
### Added
- 🖼️ **Global Image Compression Option**: Effortlessly set image compression globally so all image uploads and outputs are optimized, speeding up load times and saving bandwidth—perfect for teams dealing with large files or limited network resources.
- 🎤 **Custom Speech-to-Text Content-Type for Transcription**: Define custom content types for audio transcription, ensuring compatibility with diverse audio sources and unlocking smoother, more accurate transcriptions in advanced setups.
- 🗂️ **LDAP Group Synchronization (Experimental)**: Automatically sync user groups from your LDAP directory directly into Open WebUI for seamless enterprise access management—simplifies identity integration and governance across your organization.
- 📈 **OpenTelemetry Metrics via OTLP Exporter (Experimental)**: Gain enterprise-grade analytics and monitor your AI usage in real time with experimental OpenTelemetry Metrics support—connect to any OTLP-compatible backend for instant insights into performance, load, and user interactions.
- 🕰️ **See User Message Timestamps on Hover (Chat Bubble UI)**: Effortlessly check when any user message was sent by hovering over it in Chat Bubble mode—no more switching screens or digging through logs for context.
- 🗂️ **Leaderboard Sorting Options**: Sort the leaderboard directly in the UI for a clearer, more actionable view of top performers, models, or tools—making analysis and recognition quick and easy for teams.
- 🏆 **Evaluation Details Modal in Feedbacks and Leaderboard**: Dive deeper with new modals that display detailed evaluation information when reviewing feedbacks and leaderboard rankings—accelerates learning, progress tracking, and quality improvement.
- 🔄 **Support for Multiple Pages in External Document Loaders**: Effortlessly extract and work with content spanning multiple pages in external documents, giving you complete flexibility for in-depth research and document workflows.
- 🌐 **New Accessibility Enhancements Across the Interface**: Benefit from significant accessibility improvements—tab navigation, ARIA roles/labels, better high-contrast text/modes, accessible modals, and more—making Open WebUI more usable and equitable for everyone, including those using assistive technologies.
- ⚡ **Performance & Stability Upgrades Across Frontend and Backend**: Enjoy a smoother, more reliable experience with numerous behind-the-scenes optimizations and refactoring on both frontend and backend—resulting in faster load times, fewer errors, and even greater stability throughout your workflows.
- 🌏 **Updated and Expanded Localizations**: Enjoy improved, up-to-date translations for Finnish, German (now with model pinning features), Korean, Russian, Simplified Chinese, Spanish, and more—making every interaction smoother, clearer, and more intuitive for international users.
### Fixed
- 🦾 **Ollama Error Messages More Descriptive**: Receive clearer, more actionable error messages when something goes wrong with Ollama models—making troubleshooting and user support faster and more effective.
- 🌐 **Bypass Webloader Now Works as Expected**: Resolved an issue where the "bypass webloader" feature failed to function correctly, ensuring web search bypasses operate smoothly and reliably for lighter, faster query results.
- 🔍 **Prevent Redundant Documents in Citation List**: The expanded citation list no longer shows duplicate documents, offering a cleaner, easier-to-digest reference experience when reviewing sources in knowledge and research workflows.
- 🛡️ **Trusted Header Email Matching is Now Case-Insensitive**: Fixed a critical authentication issue where email case sensitivity could cause secure headers to mismatch, ensuring robust, seamless login and session management in all environments.
- ⚙️ **Direct Tool Server Input Accepts Empty Strings**: You can now submit direct tool server commands without unexpected errors when passing empty-string values, improving integration and automation efficiency.
- 📄 **Citation Page Number for Page 1 is Now Displayed**: Corrected an oversight where references for page 1 documents were missing the page number; citations are now always accurate and fully visible.
- 📒 **Notes Access Restored**: Fixed an issue where some users could not access their notes—everyone can now view and manage their notes reliably, ensuring seamless documentation and workflow continuity.
- 🛑 **OAuth Callback Double-Slash Issue Resolved**: Fixed rare cases where an extra slash in OAuth callbacks caused failed logins or redirects, making third-party login integrations more reliable.
### Changed
- 🔑 **Dedicated Permission for System Prompts**: System prompt access is now controlled by its own specific permission instead of being grouped with general chat controls, empowering admins with finer-grained management over who can view or modify system prompts for enhanced security and workflow customization.
- 🛠️ **YouTube Transcript API and python-pptx Updated**: Enjoy better performance, reliability, and broader compatibility thanks to underlying library upgrades—less friction with media-rich and presentation workflows.
### Removed
- 🗑️ **Console Logging Disabled in Production**: All 'console.log' and 'console.debug' statements are now disabled in production, guaranteeing improved security and cleaner browser logs for end users by removing extraneous technical output.
## [0.6.14] - 2025-06-10
### Added
- 🤖 **Automatic "Follow Up" Suggestions**: Open WebUI now intelligently generates actionable "Follow Up" suggestions automatically with each message you send, helping you stay productive and inspired without interrupting your flow; you can always disable this in Settings if you prefer a distraction-free experience.
- 🧩 **OpenAI-Compatible Embeddings Endpoint**: Introducing a fully OpenAI-style '/api/embeddings' endpoint—now you can plug in OpenAI-style embeddings workflows with zero hassle, making integrations with external tools and platforms seamless and familiar.
- ↗️ **Model Pinning for Quick Access**: Pin your favorite or most-used models to the sidebar for instant selection—no more scrolling through long model lists; your go-to models are always visible and ready for fast access.
- 📌 **Selector Model Item Menu**: Each model in the selector now features a menu where you can easily pin/unpin to the sidebar and copy a direct link—simplifying collaboration and staying organized in even the busiest environments.
- 🛑 **Reliable Stop for Ongoing Chats in Multi-Replica Setups**: Stopping or cancelling an in-progress chat now works reliably even in clustered deployments—ensuring every user can interrupt AI output at any time, no matter your scale.
- 🧠 **'Think' Parameter for Ollama Models**: Leverage new 'think' parameter support for Ollama—giving you advanced control over AI reasoning process and further tuning model behavior for your unique use cases.
- 💬 **Picture Description Modes for Docling**: Customize how images are described/extracted by Docling Loader for smarter, more detailed, and workflow-tailored image understanding in your document pipelines.
- 🛠 **Settings Modal Deep Linking**: Every tab in Settings now has its own route—making direct navigation and sharing of precise settings faster and more intuitive.
- 🎤 **Audio HTML Component Token**: Easily embed and play audio directly in your chats, improving voice-based workflows and making audio content instantly accessible and manageable from any conversation.
- 🔑 **Support for Secret Key File**: Now you can specify 'WEBUI_SECRET_KEY_FILE' for more secure and flexible key management—ideal for advanced deployments and tighter security standards.
- 💡 **Clarity When Cloning Prompts**: Cloned workspace prompts are clearly labelled with "(Clone)" and IDs have "-clone", keeping your prompt library organized and preventing accidental overwrites.
- 📝 **Dedicated User Role Edit Modal**: Updating user roles now reliably opens a dedicated edit user modal instead of cycling through roles—making it safer and more clear to manage team permissions.
- 🏞️ **Better Handling & Storage of Interpreter-Generated Images**: Code interpreter-generated images are now centrally stored and reliably loaded from the database or cloud storage, ensuring your artifacts are always available.
- 🚀 **Pinecone & Vector Search Optimizations**: Applied latest best practices from Pinecone for smarter timeouts, intelligent retry control, improved connection pooling, faster DNS, and concurrent batch handling—giving you more reliable, faster document search and RAG performance without manual tweaks.
- ⚙️ **Ollama Advanced Parameters Unified**: 'keep_alive' and 'format' options are now integrated into the advanced params section—edit everything from the model editor for flexible model control.
- 🛠️ **CUDA 12.6 Docker Image Support**: Deploy to NVIDIA GPUs with capability 7.0 and below (e.g., V100, GTX1080) via new cuda126 image—broadening your hardware options for scalable AI workloads.
- 🔒 **Experimental Table-Level PGVector Data Encryption**: Activate pgcrypto encryption support for pgvector to secure your vector search table contents, giving organizations enhanced compliance and data protection—perfect for enterprise or regulated environments.
- 👁 **Accessibility Upgrades Across Interface**: Chat buttons and close controls are now labelled and structured for optimal accessibility support, ensuring smoother operation with assistive technologies.
- 🎨 **High-Contrast Mode Expansions**: High-contrast accessibility mode now also applies to menu items, tabs, and search input fields, offering a more readable experience for all users.
- 🛠️ **Tooltip & Translation Clarity**: Improved translation and tooltip clarity, especially over radio buttons, making the UI more understandable for all users.
- 🔠 **Global Localization & Translation Improvements**: Hefty upgrades to Traditional Chinese, Simplified Chinese, Hebrew, Russian, Irish, German, and Danish translation packs—making the platform feel native and intuitive for even more users worldwide.
- ⚡ **General Backend Stability & Security Enhancements**: Refined numerous backend routines to minimize memory use, improve performance, and streamline integration with external APIs—making the entire platform more robust and secure for daily work.
### Fixed
- 🏷 **Feedback Score Display Improved**: Addressed overflow and visibility issues with feedback scores for more readable and accessible evaluations.
- 🗂 **Admin Settings Model Edits Apply Immediately**: Changes made in the Model Editor within Admin Settings now take effect instantly, eliminating confusion during model management.
- 🔄 **Assigned Tools Update Instantly on New Chats**: Models assigned with specific tools now consistently update and are available in every new chat—making tool workflows more predictable and robust.
- 🛠 **Document Settings Saved Only on User Action**: Document settings now save only when you press the Save button, reducing accidental changes and ensuring greater control.
- 🔊 **Voice Recording on Older iOS Devices Restored**: Voice input is now fully functional on older iOS devices, keeping voice workflows accessible to all users.
- 🔒 **Trusted Email Header Session Security**: User sessions now strictly verify the trusted email header matches the logged-in user's email, ensuring secure authentication and preventing accidental session switching.
- 🔒 **Consistent User Signout on Email Mismatch**: When the trusted email in the header changes, you will now be properly signed out and redirected, safeguarding your session's integrity.
- 🛠 **General Error & Content Validation Improvements**: Smarter error handling means clearer messages and fewer unnecessary retries—making batch uploads, document handling, and knowledge indexing more resilient.
- 🕵️ **Better Feedback on Chat Title Edits**: Error messages now show clearly if problems occur while editing chat titles.
## [0.6.13] - 2025-05-30
### Added
- 🟦 **Azure OpenAI Embedding Support**: You can now select Azure OpenAI endpoints for text embeddings, unlocking seamless integration with enterprise-scale Azure AI for powerful RAG and knowledge workflows—no more workarounds, connect and scale effortlessly.
- 🧩 **Smarter Custom Parameter Handling**: Instantly enjoy more flexible model setup—any JSON pasted into custom parameter fields is now parsed automatically, so you can define rich, nested parameters without tedious manual adjustment. This streamlines advanced configuration for all models and accelerates experimentation.
- ⚙️ **General Backend Refactoring**: Significant backend improvements deliver a cleaner codebase for better maintainability, faster performance, and even greater platform reliability—making all your workflows run more smoothly.
- 🌏 **Localization Upgrades**: Experience highly improved user interface translations and clarity in Simplified, Traditional Chinese, Korean, and Finnish, offering a more natural, accurate, and accessible experience for global users.
### Fixed
- 🛡️ **Robust Message Handling on Chat Load**: Fixed an issue where chat pages could fail to load if a referenced message was missing or undefined; now, chats always load smoothly and missing IDs no longer disrupt your workflow.
- 📝 **Correct Prompt Access Control**: Ensured that the prompt access controls register properly, restoring reliable permissioning and safeguarding your prompt workflows.
- 🛠 **Open WebUI-Specific Params No Longer Sent to Models**: Fixed a bug that sent internal WebUI parameters to APIs, ensuring only intended model options are transmitted—restoring predictable, error-free model operation.
- 🧠 **Refined Memory Error Handling**: Enhanced stability during memory-related operations, so even uncommon memory errors are gracefully managed without disrupting your session—resulting in a more reliable, worry-free experience.
## [0.6.12] - 2025-05-29
### Added
- 🧩 **Custom Advanced Model Parameters**: You can now add your own tailor-made advanced parameters to any model, empowering you to fine-tune behavior and unlock greater flexibility beyond just the built-in options—accelerate your experimentation.
- 🪧 **Datalab Marker API Content Extraction Support**: Seamlessly extract content from files and documents using the Datalab Marker API directly in your workflows, enabling more robust structured data extraction for RAG and document processing with just a simple engine switch in the UI.
- ⚡ **Parallelized Base Model Fetching**: Experience noticeably faster startup and model refresh times—base model data now loads in parallel, drastically shortening delays in busy or large-scale deployments.
- 🧠 **Efficient Function Loading and Caching**: Functions are now only reloaded if their content changes, preventing unnecessary duplicate loads, saving bandwidth, and boosting performance.
- 🌍 **Localization & Translation Enhancements**: Improved and expanded Simplified, Traditional Chinese, and Russian translations, providing smoother, more accurate, and context-aware experiences for global users.
### Fixed
- 💬 **Stable Message Input Box**: Fixed an issue where the message input box would shift unexpectedly (especially on mobile or with screen reader support), ensuring a smooth and reliable typing experience for every user.
- 🔊 **Reliable Read Aloud (Text-to-Speech)**: Read aloud now works seamlessly across messages, so users depending on TTS for accessibility or multitasking will experience uninterrupted and clear voice playback.
- 🖼 **Image Preview and Download Restored**: Fixed problems with image preview and downloads, ensuring frictionless creation, previewing, and downloading of images in your chats—no more interruptions in creative or documentation workflows.
- 📱 **Improved Mobile Styling for Workspace Capabilities**: Capabilities management is now readable and easy-to-use even on mobile devices, empowering admins and users to manage access quickly on the go.
- 🔁 **/api/v1/retrieval/query/collection Endpoint Reliability**: Queries to retrieval collections now return the expected results, bolstering the reliability of your knowledge workflows and citation-ready responses.
### Removed
- 🧹 **Duplicate CSS Elements**: Streamlined the UI by removing redundant CSS, reducing clutter and improving load times for a smoother visual experience.
## [0.6.11] - 2025-05-27
### Added
- 🟢 **Ollama Model Status Indicator in Model Selector**: Instantly see which Ollama models are currently loaded with a clear indicator in the model selector, helping you stay organized and optimize local model usage.
- 🗑️ **Unload Ollama Model Directly from Model Selector**: Easily release memory and resources by unloading any loaded Ollama model right in the model selector—streamline hardware management without switching pages.
- 🗣️ **User-Configurable Speech-to-Text Language Setting**: Improve transcription accuracy by letting individual users explicitly set their preferred STT language in their settings—ideal for multilingual teams and clear audio capture.
- ⚡ **Granular Audio Playback Speed Control**: Instead of just presets, you can now choose granular audio speed using a numeric input, giving you complete control over playback pace in transcriptions and media reviews.
- 📦 **GZip, Brotli, ZStd Compression Middleware**: Enjoy significantly faster page loads and reduced bandwidth usage with new server-side compression—giving users a snappier, more efficient experience.
- 🏷️ **Configurable Weight for BM25 in Hybrid Search**: Fine-tune search relevance by adjusting the weight for BM25 inside hybrid search from the UI, letting you tailor knowledge search results to your workflow.
- 🧪 **Bypass File Creation with CTRL + SHIFT + V**: When “Paste Large Text as File” is enabled, use CTRL + SHIFT + V to skip the file creation dialog and instantly upload text as a file—perfect for rapid document prep.
- 🌐 **Bypass Web Loader in Web Search**: Choose to bypass web content loading and use snippets directly in web search for faster, more reliable results when page loads are slow or blocked.
- 🚀 **Environment Variable: WEBUI_AUTH_TRUSTED_GROUPS_HEADER**: Now sync and manage user groups directly via trusted HTTP header, unlocking smoother single sign-on and identity integrations for organizations.
- 🏢 **Workspace Models Visibility Controls**: You can now hide workspace-level models from both the model selector and shared environments—keep your team focused and reduce clutter from rarely-used endpoints.
- 🛡️ **Copy Model Link**: You can now copy a direct link to any model—including those hidden from the selector—making sharing and onboarding others more seamless.
- 🔗 **Load Function Directly from URL**: Simplify custom function management—just paste any GitHub function URL into Open WebUI and import new functions in seconds.
- ⚙️ **Custom Name/Description for External Tool Servers**: Personalize and clarify external tool servers by assigning custom names and descriptions, making it easier to manage integrations in large-scale workspaces.
- 🌍 **Custom OpenAPI JSON URL Support for Tool Servers**: Supports specifying any custom OpenAPI JSON URL, unlocking more flexible integration with any backend for tool calls.
- 📊 **Source Field Now Displays in Non-Streaming Responses with Attachments**: When files or knowledge are attached, the "source" field now appears for all responses, even in non-streaming mode—enabling improved citation workflow.
- 🎛 **Pinned Chats**: Reduced payload size on pinned chat requests—leading to faster load times and less data usage, especially on busy warehouses.
- 🛠 **Import/Export Default Prompt Suggestions**: Enjoy one-click import/export of prompt suggestions, making it much easier to share, reuse, and manage best practices across teams or deployments.
- 🍰 **Banners Now Sortable from Admin Settings**: Quickly re-order or prioritize banners, letting you highlight the most critical info for your team.
- 🛠 **Advanced Chat Parameters—Clearer Ollama Support Labels**: Parameters and advanced settings now explicitly indicate if they are Ollama-specific, reducing confusion and improving setup accuracy.
- 🤏 **Scroll Bar Thumb Improved for Better Visibility**: Enhanced scrollbar styling makes navigation more accessible and visually intuitive.
- 🗄️ **Modal Redesign for Archived and User Chat Listings**: Clean, modern modal interface for browsing archived and user-specific chats makes locating conversations faster and more pleasant.
- 📝 **Add/Edit Memory Modal UX**: Memory modals are now larger and have resizable input fields, supporting easier editing of long or complex memory content.
- 🏆 **Translation & Localization Enhancements**: Major upgrades to Chinese (Simplified & Traditional), Korean, Russian, German, Danish, Finnish—not just fixing typos, but consistency, tone, and terminology for a more natural native-language experience.
- ⚡ **General Backend Stability & Security Enhancements**: Various backend refinements ensure a more resilient, reliable, and secure platform for smoother operation and peace of mind.
### Fixed
- 🖼️ **Image Generation with Allowed File Extensions Now Works Reliably**: Ensure seamless image generation even when strict file extension rules are set—no more blocked creative workflows due to technical hiccups.
- 🗂 **Remove Leading Dot for File Extension Check**: Fixed an issue where file validation failed because of a leading dot, making file uploads and knowledge management more robust.
- 🏷️ **Correct Local/External Model Classification**: The platform now accurately distinguishes between local and external models—preventing local models from showing up as external (and vice versa)—ensuring seamless setup, clarity, and management of your AI model endpoints.
- 📄 **External Document Loader Now Functions as Intended**: External document loaders are reliably invoked, ensuring smoother knowledge ingestion from external sources—expanding your RAG and knowledge workflows.
- 🎯 **Correct Handling of Toggle Filters**: Toggle filters are now robustly managed, preventing accidental auto-activation and ensuring user preferences are always respected.
- 🗃 **S3 Tagging Character Restrictions Fixed**: Tags for files in S3 now automatically meet Amazon’s allowed character set, avoiding upload errors and ensuring cross-cloud compatibility.
- 🛡️ **Authentication Now Uses Password Hash When Duplicate Emails Exist**: Ensures account security and prevents access issues if duplicate emails are present in your system.
### Changed
- 🧩 **Admin Settings: OAuth Redirects Now Use WEBUI_URL**: The OAuth redirect URL is now based on the explicitly set WEBUI_URL, ensuring single sign-on and identity provider integrations always send users to the correct frontend.
### Removed
- 💡 **Duplicate/Typo Component Removals**: Obsolete components have been cleaned up, reducing confusion and improving overall code quality for the team.
- 🚫 **Streaming Upsert in Pinecone Removed**: Removed streaming upsert references for better compatibility and future-proofing with latest Pinecone SDK updates.
## [0.6.10] - 2025-05-19
### Added
- 🧩 **Experimental Azure OpenAI Support**: Instantly connect to Azure OpenAI endpoints by simply pasting your Azure OpenAI URL into the model connections—bringing flexible, enterprise-grade AI integration directly to your workflow.
- 💧 **Watermark AI Responses**: Easily add a visible watermark to AI-generated responses for clear content provenance and compliance with EU AI regulations—perfect for regulated environments and transparent communication.
- 🔍 **Enhanced Search Experience with Dedicated Modal**: Enjoy a modern, powerful search UI in a dedicated modal (open with Ctrl/Cmd + K) accessible from anywhere—quickly find chats, models, or content and boost your productivity.
- 🔲 **"Toggle" Filter Type for Chat Input**: Add interactive toggle filters (e.g. Web Search, Image, Code Interpreter) right into the chat input—giving you one-click control to activate features and simplifying the chat configuration process.
- 🧰 **Granular Model Capabilities Editor**: Define detailed capabilities and feature access for each AI model directly from the model editor—enabling tailored behavior, modular control, and a more customized AI environment for every team or use case.
- 🌐 **Flexible Local and External Connection Support**: Now you can designate any AI connection—whether OpenAI, Ollama, or others—as local or external, enabling seamless setup for on-premise, self-hosted, or cloud configurations and giving you maximum control and flexibility.
- 🗂️ **Allowed File Extensions for RAG**: Gain full control over your Retrieval-Augmented Generation (RAG) workflows by specifying exactly which file extensions are permitted for upload, improving security and relevance of indexed documents.
- 🔊 **Enhanced Audio Transcription Logic**: Experience smoother, more reliable audio transcription—very long audio files are now automatically split and processed in segments, preventing errors and ensuring even challenging files are transcribed seamlessly, all part of a broader stability upgrade for robust media workflows.
- 🦾 **External Document Loader Support**: Enhance knowledge base building by integrating documents using external loaders from a wide range of data sources, expanding what your AI can read and process.
- 📝 **Preview Button for Code Artifacts**: Instantly jump from an HTML code block to its associated artifacts page with the click of a new preview button—speeding up review and streamlining analysis.
- 🦻 **Screen Reader Support for Response Messages**: All chat responses are now fully compatible with screen readers, making the platform more inclusive and accessible for everyone.
- 🧑‍💼 **Customizable Pending User Overlay**: You can now tailor the overlay title and content shown to pending users, ensuring onboarding messaging is perfectly aligned with your organization’s tone and requirements.
- 🔐 **Option to Disable LDAP Certificate Validation**: You can now disable LDAP certificate validation for maximum flexibility in diverse IT environments—making integrations and troubleshooting much easier.
- 🎯 **Workspace Search by Username or Email**: Easily search across workspace pages using any username or email address, streamlining user and resource management.
- 🎨 **High Contrast & Dark Mode Enhancements**: Further improved placeholder, input, suggestion, toast, and model selector contrasts—including a dedicated placeholder dark mode—for more comfortable viewing in all lighting conditions.
- 🛡️ **Refined Security for Pipelines & Model Uploads**: Strengthened safeguards against path traversal vulnerabilities during uploads—ensuring your platform’s document and model management remains secure.
- 🌏 **Major Localization Upgrades**: Comprehensive translation updates and improvements across Korean, Bulgarian, Catalan, Japanese, Italian, Traditional Chinese, and Spanish—including more accurate AI terminology for clarity; your experience is now more natural, inclusive, and professional everywhere.
- 🦾 **General Backend Stability & Security**: Multiple backend improvements (including file upload, command navigation, and logging refactorings) deliver increased resilience, better error handling, and a more robust platform for all users.
### Fixed
- ✅ **Evaluation Feedback Endpoint Reliability**: Addressed issues with feedback submission endpoints to ensure seamless user feedback collection on model responses.
- 🫰 **Model List State Fixes**: Resolved issues where model status toggles in the workspace page might inadvertently switch or confuse state, making the management of active/inactive models more dependable.
- ✍️ **Admin Signup Logic**: Admin self-signup experience and validation flow is smoother and more robust.
- 🔁 **Signout Redirect Flow Improved**: Logging out now redirects more reliably, reducing confusion and making session management seamless.
## [0.6.9] - 2025-05-10
### Added
- 📝 **Edit Attached Images/Files in Messages**: You can now easily edit your sent messages by removing attached files—streamlining document management, correcting mistakes on the fly, and keeping your chats clutter-free.
- 🚨 **Clear Alerts for Private Task Models**: When interacting with private task models, the UI now clearly alerts you—making it easier to understand resource availability and access, reducing confusion during workflow setup.
### Fixed
- 🛡️ **Confirm Dialog Focus Trap Reliability**: The focus now stays correctly within confirmation dialogs, ensuring keyboard navigation and accessibility is seamless and preventing accidental operations—especially helpful during critical or rapid workflows.
- 💬 **Temporary Chat Admin Controls & Session Cleanliness**: Admins are now able to properly enable temporary chat mode without errors, and previous session prompts or tool selections no longer carry over—delivering a fresh, predictable, and consistent temporary chat experience every time.
- 🤖 **External Reranker Integration Functionality Restored**: External reranker integrations now work correctly, allowing you to fully leverage advanced ranking services for sharper, more relevant search results in your RAG and knowledge base workflows.
## [0.6.8] - 2025-05-10
### Added
- 🏆 **External Reranker Support for Knowledge Base Search**: Supercharge your Retrieval-Augmented Generation (RAG) workflows with the new External Reranker integration; easily plug in advanced reranking services via the UI to deliver sharper and more relevant search results, accelerating research and insight discovery.
- 📤 **Unstylized PDF Export Option (Reduced File Size)**: When exporting chat transcripts or documents, you can now choose an unstylized PDF export for snappier downloads, minimal file size, and clean data archiving—perfect for large-scale storage or sharing.
- 📝 **Vazirmatn Font for Persian & Arabic**: Arabic and Persian users will now see their text beautifully rendered with the specialized Vazirmatn font for an improved localized reading experience.
- 🏷️ **SharePoint Tenant ID Support for OneDrive**: You can now specify a SharePoint tenant ID in OneDrive settings for seamless authentication and granular enterprise integration.
- 👤 **Refresh OAuth Profile Picture**: Your OAuth profile picture now updates in real-time, ensuring your presence and avatar always match your latest identity across integrated platforms.
- 🔧 **Milvus Configuration Improvements**: Configure index and metric types for Milvus directly within settings; take full control of your vector database for more accurate and robust AI search experiences.
- 🛡️ **S3 Tagging Toggle for Compatibility**: Optional S3 tagging via an environment toggle grants full compatibility with all storage backends—including those that don’t support tagging like Cloudflare R2—ensuring error-free attachment and document management.
- 👨‍🦯 **Icon Button Accessibility Improvements**: Key interactive icon-buttons now include aria-labels and ARIA descriptions, so screen readers provide precise guidance about what action each button performs for improved accessibility.
- ♿ **Enhanced Accessibility with Modal Focus Trap**: Modal dialogs and pop-ups now feature a focus trap and improved ARIA roles, ensuring seamless navigation and screen reader support—making the interface friendlier for everyone, including keyboard and assistive tech users.
- 🏃 **Improved Admin User List Loading Indicator**: The user list loading experience is now clearer and more responsive in the admin panel.
- 🧑‍🤝‍🧑 **Larger Admin User List Page Size**: Admins can now manage up to 30 users per page in the admin interface, drastically reducing pagination and making large user teams easier and faster to manage.
- 🌠 **Default Code Interpreter Prompt Clarified**: The built-in code interpreter prompt is now more explicit, preventing AI from wrapping code in Markdown blocks when not needed—ensuring properly formatted code runs as intended every time.
- 🧾 **Improved Default Title Generation Prompt Template**: Title generation now uses a robust template for reliable JSON output, improving chat organization and searchability.
- 🔗 **Support Jupyter Notebooks with Non-Root Base URLs**: Notebook-based code execution now supports non-root deployed Jupyter servers, granting full flexibility for hybrid or multi-user setups.
- 📰 **UI Scrollbar Always Visible for Overflow Tools**: When available tools overflow the display, the scrollbar is now always visible and there’s a handy "show all" toggle, making navigation of large toolsets snappier and more intuitive.
- 🛠️ **General Backend Refactoring for Stability**: Multiple under-the-hood improvements have been made across backend components, ensuring smoother performance, fewer errors, and a more reliable overall experience for all users.
- 🚀 **Optimized Web Search for Faster Results**: Web search speed and performance have been significantly enhanced, delivering answers and sources in record time to accelerate your research-heavy workflows.
- 💡 **More Supported Languages**: Expanded language support ensures an even wider range of users can enjoy an intuitive and natural interface in their native tongue.
### Fixed
- 🏃‍♂️ **Exhausting Workers in Nginx Reverse Proxy Due to Websocket Fix**: Websocket sessions are now fully compatible behind Nginx, eliminating worker exhaustion and restoring 24/7 reliability for real-time chats even in complex deployments.
- 🎤 **Audio Transcription Issue with OpenAI Resolved**: OpenAI-based audio transcription now handles WebM and newer formats without error, ensuring seamless voice-to-text workflows every time.
- 👉 **Message Input RTL Issue Fixed**: The chat message input now displays correctly for right-to-left languages, creating a flawless typing and reading experience for Arabic, Hebrew, and more.
- 🀄 **Katex: Proper Rendering of Chinese Characters Next to Math**: Math formulas now render perfectly even when directly adjacent to Chinese (CJK) characters, improving visual clarity for multilingual teams and cross-language documents.
- 🔂 **Duplicate Web Search URLs Eliminated**: Search results now reliably filter out URL duplicates, so your knowledge and search citations are always clean, trimmed, and easy to review.
- 📄 **Markdown Rendering Fixed in Knowledge Bases**: Markdown is now displayed correctly within knowledge bases, enabling better formatting and clarity of information-rich files.
- 🗂️ **LDAP Import/Loading Issue Resolved**: LDAP user imports process correctly, ensuring smooth onboarding and access without interruption.
- 🌎 **Pinecone Batch Operations and Async Safety**: All Pinecone operations (batch insert, upsert, delete) now run efficiently and safely in an async environment, boosting performance and preventing slowdowns in large-scale RAG jobs.
## [0.6.7] - 2025-05-07
### Added
- 🌐 **Custom Azure TTS API URL Support Added**: You can now define a custom Azure Text-to-Speech endpoint—enabling flexibility for enterprise deployments and regional compliance.
- ⚙️ **TOOL_SERVER_CONNECTIONS Environment Variable Suppor**: Easily configure and deploy tool servers via environment variables, streamlining setup and enabling faster enterprise provisioning.
- 👥 **Enhanced OAuth Group Handling as String or List**: OAuth group data can now be passed as either a list or a comma-separated string, improving compatibility with varied identity provider formats and reducing onboarding friction.
### Fixed
- 🧠 **Embedding with Ollama Proxy Endpoints Restored**: Fixed an issue where missing API config broke embedding for proxied Ollama models—ensuring consistent performance and compatibility.
- 🔐 **OIDC OAuth Login Issue Resolved**: Users can once again sign in seamlessly using OpenID Connect-based OAuth, eliminating login interruptions and improving reliability.
- 📝 **Notes Feature Access Fixed for Non-Admins**: Fixed an issue preventing non-admin users from accessing the Notes feature, restoring full cross-role collaboration capabilities.
- 🖼️ **Tika Loader Image Extraction Problem Resolved**: Ensured TikaLoader now processes 'extract_images' parameter correctly, restoring complete file extraction functionality in document workflows.
- 🎨 **Automatic1111 Image Model Setting Applied Properly**: Fixed an issue where switching to a specific image model via the UI wasn’t reflected in generation, re-enabling full visual creativity control.
- 🏷️ **Multiple XML Tags in Messages Now Parsed Correctly**: Fixed parsing issues when messages included multiple XML-style tags, ensuring clean and unbroken rendering of rich content in chats.
- 🖌️ **OpenAI Image Generation Issues Resolved**: Resolved broken image output when using OpenAI’s image generation, ensuring fully functional visual creation workflows.
- 🔎 **Tool Server Settings UI Privacy Restored**: Prevented restricted users from accessing tool server settings via search—restoring tight permissions control and safeguarding sensitive configurations.
- 🎧 **WebM Audio Transcription Now Supported**: Fixed an issue where WebM files failed during audio transcription—these formats are now fully supported, ensuring smoother voice note workflows and broader file compatibility.
## [0.6.6] - 2025-05-05
### Added
- 📝 **AI-Enhanced Notes (With Audio Transcription)**: Effortlessly create notes, attach meeting or voice audio, and let the AI instantly enhance, summarize, or refine your notes using audio transcriptions—making your documentation smarter, cleaner, and more insightful with minimal effort.
- 🔊 **Meeting Audio Recording & Import**: Seamlessly record audio from your meetings or capture screen audio and attach it to your notes—making it easier to revisit, annotate, and extract insights from important discussions.
- 📁 **Import Markdown Notes Effortlessly**: Bring your existing knowledge library into Open WebUI by importing your Markdown notes, so you can leverage all advanced note management and AI features right away.
- 👥 **Notes Permissions by User Group**: Fine-tune access and editing rights for notes based on user roles or groups, so you can delegate writing or restrict sensitive information as needed.
- ☁️ **OneDrive & SharePoint Integration**: Keep your content in sync by connecting notes and files directly with OneDrive or SharePoint—unlocking fast enterprise import/export and seamless collaboration with your existing workflows.
- 🗂️ **Paginated User List in Admin Panel**: Effortlessly manage and search through large teams via the new paginated user list—saving time and streamlining user administration in big organizations.
- 🕹️ **Granular Chat Share & Export Permissions**: Enjoy enhanced control over who can share or export chats, enabling tighter governance and privacy in team and enterprise settings.
- 🛑 **User Role Change Confirmation Dialog**: Reduce accidental privilege changes with a required confirmation step before updating user roles—improving security and preventing costly mistakes in team management.
- 🚨 **Audit Log for Failed Login Attempts**: Quickly detect unauthorized access attempts or troubleshoot user login problems with detailed logs of failed authentication right in the audit trail.
- 💡 **Dedicated 'Generate Title' Button for Chats**: Swiftly organize every conversation—tap the new button to let AI create relevant, clear titles for all your chats, saving time and reducing clutter.
- 💬 **Notification Sound Always-On Option**: Take control of your notifications by setting sound alerts to always play—helping you stay on top of important updates in busy environments.
- 🆔 **S3 File Tagging Support**: Uploaded files to S3 now include tags for better organization, searching, and integration with your file management policies.
- 🛡️ **OAuth Blocked Groups Support**: Gain more control over group-based access by explicitly blocking specified OAuth groups—ideal for complex identity or security requirements.
- 🚀 **Optimized Faster Web Search & Multi-Threaded Queries**: Enjoy dramatically faster web search and RAG (retrieval augmented generation) with revamped multi-threaded search—get richer, more accurate results in less time.
- 🔍 **All-Knowledge Parallel Search**: Searches across your entire knowledge base now happen in parallel even in non-hybrid mode, speeding up responses and improving knowledge accuracy for every question.
- 🌐 **New Firecrawl & Yacy Web Search Integrations**: Expand your world of information with two new advanced search engines—Firecrawl for deeper web insight and Yacy for decentralized, privacy-friendly search capabilities.
- 🧠 **Configurable Docling OCR Engine & Language**: Use environment variables to fine-tune Docling OCR engine and supported languages for smarter, more tailored document extraction and RAG workflows.
- 🗝️ **Enhanced Sentence Transformers Configuration**: Added new environment variables for easier set up and advanced customization of Sentence Transformers—ensuring best fit for your embedding needs.
- 🌲 **Pinecone Vector Database Integration**: Index, search, and manage knowledge at enterprise scale with full native support for Pinecone as your vector database—effortlessly handle even the biggest document sets.
- 🔄 **Automatic Requirements Installation for Tools & Functions**: Never worry about lost dependencies on restart—external function and tool requirements are now auto-installed at boot, ensuring tools always “just work.”
- 🔒 **Automatic Sign-Out on Token Expiry**: Security is smarter—users are now automatically logged out if their authentication token expires, protecting sensitive content and ensuring compliance without disruption.
- 🎬 **Automatic YouTube Embed Detection**: Paste YouTube links and see instant in-chat video embeds—no more manual embedding, making knowledge sharing and media consumption even easier for every team.
- 🔄 **Expanded Language & Locale Support**: Translations for Danish, French, Russian, Traditional Chinese, Simplified Chinese, Thai, Catalan, German, and Korean have been upgraded, offering smoother, more natural user experiences across the platform.
### Fixed
- 🔒 **Tighter HTML Token Security**: HTML rendering is now restricted to admin-uploaded tokens only, reducing any risk of XSS and keeping your data safe.
- 🔐 **Refined HTML Security and Token Handling**: Further hardened how HTML tokens and content are handled, guaranteeing even stronger resistance to security vulnerabilities and attacks.
- 🔏 **Correct Model Usage with Ollama Proxy Prefixes**: Enhanced model reference handling so proxied models in Ollama always download and run correctly—even when using custom prefixes.
- 📥 **Video File Upload Handling**: Prevented video files from being misclassified as text, fixing bugs with uploads and ensuring media files work as expected.
- 🔄 **No More Dependent WebSocket Sequential Delays**: Streamlined WebSocket operation to prevent delays and maintain snappy real-time collaboration, especially in multi-user environments.
- 🛠️ **More Robust Action Module Execution**: Multiple actions in a module now trigger as designed, increasing automation and scripting flexibility.
- 📧 **Notification Webhooks**: Ensured that notification webhooks are always sent for user events, even when the user isn’t currently active.
- 🗂️ **Smarter Knowledge Base Reindexing**: Knowledge reindexing continues even when corrupt or missing collections are encountered, keeping your search features running reliably.
- 🏷️ **User Import with Profile Images**: When importing users, their profile images now come along—making onboarding and collaboration visually clearer from day one.
- 💬 **OpenAI o-Series Universal Support**: All OpenAI o-series models are now seamlessly recognized and supported, unlocking more advanced capabilities and model choices for every workflow.
### Changed
- 📜 **Custom License Update & Contributor Agreement**: Open WebUI now operates under a custom license with Contributor License Agreement required by default—see https://docs.openwebui.com/license/ for details, ensuring sustainable open innovation for the community.
- 🔨 **CUDA Docker Images Updated to 12.8**: Upgraded CUDA image support for faster, more compatible model inference and futureproof GPU performance in your AI infrastructure.
- 🧱 **General Backend Refactoring for Reliability**: Continuous stability improvements streamline backend logic, reduce errors, and lay a stronger foundation for the next wave of feature releases—all under the hood for a more dependable WebUI.
## [0.6.5] - 2025-04-14
### Added
- 🛂 **Granular Voice Feature Permissions Per User Group**: Admins can now separately manage access to Speech-to-Text (record voice), Text-to-Speech (read aloud), and Tool Calls for each user group—giving teams tighter control over voice features and enhanced governance across roles.
- 🗣️ **Toggle Voice Activity Detection (VAD) for Whisper STT**: New environment variable lets you enable/disable VAD filtering with built-in Whisper speech-to-text, giving you flexibility to optimize for different audio quality and response accuracy levels.
- 📋 **Copy Formatted Response Mode**: You can now enable “Copy Formatted” in Settings > Interface to copy AI responses exactly as styled (with rich formatting, links, and structure preserved), making it faster and cleaner to paste into documents, emails, or reports.
- ⚙️ **Backend Stability and Performance Enhancements**: General backend refactoring improves system resilience, consistency, and overall reliability—offering smoother performance across workflows whether chatting, generating media, or using external tools.
- 🌎 **Translation Refinements Across Multiple Languages**: Updated translations deliver smoother language localization, clearer labels, and improved international usability throughout the UI—ensuring a better experience for non-English speakers.
### Fixed
- 🛠️ **LDAP Login Reliability Restored**: Resolved a critical issue where some LDAP setups failed due to attribute parsing—ensuring consistent, secure, and seamless user authentication across enterprise deployments.
- 🖼️ **Image Generation in Temporary Chats Now Works Properly**: Fixed a bug where image outputs weren’t generated during temporary chats—visual content can now be used reliably in all chat modes without interruptions.
## [0.6.4] - 2025-04-12
### Fixed
- 🛠️ **RAG_TEMPLATE Display Issue Resolved**: Fixed a formatting problem where the custom RAG_TEMPLATE wasn't correctly rendered in the interface—ensuring that custom retrieval prompts now appear exactly as intended for more reliable prompt engineering.
## [0.6.3] - 2025-04-12
### Added
- 🧪 **Auto-Artifact Detection Toggle**: Automatically detects artifacts in results—but now you can disable this behavior under advanced settings for full control.
- 🖼️ **Widescreen Mode for Shared Chats**: Shared link conversations now support widescreen layouts—perfect for presentations or easier review across wider displays.
- 🔁 **Reindex Knowledge Files on Demand**: Admins can now trigger reindexing of all knowledge files after changing embeddings—ensuring immediate alignment with new models for optimal RAG performance.
- 📄 **OpenAPI YAML Format Support**: External tools can now use YAML-format OpenAPI specs—making integration simpler for developers familiar with YAML-based configurations.
- 💬 **Message Content Copy Behavior**: Copy action now excludes 'details' tags—streamlining clipboard content when sharing or pasting summaries elsewhere.
- 🧭 **Sougou Web Search Integration**: New search engine option added—enhancing global relevance and diversity of search sources for multilingual users.
- 🧰 **Frontend Web Loader Engine Configuration**: Admins can now set preferred web loader engine for RAG workflows directly from the frontend—offering more control across setups.
- 👥 **Multi-Model Chat Permission Control**: Admins can manage access to multi-model chats per user group—allowing tighter governance in team environments.
- 🧱 **Persistent Configuration Can Be Disabled**: New environment variable lets advanced users and hosts turn off persistent configs—ideal for volatile or stateless deployments.
- 🧠 **Elixir Code Highlighting Support**: Elixir syntax is now beautifully rendered in code blocks—perfect for developers using this language in AI or automation projects.
- 🌐 **PWA External Manifest URL Support**: You can now define an external manifest.json—integrate Open WebUI seamlessly in managed or proxy-based PWA environments like Cloudflare Zero Trust.
- 🧪 **Azure AI Speech-to-Text Provider Integration**: Easily transcribe large audio files (up to 200MB) with high accuracy using Microsoft's Azure STT—fully configurable in Audio Settings.
- 🔏 **PKCE (Code Challenge Method) Support for OIDC**: Enhance your OIDC login security with Proof Key for Code Exchange—ideal for zero-trust and native client apps.
- ✨ **General UI/UX Enhancements**: Numerous refinements across layout, styling, and tool interactions—reducing visual noise and improving overall usability across key workflows.
- 🌍 **Translation Updates Across Multiple Languages**: Refined Catalan, Russian, Chinese (Simplified & Traditional), Hungarian, and Spanish translations for clearer navigation and instructions globally.
### Fixed
- 💥 **Chat Completion Error with Missing Models Resolved**: Fixed internal server error when referencing a model that doesn’t exist—ensuring graceful fallback and clear error guidance.
- 🔧 **Correct Knowledge Base Citations Restored**: Citations generated by RAG workflows now show accurate references—ensuring verifiability in outputs from sourced content.
- 🎙️ **Broken OGG/WebM Audio Upload Handling for OpenAI Fixed**: Uploading OGG or WebM files now converts properly to WAV before transcription—restoring accurate AI speech recognition workflows.
- 🔐 **Tool Server 'Session' Authentication Restored**: Previously broken session auth on external tool servers is now fully functional—ensuring secure and seamless access to connected tools.
- 🌐 **Folder-Based Chat Rename Now Updates Correctly**: Renaming chats in folders now reflects instantly everywhere—improving chat organization and clarity.
- 📜 **KaTeX Overflow Displays Fixed**: Math expressions now stay neatly within message bounds—preserving layout consistency even with long formulas.
- 🚫 **Stopping Ongoing Chat Fixed**: You can now return to an active (ongoing) chat and stop generation at any time—ensuring full control over sessions.
- 🔧 **TOOL_SERVERS / TOOL_SERVER_CONNECTIONS Indexing Issue Fixed**: Fixed a mismatch between tool lists and their access paths—restoring full function and preventing confusion in tool management.
- 🔐 **LDAP Login Handles Multiple Emails**: When LDAP returns multiple email attributes, the first valid one is now used—ensuring login success and account consistency.
- 🧩 **Model Visibility Toggle Fix**: Toggling model visibility now works even for untouched models—letting admins smoothly manage user access across base models.
- ⚙️ **Cross-Origin manifest.json Now Loads Properly**: Compatibility issues with Cloudflare Zero Trust (and others) resolved, allowing manifest.json to load behind authenticated proxies.
### Changed
- 🔒 **Default Access Scopes Set to Private for All Resources**: Models, tools, and knowledge are now private by default when created—ensuring better baseline security and visibility controls.
- 🧱 **General Backend Refactoring for Stability**: Numerous invisible improvements enhance backend scalability, security, and maintainability—powering upcoming features with a stronger foundation.
- 🧩 **Stable Dependency Upgrades**: Updated key platform libraries—Chromadb (0.6.3), pgvector (0.4.0), Azure Identity (1.21.0), and Youtube Transcript API (1.0.3)—for improved compatibility, functionality, and security.
## [0.6.2] - 2025-04-06
### Added
+3 -3
View File
@@ -2,13 +2,13 @@
## Our Pledge
As members, contributors, and leaders of this community, we pledge to make participation in our open-source project a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socioeconomic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
As members, contributors, and leaders of this community, we pledge to make participation in our project a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socioeconomic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
We are committed to creating and maintaining an open, respectful, and professional environment where positive contributions and meaningful discussions can flourish. By participating in this project, you agree to uphold these values and align your behavior to the standards outlined in this Code of Conduct.
## Why These Standards Are Important
Open-source projects rely on a community of volunteers dedicating their time, expertise, and effort toward a shared goal. These projects are inherently collaborative but also fragile, as the success of the project depends on the goodwill, energy, and productivity of those involved.
Projects rely on a community of volunteers dedicating their time, expertise, and effort toward a shared goal. These projects are inherently collaborative but also fragile, as the success of the project depends on the goodwill, energy, and productivity of those involved.
Maintaining a positive and respectful environment is essential to safeguarding the integrity of this project and protecting contributors' efforts. Behavior that disrupts this atmosphere—whether through hostility, entitlement, or unprofessional conduct—can severely harm the morale and productivity of the community. **Strict enforcement of these standards ensures a safe and supportive space for meaningful collaboration.**
@@ -79,7 +79,7 @@ This approach ensures that disruptive behaviors are addressed swiftly and decisi
## Why Zero Tolerance Is Necessary
Open-source projects thrive on collaboration, goodwill, and mutual respect. Toxic behaviors—such as entitlement, hostility, or persistent negativity—threaten not just individual contributors but the health of the project as a whole. Allowing such behaviors to persist robs contributors of their time, energy, and enthusiasm for the work they do.
Projects thrive on collaboration, goodwill, and mutual respect. Toxic behaviors—such as entitlement, hostility, or persistent negativity—threaten not just individual contributors but the health of the project as a whole. Allowing such behaviors to persist robs contributors of their time, energy, and enthusiasm for the work they do.
By enforcing a zero-tolerance policy, we ensure that the community remains a safe, welcoming space for all participants. These measures are not about harshness—they are about protecting contributors and fostering a productive environment where innovation can thrive.
+7
View File
@@ -0,0 +1,7 @@
# Open WebUI 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.
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.
**_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._**
-64
View File
@@ -1,64 +0,0 @@
# Run with
# caddy run --envfile ./example.env --config ./Caddyfile.localhost
#
# This is configured for
# - Automatic HTTPS (even for localhost)
# - Reverse Proxying to Ollama API Base URL (http://localhost:11434/api)
# - CORS
# - HTTP Basic Auth API Tokens (uncomment basicauth section)
# CORS Preflight (OPTIONS) + Request (GET, POST, PATCH, PUT, DELETE)
(cors-api) {
@match-cors-api-preflight method OPTIONS
handle @match-cors-api-preflight {
header {
Access-Control-Allow-Origin "{http.request.header.origin}"
Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS"
Access-Control-Allow-Headers "Origin, Accept, Authorization, Content-Type, X-Requested-With"
Access-Control-Allow-Credentials "true"
Access-Control-Max-Age "3600"
defer
}
respond "" 204
}
@match-cors-api-request {
not {
header Origin "{http.request.scheme}://{http.request.host}"
}
header Origin "{http.request.header.origin}"
}
handle @match-cors-api-request {
header {
Access-Control-Allow-Origin "{http.request.header.origin}"
Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS"
Access-Control-Allow-Headers "Origin, Accept, Authorization, Content-Type, X-Requested-With"
Access-Control-Allow-Credentials "true"
Access-Control-Max-Age "3600"
defer
}
}
}
# replace localhost with example.com or whatever
localhost {
## HTTP Basic Auth
## (uncomment to enable)
# basicauth {
# # see .example.env for how to generate tokens
# {env.OLLAMA_API_ID} {env.OLLAMA_API_TOKEN_DIGEST}
# }
handle /api/* {
# Comment to disable CORS
import cors-api
reverse_proxy localhost:11434
}
# Same-Origin Static Web Server
file_server {
root ./build/
}
}
+5 -2
View File
@@ -4,7 +4,7 @@
ARG USE_CUDA=false
ARG USE_OLLAMA=false
# Tested with cu117 for CUDA 11 and cu121 for CUDA 12 (default)
ARG USE_CUDA_VER=cu121
ARG USE_CUDA_VER=cu128
# any sentence transformer model; models to use can be found at https://huggingface.co/models?library=sentence-transformers
# Leaderboard: https://huggingface.co/spaces/mteb/leaderboard
# for better performance and multilangauge support use "intfloat/multilingual-e5-large" (~2.5GB) or "intfloat/multilingual-e5-base" (~1.5GB)
@@ -26,8 +26,11 @@ ARG BUILD_HASH
WORKDIR /app
# to store git revision in build
RUN apk add --no-cache git
COPY package.json package-lock.json ./
RUN npm ci
RUN npm ci --force
COPY . .
ENV APP_BUILD_HASH=${BUILD_HASH}
+7 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2023-2025 Timothy Jaeryang Baek
Copyright (c) 2023-2025 Timothy Jaeryang Baek (Open WebUI)
All rights reserved.
Redistribution and use in source and binary forms, with or without
@@ -15,6 +15,12 @@ modification, are permitted provided that the following conditions are met:
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
4. Notwithstanding any other provision of this License, and as a material condition of the rights granted herein, licensees are strictly prohibited from altering, removing, obscuring, or replacing any "Open WebUI" branding, including but not limited to the name, logo, or any visual, textual, or symbolic identifiers that distinguish the software and its interfaces, in any deployment or distribution, regardless of the number of users, except as explicitly set forth in Clauses 5 and 6 below.
5. The branding restriction enumerated in Clause 4 shall not apply in the following limited circumstances: (i) deployments or distributions where the total number of end users (defined as individual natural persons with direct access to the application) does not exceed fifty (50) within any rolling thirty (30) day period; (ii) cases in which the licensee is an official contributor to the codebase—with a substantive code change successfully merged into the main branch of the official codebase maintained by the copyright holder—who has obtained specific prior written permission for branding adjustment from the copyright holder; or (iii) where the licensee has obtained a duly executed enterprise license expressly permitting such modification. For all other cases, any removal or alteration of the "Open WebUI" branding shall constitute a material breach of license.
6. All code, modifications, or derivative works incorporated into this project prior to the incorporation of this branding clause remain licensed under the BSD 3-Clause License, and prior contributors retain all BSD-3 rights therein; if any such contributor requests the removal of their BSD-3-licensed code, the copyright holder will do so, and any replacement code will be licensed under the project's primary license then in effect. By contributing after this clause's adoption, you agree to the project's Contributor License Agreement (CLA) and to these updated terms for all new contributions.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+53
View File
@@ -0,0 +1,53 @@
All code and materials created before commit `60d84a3aae9802339705826e9095e272e3c83623` are subject to the following copyright and license:
Copyright (c) 2023-2025 Timothy Jaeryang Baek
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
All code and materials created before commit `a76068d69cd59568b920dfab85dc573dbbb8f131` are subject to the following copyright and license:
MIT License
Copyright (c) 2023 Timothy Jaeryang Baek
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+34 -4
View File
@@ -7,12 +7,13 @@
![GitHub language count](https://img.shields.io/github/languages/count/open-webui/open-webui)
![GitHub top language](https://img.shields.io/github/languages/top/open-webui/open-webui)
![GitHub last commit](https://img.shields.io/github/last-commit/open-webui/open-webui?color=red)
![Hits](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2Follama-webui%2Follama-wbui&count_bg=%2379C83D&title_bg=%23555555&icon=&icon_color=%23E7E7E7&title=hits&edge_flat=false)
[![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**.
Passionate about open-source AI? [Join our team →](https://careers.openwebui.com/)
![Open WebUI Demo](./demo.gif)
> [!TIP]
@@ -62,9 +63,36 @@ 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!
## 🔗 Also Check Out Open WebUI Community!
## Sponsors 🙌
Don't forget to explore our sibling project, [Open WebUI Community](https://openwebui.com/), where you can discover, download, and explore customized Modelfiles. Open WebUI Community offers a wide range of exciting possibilities for enhancing your chat interactions with Open WebUI! 🚀
#### 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>
</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!
## How to Install 🚀
@@ -155,6 +183,8 @@ 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.
### Troubleshooting
Encountering connection issues? Our [Open WebUI Documentation](https://docs.openwebui.com/troubleshooting/) has got you covered. For further assistance and to join our vibrant community, visit the [Open WebUI Discord](https://discord.gg/5rJgQTnV4s).
@@ -206,7 +236,7 @@ Discover upcoming features on our roadmap in the [Open WebUI Documentation](http
## License 📜
This project is licensed under the [BSD-3-Clause License](LICENSE) - see the [LICENSE](LICENSE) file for details. 📄
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. 📄
## Support 💬
+2 -1
View File
@@ -1,2 +1,3 @@
export CORS_ALLOW_ORIGIN=http://localhost:5173/
PORT="${PORT:-8080}"
uvicorn open_webui.main:app --port $PORT --host 0.0.0.0 --forwarded-allow-ips '*' --reload
uvicorn open_webui.main:app --port $PORT --host 0.0.0.0 --forwarded-allow-ips '*' --reload
+8 -1
View File
@@ -73,8 +73,15 @@ def serve(
os.environ["LD_LIBRARY_PATH"] = ":".join(LD_LIBRARY_PATH)
import open_webui.main # we need set environment variables before importing main
from open_webui.env import UVICORN_WORKERS # Import the workers setting
uvicorn.run(open_webui.main.app, host=host, port=port, forwarded_allow_ips="*")
uvicorn.run(
"open_webui.main:app",
host=host,
port=port,
forwarded_allow_ips="*",
workers=UVICORN_WORKERS,
)
@app.command()
File diff suppressed because it is too large Load Diff
+2
View File
@@ -31,6 +31,7 @@ class ERROR_MESSAGES(str, Enum):
USERNAME_TAKEN = (
"Uh-oh! This username is already registered. Please choose another username."
)
PASSWORD_TOO_LONG = "Uh-oh! The password you entered is too long. Please make sure your password is less than 72 bytes long."
COMMAND_TAKEN = "Uh-oh! This command is already registered. Please choose another command string."
FILE_EXISTS = "Uh-oh! This file is already registered. Please choose another file."
@@ -110,6 +111,7 @@ class TASKS(str, Enum):
DEFAULT = lambda task="": f"{task if task else 'generation'}"
TITLE_GENERATION = "title_generation"
FOLLOW_UP_GENERATION = "follow_up_generation"
TAGS_GENERATION = "tags_generation"
EMOJI_GENERATION = "emoji_generation"
QUERY_GENERATION = "query_generation"
+191 -6
View File
@@ -5,6 +5,7 @@ import os
import pkgutil
import sys
import shutil
from uuid import uuid4
from pathlib import Path
import markdown
@@ -130,6 +131,7 @@ else:
PACKAGE_DATA = {"version": "0.0.0"}
VERSION = PACKAGE_DATA["version"]
INSTANCE_ID = os.environ.get("INSTANCE_ID", str(uuid4()))
# Function to parse each section
@@ -197,6 +199,7 @@ CHANGELOG = changelog_json
SAFE_MODE = os.environ.get("SAFE_MODE", "false").lower() == "true"
####################################
# ENABLE_FORWARD_USER_INFO_HEADERS
####################################
@@ -264,21 +267,40 @@ else:
DATABASE_URL = os.environ.get("DATABASE_URL", f"sqlite:///{DATA_DIR}/webui.db")
DATABASE_TYPE = os.environ.get("DATABASE_TYPE")
DATABASE_USER = os.environ.get("DATABASE_USER")
DATABASE_PASSWORD = os.environ.get("DATABASE_PASSWORD")
DATABASE_CRED = ""
if DATABASE_USER:
DATABASE_CRED += f"{DATABASE_USER}"
if DATABASE_PASSWORD:
DATABASE_CRED += f":{DATABASE_PASSWORD}"
DB_VARS = {
"db_type": DATABASE_TYPE,
"db_cred": DATABASE_CRED,
"db_host": os.environ.get("DATABASE_HOST"),
"db_port": os.environ.get("DATABASE_PORT"),
"db_name": os.environ.get("DATABASE_NAME"),
}
if all(DB_VARS.values()):
DATABASE_URL = f"{DB_VARS['db_type']}://{DB_VARS['db_cred']}@{DB_VARS['db_host']}:{DB_VARS['db_port']}/{DB_VARS['db_name']}"
# Replace the postgres:// with postgresql://
if "postgres://" in DATABASE_URL:
DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql://")
DATABASE_SCHEMA = os.environ.get("DATABASE_SCHEMA", None)
DATABASE_POOL_SIZE = os.environ.get("DATABASE_POOL_SIZE", 0)
DATABASE_POOL_SIZE = os.environ.get("DATABASE_POOL_SIZE", None)
if DATABASE_POOL_SIZE == "":
DATABASE_POOL_SIZE = 0
else:
if DATABASE_POOL_SIZE != None:
try:
DATABASE_POOL_SIZE = int(DATABASE_POOL_SIZE)
except Exception:
DATABASE_POOL_SIZE = 0
DATABASE_POOL_SIZE = None
DATABASE_POOL_MAX_OVERFLOW = os.environ.get("DATABASE_POOL_MAX_OVERFLOW", 0)
@@ -323,9 +345,33 @@ ENABLE_REALTIME_CHAT_SAVE = (
####################################
REDIS_URL = os.environ.get("REDIS_URL", "")
REDIS_KEY_PREFIX = os.environ.get("REDIS_KEY_PREFIX", "open-webui")
REDIS_SENTINEL_HOSTS = os.environ.get("REDIS_SENTINEL_HOSTS", "")
REDIS_SENTINEL_PORT = os.environ.get("REDIS_SENTINEL_PORT", "26379")
# Maximum number of retries for Redis operations when using Sentinel fail-over
REDIS_SENTINEL_MAX_RETRY_COUNT = os.environ.get("REDIS_SENTINEL_MAX_RETRY_COUNT", "2")
try:
REDIS_SENTINEL_MAX_RETRY_COUNT = int(REDIS_SENTINEL_MAX_RETRY_COUNT)
if REDIS_SENTINEL_MAX_RETRY_COUNT < 1:
REDIS_SENTINEL_MAX_RETRY_COUNT = 2
except ValueError:
REDIS_SENTINEL_MAX_RETRY_COUNT = 2
####################################
# UVICORN WORKERS
####################################
# Number of uvicorn worker processes for handling requests
UVICORN_WORKERS = os.environ.get("UVICORN_WORKERS", "1")
try:
UVICORN_WORKERS = int(UVICORN_WORKERS)
if UVICORN_WORKERS < 1:
UVICORN_WORKERS = 1
except ValueError:
UVICORN_WORKERS = 1
log.info(f"Invalid UVICORN_WORKERS value, defaulting to {UVICORN_WORKERS}")
####################################
# WEBUI_AUTH (Required for security)
####################################
@@ -335,11 +381,19 @@ WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get(
"WEBUI_AUTH_TRUSTED_EMAIL_HEADER", None
)
WEBUI_AUTH_TRUSTED_NAME_HEADER = os.environ.get("WEBUI_AUTH_TRUSTED_NAME_HEADER", None)
WEBUI_AUTH_TRUSTED_GROUPS_HEADER = os.environ.get(
"WEBUI_AUTH_TRUSTED_GROUPS_HEADER", None
)
BYPASS_MODEL_ACCESS_CONTROL = (
os.environ.get("BYPASS_MODEL_ACCESS_CONTROL", "False").lower() == "true"
)
WEBUI_AUTH_SIGNOUT_REDIRECT_URL = os.environ.get(
"WEBUI_AUTH_SIGNOUT_REDIRECT_URL", None
)
####################################
# WEBUI_SECRET_KEY
####################################
@@ -372,14 +426,43 @@ WEBUI_AUTH_COOKIE_SECURE = (
if WEBUI_AUTH and WEBUI_SECRET_KEY == "":
raise ValueError(ERROR_MESSAGES.ENV_VAR_NOT_FOUND)
ENABLE_COMPRESSION_MIDDLEWARE = (
os.environ.get("ENABLE_COMPRESSION_MIDDLEWARE", "True").lower() == "true"
)
####################################
# MODELS
####################################
MODELS_CACHE_TTL = os.environ.get("MODELS_CACHE_TTL", "1")
if MODELS_CACHE_TTL == "":
MODELS_CACHE_TTL = None
else:
try:
MODELS_CACHE_TTL = int(MODELS_CACHE_TTL)
except Exception:
MODELS_CACHE_TTL = 1
####################################
# WEBSOCKET SUPPORT
####################################
ENABLE_WEBSOCKET_SUPPORT = (
os.environ.get("ENABLE_WEBSOCKET_SUPPORT", "True").lower() == "true"
)
WEBSOCKET_MANAGER = os.environ.get("WEBSOCKET_MANAGER", "")
WEBSOCKET_REDIS_URL = os.environ.get("WEBSOCKET_REDIS_URL", REDIS_URL)
WEBSOCKET_REDIS_LOCK_TIMEOUT = os.environ.get("WEBSOCKET_REDIS_LOCK_TIMEOUT", 60)
websocket_redis_lock_timeout = os.environ.get("WEBSOCKET_REDIS_LOCK_TIMEOUT", "60")
try:
WEBSOCKET_REDIS_LOCK_TIMEOUT = int(websocket_redis_lock_timeout)
except ValueError:
WEBSOCKET_REDIS_LOCK_TIMEOUT = 60
WEBSOCKET_SENTINEL_HOSTS = os.environ.get("WEBSOCKET_SENTINEL_HOSTS", "")
@@ -395,6 +478,11 @@ else:
except Exception:
AIOHTTP_CLIENT_TIMEOUT = 300
AIOHTTP_CLIENT_SESSION_SSL = (
os.environ.get("AIOHTTP_CLIENT_SESSION_SSL", "True").lower() == "true"
)
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = os.environ.get(
"AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST",
os.environ.get("AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST", "10"),
@@ -408,14 +496,83 @@ else:
except Exception:
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = 10
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = os.environ.get(
"AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA", "10"
)
if AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA == "":
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = None
else:
try:
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = int(
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA
)
except Exception:
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = 10
AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL = (
os.environ.get("AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL", "True").lower() == "true"
)
####################################
# SENTENCE TRANSFORMERS
####################################
SENTENCE_TRANSFORMERS_BACKEND = os.environ.get("SENTENCE_TRANSFORMERS_BACKEND", "")
if SENTENCE_TRANSFORMERS_BACKEND == "":
SENTENCE_TRANSFORMERS_BACKEND = "torch"
SENTENCE_TRANSFORMERS_MODEL_KWARGS = os.environ.get(
"SENTENCE_TRANSFORMERS_MODEL_KWARGS", ""
)
if SENTENCE_TRANSFORMERS_MODEL_KWARGS == "":
SENTENCE_TRANSFORMERS_MODEL_KWARGS = None
else:
try:
SENTENCE_TRANSFORMERS_MODEL_KWARGS = json.loads(
SENTENCE_TRANSFORMERS_MODEL_KWARGS
)
except Exception:
SENTENCE_TRANSFORMERS_MODEL_KWARGS = None
SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND = os.environ.get(
"SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND", ""
)
if SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND == "":
SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND = "torch"
SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = os.environ.get(
"SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS", ""
)
if SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS == "":
SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = None
else:
try:
SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = json.loads(
SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS
)
except Exception:
SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = None
####################################
# OFFLINE_MODE
####################################
ENABLE_VERSION_UPDATE_CHECK = (
os.environ.get("ENABLE_VERSION_UPDATE_CHECK", "true").lower() == "true"
)
OFFLINE_MODE = os.environ.get("OFFLINE_MODE", "false").lower() == "true"
if OFFLINE_MODE:
os.environ["HF_HUB_OFFLINE"] = "1"
ENABLE_VERSION_UPDATE_CHECK = False
####################################
# AUDIT LOGGING
@@ -424,6 +581,14 @@ if OFFLINE_MODE:
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")
# Comma separated list of logger names to use for audit logging
# Default is "uvicorn.access" which is the access log for Uvicorn
# You can add more logger names to this list if you want to capture more logs
AUDIT_UVICORN_LOGGER_NAMES = os.getenv(
"AUDIT_UVICORN_LOGGER_NAMES", "uvicorn.access"
).split(",")
# METADATA | REQUEST | REQUEST_RESPONSE
AUDIT_LOG_LEVEL = os.getenv("AUDIT_LOG_LEVEL", "NONE").upper()
try:
@@ -438,14 +603,19 @@ AUDIT_EXCLUDED_PATHS = os.getenv("AUDIT_EXCLUDED_PATHS", "/chats,/chat,/folders"
AUDIT_EXCLUDED_PATHS = [path.strip() for path in AUDIT_EXCLUDED_PATHS]
AUDIT_EXCLUDED_PATHS = [path.lstrip("/") for path in AUDIT_EXCLUDED_PATHS]
####################################
# OPENTELEMETRY
####################################
ENABLE_OTEL = os.environ.get("ENABLE_OTEL", "False").lower() == "true"
ENABLE_OTEL_METRICS = os.environ.get("ENABLE_OTEL_METRICS", "False").lower() == "true"
OTEL_EXPORTER_OTLP_ENDPOINT = os.environ.get(
"OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317"
)
OTEL_EXPORTER_OTLP_INSECURE = (
os.environ.get("OTEL_EXPORTER_OTLP_INSECURE", "False").lower() == "true"
)
OTEL_SERVICE_NAME = os.environ.get("OTEL_SERVICE_NAME", "open-webui")
OTEL_RESOURCE_ATTRIBUTES = os.environ.get(
"OTEL_RESOURCE_ATTRIBUTES", ""
@@ -453,6 +623,14 @@ OTEL_RESOURCE_ATTRIBUTES = os.environ.get(
OTEL_TRACES_SAMPLER = os.environ.get(
"OTEL_TRACES_SAMPLER", "parentbased_always_on"
).lower()
OTEL_BASIC_AUTH_USERNAME = os.environ.get("OTEL_BASIC_AUTH_USERNAME", "")
OTEL_BASIC_AUTH_PASSWORD = os.environ.get("OTEL_BASIC_AUTH_PASSWORD", "")
OTEL_OTLP_SPAN_EXPORTER = os.environ.get(
"OTEL_OTLP_SPAN_EXPORTER", "grpc"
).lower() # grpc or http
####################################
# TOOLS/FUNCTIONS PIP OPTIONS
@@ -460,3 +638,10 @@ OTEL_TRACES_SAMPLER = os.environ.get(
PIP_OPTIONS = os.getenv("PIP_OPTIONS", "").split()
PIP_PACKAGE_INDEX_OPTIONS = os.getenv("PIP_PACKAGE_INDEX_OPTIONS", "").split()
####################################
# PROGRESSIVE WEB APP OPTIONS
####################################
EXTERNAL_PWA_MANIFEST_URL = os.environ.get("EXTERNAL_PWA_MANIFEST_URL")
+14 -15
View File
@@ -25,10 +25,14 @@ from open_webui.socket.main import (
)
from open_webui.models.users import UserModel
from open_webui.models.functions import Functions
from open_webui.models.models import Models
from open_webui.utils.plugin import load_function_module_by_id
from open_webui.utils.plugin import (
load_function_module_by_id,
get_function_module_from_cache,
)
from open_webui.utils.tools import get_tools
from open_webui.utils.access_control import has_access
@@ -53,12 +57,7 @@ log.setLevel(SRC_LOG_LEVELS["MAIN"])
def get_function_module_by_id(request: Request, pipe_id: str):
# Check if function is already loaded
if pipe_id not in request.app.state.FUNCTIONS:
function_module, _, _ = load_function_module_by_id(pipe_id)
request.app.state.FUNCTIONS[pipe_id] = function_module
else:
function_module = request.app.state.FUNCTIONS[pipe_id]
function_module, _, _ = get_function_module_from_cache(request, pipe_id)
if hasattr(function_module, "valves") and hasattr(function_module, "Valves"):
valves = Functions.get_function_valves_by_id(pipe_id)
@@ -229,12 +228,7 @@ async def generate_function_chat_completion(
"__task__": __task__,
"__task_body__": __task_body__,
"__files__": files,
"__user__": {
"id": user.id,
"email": user.email,
"name": user.name,
"role": user.role,
},
"__user__": user.model_dump() if isinstance(user, UserModel) else {},
"__metadata__": metadata,
"__request__": request,
}
@@ -255,8 +249,13 @@ async def generate_function_chat_completion(
form_data["model"] = model_info.base_model_id
params = model_info.params.model_dump()
form_data = apply_model_params_to_body_openai(params, form_data)
form_data = apply_model_system_prompt_to_body(params, form_data, metadata, user)
if params:
system = params.pop("system", None)
form_data = apply_model_params_to_body_openai(params, form_data)
form_data = apply_model_system_prompt_to_body(
system, form_data, metadata, user
)
pipe_id = get_pipe_id(form_data)
function_module = get_function_module_by_id(request, pipe_id)
+19 -13
View File
@@ -62,6 +62,9 @@ def handle_peewee_migration(DATABASE_URL):
except Exception as e:
log.error(f"Failed to initialize the database connection: {e}")
log.warning(
"Hint: If your database password contains special characters, you may need to URL-encode it."
)
raise
finally:
# Properly closing the database connection
@@ -81,20 +84,23 @@ if "sqlite" in SQLALCHEMY_DATABASE_URL:
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
else:
if DATABASE_POOL_SIZE > 0:
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
pool_size=DATABASE_POOL_SIZE,
max_overflow=DATABASE_POOL_MAX_OVERFLOW,
pool_timeout=DATABASE_POOL_TIMEOUT,
pool_recycle=DATABASE_POOL_RECYCLE,
pool_pre_ping=True,
poolclass=QueuePool,
)
if isinstance(DATABASE_POOL_SIZE, int):
if DATABASE_POOL_SIZE > 0:
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
pool_size=DATABASE_POOL_SIZE,
max_overflow=DATABASE_POOL_MAX_OVERFLOW,
pool_timeout=DATABASE_POOL_TIMEOUT,
pool_recycle=DATABASE_POOL_RECYCLE,
pool_pre_ping=True,
poolclass=QueuePool,
)
else:
engine = create_engine(
SQLALCHEMY_DATABASE_URL, pool_pre_ping=True, poolclass=NullPool
)
else:
engine = create_engine(
SQLALCHEMY_DATABASE_URL, pool_pre_ping=True, poolclass=NullPool
)
engine = create_engine(SQLALCHEMY_DATABASE_URL, pool_pre_ping=True)
SessionLocal = sessionmaker(
+2 -2
View File
@@ -43,7 +43,7 @@ class ReconnectingPostgresqlDatabase(CustomReconnectMixin, PostgresqlDatabase):
def register_connection(db_url):
db = connect(db_url, unquote_password=True)
db = connect(db_url, unquote_user=True, unquote_password=True)
if isinstance(db, PostgresqlDatabase):
# Enable autoconnect for SQLite databases, managed by Peewee
db.autoconnect = True
@@ -51,7 +51,7 @@ def register_connection(db_url):
log.info("Connected to PostgreSQL database")
# Get the connection details
connection = parse(db_url, unquote_password=True)
connection = parse(db_url, unquote_user=True, unquote_password=True)
# Use our custom database class that supports reconnection
db = ReconnectingPostgresqlDatabase(**connection)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,33 @@
"""Add note table
Revision ID: 9f0c9cd09105
Revises: 3781e22d8b01
Create Date: 2025-05-03 03:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
revision = "9f0c9cd09105"
down_revision = "3781e22d8b01"
branch_labels = None
depends_on = None
def upgrade():
op.create_table(
"note",
sa.Column("id", sa.Text(), nullable=False, primary_key=True, unique=True),
sa.Column("user_id", sa.Text(), nullable=True),
sa.Column("title", sa.Text(), nullable=True),
sa.Column("data", sa.JSON(), nullable=True),
sa.Column("meta", sa.JSON(), nullable=True),
sa.Column("access_control", sa.JSON(), nullable=True),
sa.Column("created_at", sa.BigInteger(), nullable=True),
sa.Column("updated_at", sa.BigInteger(), nullable=True),
)
def downgrade():
op.drop_table("note")
@@ -0,0 +1,23 @@
"""Update folder table data
Revision ID: d31026856c01
Revises: 9f0c9cd09105
Create Date: 2025-07-13 03:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
revision = "d31026856c01"
down_revision = "9f0c9cd09105"
branch_labels = None
depends_on = None
def upgrade():
op.add_column("folder", sa.Column("data", sa.JSON(), nullable=True))
def downgrade():
op.drop_column("folder", "data")
+8 -4
View File
@@ -129,12 +129,16 @@ class AuthsTable:
def authenticate_user(self, email: str, password: str) -> Optional[UserModel]:
log.info(f"authenticate_user: {email}")
user = Users.get_user_by_email(email)
if not user:
return None
try:
with get_db() as db:
auth = db.query(Auth).filter_by(email=email, active=True).first()
auth = db.query(Auth).filter_by(id=user.id, active=True).first()
if auth:
if verify_password(password, auth.password):
user = Users.get_user_by_id(auth.id)
return user
else:
return None
@@ -155,8 +159,8 @@ class AuthsTable:
except Exception:
return False
def authenticate_user_by_trusted_header(self, email: str) -> Optional[UserModel]:
log.info(f"authenticate_user_by_trusted_header: {email}")
def authenticate_user_by_email(self, email: str) -> 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()
+97 -43
View File
@@ -12,6 +12,7 @@ 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 sqlalchemy.sql.expression import bindparam
####################
# Chat DB Schema
@@ -66,12 +67,14 @@ class ChatModel(BaseModel):
class ChatForm(BaseModel):
chat: dict
folder_id: Optional[str] = None
class ChatImportForm(ChatForm):
meta: Optional[dict] = {}
pinned: Optional[bool] = False
folder_id: Optional[str] = None
created_at: Optional[int] = None
updated_at: Optional[int] = None
class ChatTitleMessagesForm(BaseModel):
@@ -118,6 +121,7 @@ class ChatTable:
else "New Chat"
),
"chat": form_data.chat,
"folder_id": form_data.folder_id,
"created_at": int(time.time()),
"updated_at": int(time.time()),
}
@@ -147,8 +151,16 @@ class ChatTable:
"meta": form_data.meta,
"pinned": form_data.pinned,
"folder_id": form_data.folder_id,
"created_at": int(time.time()),
"updated_at": int(time.time()),
"created_at": (
form_data.created_at
if form_data.created_at
else int(time.time())
),
"updated_at": (
form_data.updated_at
if form_data.updated_at
else int(time.time())
),
}
)
@@ -232,6 +244,10 @@ class ChatTable:
if chat is None:
return None
# Sanitize message content for null characters before upserting
if isinstance(message.get("content"), str):
message["content"] = message["content"].replace("\x00", "")
chat = chat.chat
history = chat.get("history", {})
@@ -377,22 +393,47 @@ class ChatTable:
return False
def get_archived_chat_list_by_user_id(
self, user_id: str, skip: int = 0, limit: int = 50
self,
user_id: str,
filter: Optional[dict] = None,
skip: int = 0,
limit: int = 50,
) -> list[ChatModel]:
with get_db() as db:
all_chats = (
db.query(Chat)
.filter_by(user_id=user_id, archived=True)
.order_by(Chat.updated_at.desc())
# .limit(limit).offset(skip)
.all()
)
query = db.query(Chat).filter_by(user_id=user_id, archived=True)
if filter:
query_key = filter.get("query")
if query_key:
query = query.filter(Chat.title.ilike(f"%{query_key}%"))
order_by = filter.get("order_by")
direction = filter.get("direction")
if order_by and direction and getattr(Chat, order_by):
if direction.lower() == "asc":
query = query.order_by(getattr(Chat, order_by).asc())
elif direction.lower() == "desc":
query = query.order_by(getattr(Chat, order_by).desc())
else:
raise ValueError("Invalid direction for ordering")
else:
query = query.order_by(Chat.updated_at.desc())
if skip:
query = query.offset(skip)
if limit:
query = query.limit(limit)
all_chats = query.all()
return [ChatModel.model_validate(chat) for chat in all_chats]
def get_chat_list_by_user_id(
self,
user_id: str,
include_archived: bool = False,
filter: Optional[dict] = None,
skip: int = 0,
limit: int = 50,
) -> list[ChatModel]:
@@ -401,7 +442,23 @@ class ChatTable:
if not include_archived:
query = query.filter_by(archived=False)
query = query.order_by(Chat.updated_at.desc())
if filter:
query_key = filter.get("query")
if query_key:
query = query.filter(Chat.title.ilike(f"%{query_key}%"))
order_by = filter.get("order_by")
direction = filter.get("direction")
if order_by and direction and getattr(Chat, order_by):
if direction.lower() == "asc":
query = query.order_by(getattr(Chat, order_by).asc())
elif direction.lower() == "desc":
query = query.order_by(getattr(Chat, order_by).desc())
else:
raise ValueError("Invalid direction for ordering")
else:
query = query.order_by(Chat.updated_at.desc())
if skip:
query = query.offset(skip)
@@ -436,7 +493,7 @@ class ChatTable:
all_chats = query.all()
# result has to be destrctured from sqlalchemy `row` and mapped to a dict since the `ChatModel`is not the returned dataclass.
# result has to be destructured from sqlalchemy `row` and mapped to a dict since the `ChatModel`is not the returned dataclass.
return [
ChatTitleIdResponse.model_validate(
{
@@ -539,10 +596,12 @@ class ChatTable:
"""
Filters chats based on a search query using Python, allowing pagination using skip and limit.
"""
search_text = search_text.lower().strip()
search_text = search_text.replace("\u0000", "").lower().strip()
if not search_text:
return self.get_chat_list_by_user_id(user_id, include_archived, skip, limit)
return self.get_chat_list_by_user_id(
user_id, include_archived, filter={}, skip=skip, limit=limit
)
search_text_words = search_text.split(" ")
@@ -571,21 +630,18 @@ class ChatTable:
dialect_name = db.bind.dialect.name
if dialect_name == "sqlite":
# SQLite case: using JSON1 extension for JSON searching
sqlite_content_sql = (
"EXISTS ("
" SELECT 1 "
" FROM json_each(Chat.chat, '$.messages') AS message "
" WHERE LOWER(message.value->>'content') LIKE '%' || :content_key || '%'"
")"
)
sqlite_content_clause = text(sqlite_content_sql)
query = query.filter(
(
Chat.title.ilike(
f"%{search_text}%"
) # Case-insensitive search in title
| text(
"""
EXISTS (
SELECT 1
FROM json_each(Chat.chat, '$.messages') AS message
WHERE LOWER(message.value->>'content') LIKE '%' || :search_text || '%'
)
"""
)
).params(search_text=search_text)
or_(
Chat.title.ilike(bindparam("title_key")), sqlite_content_clause
).params(title_key=f"%{search_text}%", content_key=search_text)
)
# Check if there are any tags to filter, it should have all the tags
@@ -620,21 +676,19 @@ class ChatTable:
elif dialect_name == "postgresql":
# PostgreSQL relies on proper JSON query for search
postgres_content_sql = (
"EXISTS ("
" SELECT 1 "
" FROM json_array_elements(Chat.chat->'messages') AS message "
" WHERE LOWER(message->>'content') LIKE '%' || :content_key || '%'"
")"
)
postgres_content_clause = text(postgres_content_sql)
query = query.filter(
(
Chat.title.ilike(
f"%{search_text}%"
) # Case-insensitive search in title
| text(
"""
EXISTS (
SELECT 1
FROM json_array_elements(Chat.chat->'messages') AS message
WHERE LOWER(message->>'content') LIKE '%' || :search_text || '%'
)
"""
)
).params(search_text=search_text)
or_(
Chat.title.ilike(bindparam("title_key")),
postgres_content_clause,
).params(title_key=f"%{search_text}%", content_key=search_text)
)
# Check if there are any tags to filter, it should have all the tags
+22 -7
View File
@@ -29,6 +29,7 @@ class Folder(Base):
name = Column(Text)
items = Column(JSON, nullable=True)
meta = Column(JSON, nullable=True)
data = Column(JSON, nullable=True)
is_expanded = Column(Boolean, default=False)
created_at = Column(BigInteger)
updated_at = Column(BigInteger)
@@ -41,6 +42,7 @@ class FolderModel(BaseModel):
name: str
items: Optional[dict] = None
meta: Optional[dict] = None
data: Optional[dict] = None
is_expanded: bool = False
created_at: int
updated_at: int
@@ -55,12 +57,13 @@ class FolderModel(BaseModel):
class FolderForm(BaseModel):
name: str
data: Optional[dict] = None
model_config = ConfigDict(extra="allow")
class FolderTable:
def insert_new_folder(
self, user_id: str, name: str, parent_id: Optional[str] = None
self, user_id: str, form_data: FolderForm, parent_id: Optional[str] = None
) -> Optional[FolderModel]:
with get_db() as db:
id = str(uuid.uuid4())
@@ -68,7 +71,7 @@ class FolderTable:
**{
"id": id,
"user_id": user_id,
"name": name,
**(form_data.model_dump(exclude_unset=True) or {}),
"parent_id": parent_id,
"created_at": int(time.time()),
"updated_at": int(time.time()),
@@ -187,8 +190,8 @@ class FolderTable:
log.error(f"update_folder: {e}")
return
def update_folder_name_by_id_and_user_id(
self, id: str, user_id: str, name: str
def update_folder_by_id_and_user_id(
self, id: str, user_id: str, form_data: FolderForm
) -> Optional[FolderModel]:
try:
with get_db() as db:
@@ -197,16 +200,28 @@ class FolderTable:
if not folder:
return None
form_data = form_data.model_dump(exclude_unset=True)
existing_folder = (
db.query(Folder)
.filter_by(name=name, parent_id=folder.parent_id, user_id=user_id)
.filter_by(
name=form_data.get("name"),
parent_id=folder.parent_id,
user_id=user_id,
)
.first()
)
if existing_folder:
if existing_folder and existing_folder.id != id:
return None
folder.name = name
folder.name = form_data.get("name", folder.name)
if "data" in form_data:
folder.data = {
**(folder.data or {}),
**form_data["data"],
}
folder.updated_at = int(time.time())
db.commit()
+48
View File
@@ -108,6 +108,54 @@ class FunctionsTable:
log.exception(f"Error creating a new function: {e}")
return None
def sync_functions(
self, user_id: str, functions: list[FunctionModel]
) -> list[FunctionModel]:
# 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:
# Get existing functions
existing_functions = db.query(Function).all()
existing_ids = {func.id for func in existing_functions}
# Prepare a set of new function IDs
new_function_ids = {func.id for func in functions}
# Update or insert functions
for func in functions:
if func.id in existing_ids:
db.query(Function).filter_by(id=func.id).update(
{
**func.model_dump(),
"user_id": user_id,
"updated_at": int(time.time()),
}
)
else:
new_func = Function(
**{
**func.model_dump(),
"user_id": user_id,
"updated_at": int(time.time()),
}
)
db.add(new_func)
# Remove functions that are no longer present
for func in existing_functions:
if func.id not in new_function_ids:
db.delete(func)
db.commit()
return [
FunctionModel.model_validate(func)
for func in db.query(Function).all()
]
except Exception as e:
log.exception(f"Error syncing functions for user {user_id}: {e}")
return []
def get_function_by_id(self, id: str) -> Optional[FunctionModel]:
try:
with get_db() as db:
+121 -1
View File
@@ -83,10 +83,14 @@ class GroupForm(BaseModel):
permissions: Optional[dict] = None
class GroupUpdateForm(GroupForm):
class UserIdsForm(BaseModel):
user_ids: Optional[list[str]] = None
class GroupUpdateForm(GroupForm, UserIdsForm):
pass
class GroupTable:
def insert_new_group(
self, user_id: str, form_data: GroupForm
@@ -207,5 +211,121 @@ class GroupTable:
except Exception:
return False
def create_groups_by_group_names(
self, user_id: str, group_names: list[str]
) -> list[GroupModel]:
# check for existing groups
existing_groups = self.get_groups()
existing_group_names = {group.name for group in existing_groups}
new_groups = []
with get_db() as db:
for group_name in group_names:
if group_name not in existing_group_names:
new_group = GroupModel(
id=str(uuid.uuid4()),
user_id=user_id,
name=group_name,
description="",
created_at=int(time.time()),
updated_at=int(time.time()),
)
try:
result = Group(**new_group.model_dump())
db.add(result)
db.commit()
db.refresh(result)
new_groups.append(GroupModel.model_validate(result))
except Exception as e:
log.exception(e)
continue
return new_groups
def sync_groups_by_group_names(self, user_id: str, group_names: list[str]) -> bool:
with get_db() as db:
try:
groups = db.query(Group).filter(Group.name.in_(group_names)).all()
group_ids = [group.id for group in groups]
# Remove user from groups not in the new list
existing_groups = self.get_groups_by_member_id(user_id)
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()),
}
)
# 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()),
}
)
db.commit()
return True
except Exception as e:
log.exception(e)
return False
def add_users_to_group(
self, id: str, user_ids: Optional[list[str]] = None
) -> Optional[GroupModel]:
try:
with get_db() as db:
group = db.query(Group).filter_by(id=id).first()
if not group:
return None
if not group.user_ids:
group.user_ids = []
for user_id in user_ids:
if user_id not in group.user_ids:
group.user_ids.append(user_id)
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
def remove_users_from_group(
self, id: str, user_ids: Optional[list[str]] = None
) -> Optional[GroupModel]:
try:
with get_db() as db:
group = db.query(Group).filter_by(id=id).first()
if not group:
return None
if not group.user_ids:
return GroupModel.model_validate(group)
for user_id in user_ids:
if user_id in group.user_ids:
group.user_ids.remove(user_id)
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
Groups = GroupTable()
+3 -2
View File
@@ -63,14 +63,15 @@ class MemoriesTable:
else:
return None
def update_memory_by_id(
def update_memory_by_id_and_user_id(
self,
id: str,
user_id: str,
content: str,
) -> Optional[MemoryModel]:
with get_db() as db:
try:
db.query(Memory).filter_by(id=id).update(
db.query(Memory).filter_by(id=id, user_id=user_id).update(
{"content": content, "updated_at": int(time.time())}
)
db.commit()
+152
View File
@@ -0,0 +1,152 @@
import json
import time
import uuid
from typing import Optional
from open_webui.internal.db import Base, get_db
from open_webui.utils.access_control import has_access
from open_webui.models.users import Users, UserResponse
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
####################
# Note DB Schema
####################
class Note(Base):
__tablename__ = "note"
id = Column(Text, primary_key=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)
class NoteModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: str
user_id: str
title: str
data: Optional[dict] = None
meta: Optional[dict] = None
access_control: Optional[dict] = None
created_at: int # timestamp in epoch
updated_at: int # timestamp in epoch
####################
# Forms
####################
class NoteForm(BaseModel):
title: str
data: Optional[dict] = None
meta: Optional[dict] = None
access_control: Optional[dict] = None
class NoteUpdateForm(BaseModel):
title: Optional[str] = None
data: Optional[dict] = None
meta: Optional[dict] = None
access_control: Optional[dict] = None
class NoteUserResponse(NoteModel):
user: Optional[UserResponse] = None
class NoteTable:
def insert_new_note(
self,
form_data: NoteForm,
user_id: str,
) -> Optional[NoteModel]:
with get_db() as db:
note = NoteModel(
**{
"id": str(uuid.uuid4()),
"user_id": user_id,
**form_data.model_dump(),
"created_at": int(time.time_ns()),
"updated_at": int(time.time_ns()),
}
)
new_note = Note(**note.model_dump())
db.add(new_note)
db.commit()
return note
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_by_user_id(
self, user_id: str, permission: str = "write"
) -> list[NoteModel]:
notes = self.get_notes()
return [
note
for note in notes
if note.user_id == user_id
or has_access(user_id, permission, note.access_control)
]
def get_note_by_id(self, id: str) -> Optional[NoteModel]:
with get_db() as db:
note = db.query(Note).filter(Note.id == id).first()
return NoteModel.model_validate(note) if note else None
def update_note_by_id(
self, id: str, form_data: NoteUpdateForm
) -> Optional[NoteModel]:
with get_db() as db:
note = db.query(Note).filter(Note.id == id).first()
if not note:
return None
form_data = form_data.model_dump(exclude_unset=True)
if "title" in form_data:
note.title = form_data["title"]
if "data" in form_data:
note.data = {**note.data, **form_data["data"]}
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"]
note.updated_at = int(time.time_ns())
db.commit()
return NoteModel.model_validate(note) 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
Notes = NoteTable()
+88 -6
View File
@@ -10,6 +10,8 @@ from open_webui.models.groups import Groups
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Column, String, Text
from sqlalchemy import or_
####################
# User DB Schema
@@ -67,6 +69,23 @@ class UserModel(BaseModel):
####################
class UserListResponse(BaseModel):
users: list[UserModel]
total: int
class UserInfoResponse(BaseModel):
id: str
name: str
email: str
role: str
class UserInfoListResponse(BaseModel):
users: list[UserInfoResponse]
total: int
class UserResponse(BaseModel):
id: str
name: str
@@ -88,6 +107,7 @@ class UserRoleUpdateForm(BaseModel):
class UserUpdateForm(BaseModel):
role: str
name: str
email: str
profile_image_url: str
@@ -160,11 +180,63 @@ class UsersTable:
return None
def get_users(
self, skip: Optional[int] = None, limit: Optional[int] = None
) -> list[UserModel]:
self,
filter: Optional[dict] = None,
skip: Optional[int] = None,
limit: Optional[int] = None,
) -> UserListResponse:
with get_db() as db:
query = db.query(User)
query = db.query(User).order_by(User.created_at.desc())
if filter:
query_key = filter.get("query")
if query_key:
query = query.filter(
or_(
User.name.ilike(f"%{query_key}%"),
User.email.ilike(f"%{query_key}%"),
)
)
order_by = filter.get("order_by")
direction = filter.get("direction")
if 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())
else:
query = query.order_by(User.email.desc())
elif order_by == "created_at":
if direction == "asc":
query = query.order_by(User.created_at.asc())
else:
query = query.order_by(User.created_at.desc())
elif order_by == "last_active_at":
if direction == "asc":
query = query.order_by(User.last_active_at.asc())
else:
query = query.order_by(User.last_active_at.desc())
elif order_by == "updated_at":
if direction == "asc":
query = query.order_by(User.updated_at.asc())
else:
query = query.order_by(User.updated_at.desc())
elif order_by == "role":
if direction == "asc":
query = query.order_by(User.role.asc())
else:
query = query.order_by(User.role.desc())
else:
query = query.order_by(User.created_at.desc())
if skip:
query = query.offset(skip)
@@ -172,8 +244,10 @@ class UsersTable:
query = query.limit(limit)
users = query.all()
return [UserModel.model_validate(user) for user in users]
return {
"users": [UserModel.model_validate(user) for user in users],
"total": db.query(User).count(),
}
def get_users_by_user_ids(self, user_ids: list[str]) -> list[UserModel]:
with get_db() as db:
@@ -308,7 +382,7 @@ class UsersTable:
except Exception:
return False
def update_user_api_key_by_id(self, id: str, api_key: str) -> str:
def update_user_api_key_by_id(self, id: str, api_key: str) -> bool:
try:
with get_db() as db:
result = db.query(User).filter_by(id=id).update({"api_key": api_key})
@@ -330,5 +404,13 @@ class UsersTable:
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:
user = db.query(User).filter_by(role="admin").first()
if user:
return UserModel.model_validate(user)
else:
return None
Users = UsersTable()
@@ -0,0 +1,251 @@
import os
import time
import requests
import logging
import json
from typing import List, Optional
from langchain_core.documents import Document
from fastapi import HTTPException, status
log = logging.getLogger(__name__)
class DatalabMarkerLoader:
def __init__(
self,
file_path: str,
api_key: str,
langs: Optional[str] = None,
use_llm: bool = False,
skip_cache: bool = False,
force_ocr: bool = False,
paginate: bool = False,
strip_existing_ocr: bool = False,
disable_image_extraction: bool = False,
output_format: str = None,
):
self.file_path = file_path
self.api_key = api_key
self.langs = langs
self.use_llm = use_llm
self.skip_cache = skip_cache
self.force_ocr = force_ocr
self.paginate = paginate
self.strip_existing_ocr = strip_existing_ocr
self.disable_image_extraction = disable_image_extraction
self.output_format = output_format
def _get_mime_type(self, filename: str) -> str:
ext = filename.rsplit(".", 1)[-1].lower()
mime_map = {
"pdf": "application/pdf",
"xls": "application/vnd.ms-excel",
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"ods": "application/vnd.oasis.opendocument.spreadsheet",
"doc": "application/msword",
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"odt": "application/vnd.oasis.opendocument.text",
"ppt": "application/vnd.ms-powerpoint",
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
"odp": "application/vnd.oasis.opendocument.presentation",
"html": "text/html",
"epub": "application/epub+zip",
"png": "image/png",
"jpeg": "image/jpeg",
"jpg": "image/jpeg",
"webp": "image/webp",
"gif": "image/gif",
"tiff": "image/tiff",
}
return mime_map.get(ext, "application/octet-stream")
def check_marker_request_status(self, request_id: str) -> dict:
url = f"https://www.datalab.to/api/v1/marker/{request_id}"
headers = {"X-Api-Key": self.api_key}
try:
response = requests.get(url, headers=headers)
response.raise_for_status()
result = response.json()
log.info(f"Marker API status check for request {request_id}: {result}")
return result
except requests.HTTPError as e:
log.error(f"Error checking Marker request status: {e}")
raise HTTPException(
status.HTTP_502_BAD_GATEWAY,
detail=f"Failed to check Marker request: {e}",
)
except ValueError as e:
log.error(f"Invalid JSON checking Marker request: {e}")
raise HTTPException(
status.HTTP_502_BAD_GATEWAY, detail=f"Invalid JSON: {e}"
)
def load(self) -> List[Document]:
url = "https://www.datalab.to/api/v1/marker"
filename = os.path.basename(self.file_path)
mime_type = self._get_mime_type(filename)
headers = {"X-Api-Key": self.api_key}
form_data = {
"langs": self.langs,
"use_llm": str(self.use_llm).lower(),
"skip_cache": str(self.skip_cache).lower(),
"force_ocr": str(self.force_ocr).lower(),
"paginate": str(self.paginate).lower(),
"strip_existing_ocr": str(self.strip_existing_ocr).lower(),
"disable_image_extraction": str(self.disable_image_extraction).lower(),
"output_format": self.output_format,
}
log.info(
f"Datalab Marker POST request parameters: {{'filename': '{filename}', 'mime_type': '{mime_type}', **{form_data}}}"
)
try:
with open(self.file_path, "rb") as f:
files = {"file": (filename, f, mime_type)}
response = requests.post(
url, data=form_data, files=files, headers=headers
)
response.raise_for_status()
result = response.json()
except FileNotFoundError:
raise HTTPException(
status.HTTP_404_NOT_FOUND, detail=f"File not found: {self.file_path}"
)
except requests.HTTPError as e:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail=f"Datalab Marker request failed: {e}",
)
except ValueError as e:
raise HTTPException(
status.HTTP_502_BAD_GATEWAY, detail=f"Invalid JSON response: {e}"
)
except Exception as e:
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
if not result.get("success"):
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail=f"Datalab Marker request failed: {result.get('error', 'Unknown error')}",
)
check_url = result.get("request_check_url")
request_id = result.get("request_id")
if not check_url:
raise HTTPException(
status.HTTP_502_BAD_GATEWAY, detail="No request_check_url returned."
)
for _ in range(300): # Up to 10 minutes
time.sleep(2)
try:
poll_response = requests.get(check_url, headers=headers)
poll_response.raise_for_status()
poll_result = poll_response.json()
except (requests.HTTPError, ValueError) as e:
raw_body = poll_response.text
log.error(f"Polling error: {e}, response body: {raw_body}")
raise HTTPException(
status.HTTP_502_BAD_GATEWAY, detail=f"Polling failed: {e}"
)
status_val = poll_result.get("status")
success_val = poll_result.get("success")
if status_val == "complete":
summary = {
k: poll_result.get(k)
for k in (
"status",
"output_format",
"success",
"error",
"page_count",
"total_cost",
)
}
log.info(
f"Marker processing completed successfully: {json.dumps(summary, indent=2)}"
)
break
if status_val == "failed" or success_val is False:
log.error(
f"Marker poll failed full response: {json.dumps(poll_result, indent=2)}"
)
error_msg = (
poll_result.get("error")
or "Marker returned failure without error message"
)
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail=f"Marker processing failed: {error_msg}",
)
else:
raise HTTPException(
status.HTTP_504_GATEWAY_TIMEOUT, detail="Marker processing timed out"
)
if not poll_result.get("success", False):
error_msg = poll_result.get("error") or "Unknown processing error"
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail=f"Final processing failed: {error_msg}",
)
content_key = self.output_format.lower()
raw_content = poll_result.get(content_key)
if content_key == "json":
full_text = json.dumps(raw_content, indent=2)
elif content_key in {"markdown", "html"}:
full_text = str(raw_content).strip()
else:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail=f"Unsupported output format: {self.output_format}",
)
if not full_text:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail="Datalab Marker returned empty content",
)
marker_output_dir = os.path.join("/app/backend/data/uploads", "marker_output")
os.makedirs(marker_output_dir, exist_ok=True)
file_ext_map = {"markdown": "md", "json": "json", "html": "html"}
file_ext = file_ext_map.get(content_key, "txt")
output_filename = f"{os.path.splitext(filename)[0]}.{file_ext}"
output_path = os.path.join(marker_output_dir, output_filename)
try:
with open(output_path, "w", encoding="utf-8") as f:
f.write(full_text)
log.info(f"Saved Marker output to: {output_path}")
except Exception as e:
log.warning(f"Failed to write marker output to disk: {e}")
metadata = {
"source": filename,
"output_format": poll_result.get("output_format", self.output_format),
"page_count": poll_result.get("page_count", 0),
"processed_with_llm": self.use_llm,
"request_id": request_id or "",
}
images = poll_result.get("images", {})
if images:
metadata["image_count"] = len(images)
metadata["images"] = json.dumps(list(images.keys()))
for k, v in metadata.items():
if isinstance(v, (dict, list)):
metadata[k] = json.dumps(v)
elif v is None:
metadata[k] = ""
return [Document(page_content=full_text, metadata=metadata)]
@@ -0,0 +1,83 @@
import requests
import logging, os
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 ExternalDocumentLoader(BaseLoader):
def __init__(
self,
file_path,
url: str,
api_key: str,
mime_type=None,
**kwargs,
) -> None:
self.url = url
self.api_key = api_key
self.file_path = file_path
self.mime_type = mime_type
def load(self) -> List[Document]:
with open(self.file_path, "rb") as f:
data = f.read()
headers = {}
if self.mime_type is not None:
headers["Content-Type"] = self.mime_type
if self.api_key is not None:
headers["Authorization"] = f"Bearer {self.api_key}"
try:
headers["X-Filename"] = os.path.basename(self.file_path)
except:
pass
url = self.url
if url.endswith("/"):
url = url[:-1]
try:
response = requests.put(f"{url}/process", data=data, headers=headers)
except Exception as e:
log.error(f"Error connecting to endpoint: {e}")
raise Exception(f"Error connecting to endpoint: {e}")
if response.ok:
response_data = response.json()
if response_data:
if isinstance(response_data, dict):
return [
Document(
page_content=response_data.get("page_content"),
metadata=response_data.get("metadata"),
)
]
elif isinstance(response_data, list):
documents = []
for document in response_data:
documents.append(
Document(
page_content=document.get("page_content"),
metadata=document.get("metadata"),
)
)
return documents
else:
raise Exception("Error loading document: Unable to parse content")
else:
raise Exception("Error loading document: No content returned")
else:
raise Exception(
f"Error loading document: {response.status_code} {response.text}"
)
@@ -0,0 +1,53 @@
import requests
import logging
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):
def __init__(
self,
web_paths: Union[str, List[str]],
external_url: str,
external_api_key: str,
continue_on_failure: bool = True,
**kwargs,
) -> None:
self.external_url = external_url
self.external_api_key = external_api_key
self.urls = web_paths if isinstance(web_paths, list) else [web_paths]
self.continue_on_failure = continue_on_failure
def lazy_load(self) -> Iterator[Document]:
batch_size = 20
for i in range(0, len(self.urls), batch_size):
urls = self.urls[i : i + batch_size]
try:
response = requests.post(
self.external_url,
headers={
"User-Agent": "Open WebUI (https://github.com/open-webui/open-webui) External Web Loader",
"Authorization": f"Bearer {self.external_api_key}",
},
json={
"urls": urls,
},
)
response.raise_for_status()
results = response.json()
for result in results:
yield Document(
page_content=result.get("page_content", ""),
metadata=result.get("metadata", {}),
)
except Exception as e:
if self.continue_on_failure:
log.error(f"Error extracting content from batch {urls}: {e}")
else:
raise e
+129 -10
View File
@@ -2,6 +2,7 @@ import requests
import logging
import ftfy
import sys
import json
from langchain_community.document_loaders import (
AzureAIDocumentIntelligenceLoader,
@@ -13,7 +14,7 @@ from langchain_community.document_loaders import (
TextLoader,
UnstructuredEPubLoader,
UnstructuredExcelLoader,
UnstructuredMarkdownLoader,
UnstructuredODTLoader,
UnstructuredPowerPointLoader,
UnstructuredRSTLoader,
UnstructuredXMLLoader,
@@ -21,7 +22,11 @@ from langchain_community.document_loaders import (
)
from langchain_core.documents import Document
from open_webui.retrieval.loaders.external_document import ExternalDocumentLoader
from open_webui.retrieval.loaders.mistral import MistralLoader
from open_webui.retrieval.loaders.datalab_marker import DatalabMarkerLoader
from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL
@@ -72,7 +77,6 @@ known_source_ext = [
"swift",
"vue",
"svelte",
"msg",
"ex",
"exs",
"erl",
@@ -85,11 +89,13 @@ known_source_ext = [
class TikaLoader:
def __init__(self, url, file_path, mime_type=None):
def __init__(self, url, file_path, mime_type=None, extract_images=None):
self.url = url
self.file_path = file_path
self.mime_type = mime_type
self.extract_images = extract_images
def load(self) -> list[Document]:
with open(self.file_path, "rb") as f:
data = f.read()
@@ -99,6 +105,9 @@ class TikaLoader:
else:
headers = {}
if self.extract_images == True:
headers["X-Tika-PDFextractInlineImages"] = "true"
endpoint = self.url
if not endpoint.endswith("/"):
endpoint += "/"
@@ -121,11 +130,13 @@ class TikaLoader:
class DoclingLoader:
def __init__(self, url, file_path=None, mime_type=None):
def __init__(self, url, file_path=None, mime_type=None, params=None):
self.url = url.rstrip("/")
self.file_path = file_path
self.mime_type = mime_type
self.params = params or {}
def load(self) -> list[Document]:
with open(self.file_path, "rb") as f:
files = {
@@ -136,10 +147,39 @@ class DoclingLoader:
)
}
params = {
"image_export_mode": "placeholder",
"table_mode": "accurate",
}
params = {"image_export_mode": "placeholder", "table_mode": "accurate"}
if self.params:
if self.params.get("do_picture_description"):
params["do_picture_description"] = self.params.get(
"do_picture_description"
)
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", {})
)
if 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()
]
endpoint = f"{self.url}/v1alpha/convert/file"
r = requests.post(endpoint, files=files, data=params)
@@ -186,13 +226,27 @@ class Loader:
def _is_text_file(self, file_ext: str, file_content_type: str) -> bool:
return file_ext in known_source_ext or (
file_content_type and file_content_type.find("text/") >= 0
file_content_type
and file_content_type.find("text/") >= 0
# Avoid text/html files being detected as text
and not file_content_type.find("html") >= 0
)
def _get_loader(self, filename: str, file_content_type: str, file_path: str):
file_ext = filename.split(".")[-1].lower()
if self.engine == "tika" and self.kwargs.get("TIKA_SERVER_URL"):
if (
self.engine == "external"
and self.kwargs.get("EXTERNAL_DOCUMENT_LOADER_URL")
and self.kwargs.get("EXTERNAL_DOCUMENT_LOADER_API_KEY")
):
loader = ExternalDocumentLoader(
file_path=file_path,
url=self.kwargs.get("EXTERNAL_DOCUMENT_LOADER_URL"),
api_key=self.kwargs.get("EXTERNAL_DOCUMENT_LOADER_API_KEY"),
mime_type=file_content_type,
)
elif self.engine == "tika" and self.kwargs.get("TIKA_SERVER_URL"):
if self._is_text_file(file_ext, file_content_type):
loader = TextLoader(file_path, autodetect_encoding=True)
else:
@@ -200,15 +254,69 @@ class Loader:
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 (
self.engine == "datalab_marker"
and self.kwargs.get("DATALAB_MARKER_API_KEY")
and file_ext
in [
"pdf",
"xls",
"xlsx",
"ods",
"doc",
"docx",
"odt",
"ppt",
"pptx",
"odp",
"html",
"epub",
"png",
"jpeg",
"jpg",
"webp",
"gif",
"tiff",
]
):
loader = DatalabMarkerLoader(
file_path=file_path,
api_key=self.kwargs["DATALAB_MARKER_API_KEY"],
langs=self.kwargs.get("DATALAB_MARKER_LANGS"),
use_llm=self.kwargs.get("DATALAB_MARKER_USE_LLM", False),
skip_cache=self.kwargs.get("DATALAB_MARKER_SKIP_CACHE", False),
force_ocr=self.kwargs.get("DATALAB_MARKER_FORCE_OCR", False),
paginate=self.kwargs.get("DATALAB_MARKER_PAGINATE", False),
strip_existing_ocr=self.kwargs.get(
"DATALAB_MARKER_STRIP_EXISTING_OCR", False
),
disable_image_extraction=self.kwargs.get(
"DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION", False
),
output_format=self.kwargs.get(
"DATALAB_MARKER_OUTPUT_FORMAT", "markdown"
),
)
elif self.engine == "docling" and self.kwargs.get("DOCLING_SERVER_URL"):
if self._is_text_file(file_ext, file_content_type):
loader = TextLoader(file_path, autodetect_encoding=True)
else:
# Build params for DoclingLoader
params = self.kwargs.get("DOCLING_PARAMS", {})
if not isinstance(params, dict):
try:
params = json.loads(params)
except json.JSONDecodeError:
log.error("Invalid DOCLING_PARAMS format, expected JSON object")
params = {}
loader = DoclingLoader(
url=self.kwargs.get("DOCLING_SERVER_URL"),
file_path=file_path,
mime_type=file_content_type,
params=params,
)
elif (
self.engine == "document_intelligence"
@@ -240,6 +348,15 @@ class Loader:
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
)
else:
if file_ext == "pdf":
loader = PyPDFLoader(
@@ -275,6 +392,8 @@ class Loader:
loader = UnstructuredPowerPointLoader(file_path)
elif file_ext == "msg":
loader = OutlookMessageLoader(file_path)
elif file_ext == "odt":
loader = UnstructuredODTLoader(file_path)
elif self._is_text_file(file_ext, file_content_type):
loader = TextLoader(file_path, autodetect_encoding=True)
else:
+619 -76
View File
@@ -1,8 +1,12 @@
import requests
import aiohttp
import asyncio
import logging
import os
import sys
import time
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
@@ -14,18 +18,37 @@ log.setLevel(SRC_LOG_LEVELS["RAG"])
class MistralLoader:
"""
Enhanced Mistral OCR loader with both sync and async support.
Loads documents by processing them through the Mistral OCR API.
Performance Optimizations:
- Differentiated timeouts for different operations
- Intelligent retry logic with exponential backoff
- Memory-efficient file streaming for large files
- Connection pooling and keepalive optimization
- Semaphore-based concurrency control for batch processing
- Enhanced error handling with retryable error classification
"""
BASE_API_URL = "https://api.mistral.ai/v1"
def __init__(self, api_key: str, file_path: str):
def __init__(
self,
api_key: str,
file_path: str,
timeout: int = 300, # 5 minutes default
max_retries: int = 3,
enable_debug_logging: bool = False,
):
"""
Initializes the loader.
Initializes the loader with enhanced features.
Args:
api_key: Your Mistral API key.
file_path: The local path to the PDF file to process.
timeout: Request timeout in seconds.
max_retries: Maximum number of retry attempts.
enable_debug_logging: Enable detailed debug logs.
"""
if not api_key:
raise ValueError("API key cannot be empty.")
@@ -34,7 +57,46 @@ class MistralLoader:
self.api_key = api_key
self.file_path = file_path
self.headers = {"Authorization": f"Bearer {self.api_key}"}
self.timeout = timeout
self.max_retries = max_retries
self.debug = enable_debug_logging
# PERFORMANCE OPTIMIZATION: Differentiated timeouts for different operations
# This prevents long-running OCR operations from affecting quick operations
# and improves user experience by failing fast on operations that should be quick
self.upload_timeout = min(
timeout, 120
) # Cap upload at 2 minutes - prevents hanging on large files
self.url_timeout = (
30 # URL requests should be fast - fail quickly if API is slow
)
self.ocr_timeout = (
timeout # OCR can take the full timeout - this is the heavy operation
)
self.cleanup_timeout = (
30 # Cleanup should be quick - don't hang on file deletion
)
# PERFORMANCE OPTIMIZATION: Pre-compute file info to avoid repeated filesystem calls
# This avoids multiple os.path.basename() and os.path.getsize() calls during processing
self.file_name = os.path.basename(file_path)
self.file_size = os.path.getsize(file_path)
# ENHANCEMENT: Added User-Agent for better API tracking and debugging
self.headers = {
"Authorization": f"Bearer {self.api_key}",
"User-Agent": "OpenWebUI-MistralLoader/2.0", # Helps API provider track usage
}
def _debug_log(self, message: str, *args) -> None:
"""
PERFORMANCE OPTIMIZATION: Conditional debug logging for performance.
Only processes debug messages when debug mode is enabled, avoiding
string formatting overhead in production environments.
"""
if self.debug:
log.debug(message, *args)
def _handle_response(self, response: requests.Response) -> Dict[str, Any]:
"""Checks response status and returns JSON content."""
@@ -54,24 +116,154 @@ class MistralLoader:
log.error(f"JSON decode error: {json_err} - Response: {response.text}")
raise # Re-raise after logging
def _upload_file(self) -> str:
"""Uploads the file to Mistral for OCR processing."""
log.info("Uploading file to Mistral API")
url = f"{self.BASE_API_URL}/files"
file_name = os.path.basename(self.file_path)
async def _handle_response_async(
self, response: aiohttp.ClientResponse
) -> Dict[str, Any]:
"""Async version of response handling with better error info."""
try:
with open(self.file_path, "rb") as f:
files = {"file": (file_name, f, "application/pdf")}
data = {"purpose": "ocr"}
response.raise_for_status()
upload_headers = self.headers.copy() # Avoid modifying self.headers
response = requests.post(
url, headers=upload_headers, files=files, data=data
# Check content type
content_type = response.headers.get("content-type", "")
if "application/json" not in content_type:
if response.status == 204:
return {}
text = await response.text()
raise ValueError(
f"Unexpected content type: {content_type}, body: {text[:200]}..."
)
response_data = self._handle_response(response)
return await response.json()
except aiohttp.ClientResponseError as e:
error_text = await response.text() if response else "No response"
log.error(f"HTTP {e.status}: {e.message} - Response: {error_text[:500]}")
raise
except aiohttp.ClientError as e:
log.error(f"Client error: {e}")
raise
except Exception as e:
log.error(f"Unexpected error processing response: {e}")
raise
def _is_retryable_error(self, error: Exception) -> bool:
"""
ENHANCEMENT: Intelligent error classification for retry logic.
Determines if an error is retryable based on its type and status code.
This prevents wasting time retrying errors that will never succeed
(like authentication errors) while ensuring transient errors are retried.
Retryable errors:
- Network connection errors (temporary network issues)
- Timeouts (server might be temporarily overloaded)
- Server errors (5xx status codes - server-side issues)
- Rate limiting (429 status - temporary throttling)
Non-retryable errors:
- Authentication errors (401, 403 - won't fix with retry)
- Bad request errors (400 - malformed request)
- Not found errors (404 - resource doesn't exist)
"""
if isinstance(error, requests.exceptions.ConnectionError):
return True # Network issues are usually temporary
if isinstance(error, requests.exceptions.Timeout):
return True # Timeouts might resolve on retry
if isinstance(error, requests.exceptions.HTTPError):
# Only retry on server errors (5xx) or rate limits (429)
if hasattr(error, "response") and error.response is not None:
status_code = error.response.status_code
return status_code >= 500 or status_code == 429
return False
if isinstance(
error, (aiohttp.ClientConnectionError, aiohttp.ServerTimeoutError)
):
return True # Async network/timeout errors are retryable
if isinstance(error, aiohttp.ClientResponseError):
return error.status >= 500 or error.status == 429
return False # All other errors are non-retryable
def _retry_request_sync(self, request_func, *args, **kwargs):
"""
ENHANCEMENT: Synchronous retry logic with intelligent error classification.
Uses exponential backoff with jitter to avoid thundering herd problems.
The wait time increases exponentially but is capped at 30 seconds to
prevent excessive delays. Only retries errors that are likely to succeed
on subsequent attempts.
"""
for attempt in range(self.max_retries):
try:
return request_func(*args, **kwargs)
except Exception as e:
if attempt == self.max_retries - 1 or not self._is_retryable_error(e):
raise
# PERFORMANCE OPTIMIZATION: Exponential backoff with cap
# Prevents overwhelming the server while ensuring reasonable retry delays
wait_time = min((2**attempt) + 0.5, 30) # Cap at 30 seconds
log.warning(
f"Retryable error (attempt {attempt + 1}/{self.max_retries}): {e}. "
f"Retrying in {wait_time}s..."
)
time.sleep(wait_time)
async def _retry_request_async(self, request_func, *args, **kwargs):
"""
ENHANCEMENT: Async retry logic with intelligent error classification.
Async version of retry logic that doesn't block the event loop during
wait periods. Uses the same exponential backoff strategy as sync version.
"""
for attempt in range(self.max_retries):
try:
return await request_func(*args, **kwargs)
except Exception as e:
if attempt == self.max_retries - 1 or not self._is_retryable_error(e):
raise
# PERFORMANCE OPTIMIZATION: Non-blocking exponential backoff
wait_time = min((2**attempt) + 0.5, 30) # Cap at 30 seconds
log.warning(
f"Retryable error (attempt {attempt + 1}/{self.max_retries}): {e}. "
f"Retrying in {wait_time}s..."
)
await asyncio.sleep(wait_time) # Non-blocking wait
def _upload_file(self) -> str:
"""
PERFORMANCE OPTIMIZATION: Enhanced file upload with streaming consideration.
Uploads the file to Mistral for OCR processing (sync version).
Uses context manager for file handling to ensure proper resource cleanup.
Although streaming is not enabled for this endpoint, the file is opened
in a context manager to minimize memory usage duration.
"""
log.info("Uploading file to Mistral API")
url = f"{self.BASE_API_URL}/files"
def upload_request():
# MEMORY OPTIMIZATION: Use context manager to minimize file handle lifetime
# This ensures the file is closed immediately after reading, reducing memory usage
with open(self.file_path, "rb") as f:
files = {"file": (self.file_name, f, "application/pdf")}
data = {"purpose": "ocr"}
# NOTE: stream=False is required for this endpoint
# The Mistral API doesn't support chunked uploads for this endpoint
response = requests.post(
url,
headers=self.headers,
files=files,
data=data,
timeout=self.upload_timeout, # Use specialized upload timeout
stream=False, # Keep as False for this endpoint
)
return self._handle_response(response)
try:
response_data = self._retry_request_sync(upload_request)
file_id = response_data.get("id")
if not file_id:
raise ValueError("File ID not found in upload response.")
@@ -81,16 +273,66 @@ class MistralLoader:
log.error(f"Failed to upload file: {e}")
raise
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"
async def upload_request():
# Create multipart writer for streaming upload
writer = aiohttp.MultipartWriter("form-data")
# Add purpose field
purpose_part = writer.append("ocr")
purpose_part.set_content_disposition("form-data", name="purpose")
# Add file part with streaming
file_part = writer.append_payload(
aiohttp.streams.FilePayload(
self.file_path,
filename=self.file_name,
content_type="application/pdf",
)
)
file_part.set_content_disposition(
"form-data", name="file", filename=self.file_name
)
self._debug_log(
f"Uploading file: {self.file_name} ({self.file_size:,} bytes)"
)
async with session.post(
url,
data=writer,
headers=self.headers,
timeout=aiohttp.ClientTimeout(total=self.upload_timeout),
) as response:
return await self._handle_response_async(response)
response_data = await self._retry_request_async(upload_request)
file_id = response_data.get("id")
if not file_id:
raise ValueError("File ID not found in upload response.")
log.info(f"File uploaded successfully. File ID: {file_id}")
return file_id
def _get_signed_url(self, file_id: str) -> str:
"""Retrieves a temporary signed URL for the uploaded file."""
"""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"
params = {"expiry": 1}
signed_url_headers = {**self.headers, "Accept": "application/json"}
def url_request():
response = requests.get(
url, headers=signed_url_headers, params=params, timeout=self.url_timeout
)
return self._handle_response(response)
try:
response = requests.get(url, headers=signed_url_headers, params=params)
response_data = self._handle_response(response)
response_data = self._retry_request_sync(url_request)
signed_url = response_data.get("url")
if not signed_url:
raise ValueError("Signed URL not found in response.")
@@ -100,8 +342,36 @@ class MistralLoader:
log.error(f"Failed to get signed URL: {e}")
raise
async def _get_signed_url_async(
self, session: aiohttp.ClientSession, file_id: str
) -> str:
"""Async signed URL retrieval."""
url = f"{self.BASE_API_URL}/files/{file_id}/url"
params = {"expiry": 1}
headers = {**self.headers, "Accept": "application/json"}
async def url_request():
self._debug_log(f"Getting signed URL for file ID: {file_id}")
async with session.get(
url,
headers=headers,
params=params,
timeout=aiohttp.ClientTimeout(total=self.url_timeout),
) as response:
return await self._handle_response_async(response)
response_data = await self._retry_request_async(url_request)
signed_url = response_data.get("url")
if not signed_url:
raise ValueError("Signed URL not found in response.")
self._debug_log("Signed URL received successfully")
return signed_url
def _process_ocr(self, signed_url: str) -> Dict[str, Any]:
"""Sends the signed URL to the OCR endpoint for processing."""
"""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"
ocr_headers = {
@@ -118,43 +388,218 @@ class MistralLoader:
"include_image_base64": False,
}
def ocr_request():
response = requests.post(
url, headers=ocr_headers, json=payload, timeout=self.ocr_timeout
)
return self._handle_response(response)
try:
response = requests.post(url, headers=ocr_headers, json=payload)
ocr_response = self._handle_response(response)
ocr_response = self._retry_request_sync(ocr_request)
log.info("OCR processing done.")
log.debug("OCR response: %s", ocr_response)
self._debug_log("OCR response: %s", ocr_response)
return ocr_response
except Exception as e:
log.error(f"Failed during OCR processing: {e}")
raise
async def _process_ocr_async(
self, session: aiohttp.ClientSession, signed_url: str
) -> Dict[str, Any]:
"""Async OCR processing with timing metrics."""
url = f"{self.BASE_API_URL}/ocr"
headers = {
**self.headers,
"Content-Type": "application/json",
"Accept": "application/json",
}
payload = {
"model": "mistral-ocr-latest",
"document": {
"type": "document_url",
"document_url": signed_url,
},
"include_image_base64": False,
}
async def ocr_request():
log.info("Starting OCR processing via Mistral API")
start_time = time.time()
async with session.post(
url,
json=payload,
headers=headers,
timeout=aiohttp.ClientTimeout(total=self.ocr_timeout),
) as response:
ocr_response = await self._handle_response_async(response)
processing_time = time.time() - start_time
log.info(f"OCR processing completed in {processing_time:.2f}s")
return ocr_response
return await self._retry_request_async(ocr_request)
def _delete_file(self, file_id: str) -> None:
"""Deletes the file from Mistral storage."""
"""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}"
# No specific Accept header needed, default or Authorization is usually sufficient
try:
response = requests.delete(url, headers=self.headers)
delete_response = self._handle_response(
response
) # Check status, ignore response body unless needed
log.info(
f"File deleted successfully: {delete_response}"
) # Log the response if available
response = requests.delete(
url, headers=self.headers, timeout=self.cleanup_timeout
)
delete_response = self._handle_response(response)
log.info(f"File deleted successfully: {delete_response}")
except Exception as e:
# Log error but don't necessarily halt execution if deletion fails
log.error(f"Failed to delete file ID {file_id}: {e}")
# Depending on requirements, you might choose to raise the error here
async def _delete_file_async(
self, session: aiohttp.ClientSession, file_id: str
) -> None:
"""Async file deletion with error tolerance."""
try:
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}",
headers=self.headers,
timeout=aiohttp.ClientTimeout(
total=self.cleanup_timeout
), # Shorter timeout for cleanup
) as response:
return await self._handle_response_async(response)
await self._retry_request_async(delete_request)
self._debug_log(f"File {file_id} deleted successfully")
except Exception as e:
# Don't fail the entire process if cleanup fails
log.warning(f"Failed to delete file ID {file_id}: {e}")
@asynccontextmanager
async def _get_session(self):
"""Context manager for HTTP session with optimized settings."""
connector = aiohttp.TCPConnector(
limit=20, # Increased total connection limit for better throughput
limit_per_host=10, # Increased per-host limit for API endpoints
ttl_dns_cache=600, # Longer DNS cache TTL (10 minutes)
use_dns_cache=True,
keepalive_timeout=60, # Increased keepalive for connection reuse
enable_cleanup_closed=True,
force_close=False, # Allow connection reuse
resolver=aiohttp.AsyncResolver(), # Use async DNS resolver
)
timeout = aiohttp.ClientTimeout(
total=self.timeout,
connect=30, # Connection timeout
sock_read=60, # Socket read timeout
)
async with aiohttp.ClientSession(
connector=connector,
timeout=timeout,
headers={"User-Agent": "OpenWebUI-MistralLoader/2.0"},
raise_for_status=False, # We handle status codes manually
trust_env=True,
) as session:
yield session
def _process_results(self, ocr_response: Dict[str, Any]) -> List[Document]:
"""Process OCR results into Document objects with enhanced metadata and memory efficiency."""
pages_data = ocr_response.get("pages")
if not pages_data:
log.warning("No pages found in OCR response.")
return [
Document(
page_content="No text content found",
metadata={"error": "no_pages", "file_name": self.file_name},
)
]
documents = []
total_pages = len(pages_data)
skipped_pages = 0
# Process pages in a memory-efficient way
for page_data in pages_data:
page_content = page_data.get("markdown")
page_index = page_data.get("index") # API uses 0-based index
if page_content is None or page_index is None:
skipped_pages += 1
self._debug_log(
f"Skipping page due to missing 'markdown' or 'index'. Data keys: {list(page_data.keys())}"
)
continue
# Clean up content efficiently with early exit for empty content
if isinstance(page_content, str):
cleaned_content = page_content.strip()
else:
cleaned_content = str(page_content).strip()
if not cleaned_content:
skipped_pages += 1
self._debug_log(f"Skipping empty page {page_index}")
continue
# Create document with optimized metadata
documents.append(
Document(
page_content=cleaned_content,
metadata={
"page": page_index, # 0-based index from API
"page_label": page_index + 1, # 1-based label for convenience
"total_pages": total_pages,
"file_name": self.file_name,
"file_size": self.file_size,
"processing_engine": "mistral-ocr",
"content_length": len(cleaned_content),
},
)
)
if skipped_pages > 0:
log.info(
f"Processed {len(documents)} pages, skipped {skipped_pages} empty/invalid pages"
)
if not documents:
# Case where pages existed but none had valid markdown/index
log.warning(
"OCR response contained pages, but none had valid content/index."
)
return [
Document(
page_content="No valid text content found in document",
metadata={
"error": "no_valid_pages",
"total_pages": total_pages,
"file_name": self.file_name,
},
)
]
return documents
def load(self) -> List[Document]:
"""
Executes the full OCR workflow: upload, get URL, process OCR, delete file.
Synchronous version for backward compatibility.
Returns:
A list of Document objects, one for each page processed.
"""
file_id = None
start_time = time.time()
try:
# 1. Upload file
file_id = self._upload_file()
@@ -166,53 +611,30 @@ class MistralLoader:
ocr_response = self._process_ocr(signed_url)
# 4. Process results
pages_data = ocr_response.get("pages")
if not pages_data:
log.warning("No pages found in OCR response.")
return [Document(page_content="No text content found", metadata={})]
documents = self._process_results(ocr_response)
documents = []
total_pages = len(pages_data)
for page_data in pages_data:
page_content = page_data.get("markdown")
page_index = page_data.get("index") # API uses 0-based index
if page_content is not None and page_index is not None:
documents.append(
Document(
page_content=page_content,
metadata={
"page": page_index, # 0-based index from API
"page_label": page_index
+ 1, # 1-based label for convenience
"total_pages": total_pages,
# Add other relevant metadata from page_data if available/needed
# e.g., page_data.get('width'), page_data.get('height')
},
)
)
else:
log.warning(
f"Skipping page due to missing 'markdown' or 'index'. Data: {page_data}"
)
if not documents:
# Case where pages existed but none had valid markdown/index
log.warning(
"OCR response contained pages, but none had valid content/index."
)
return [
Document(
page_content="No text content found in valid pages", metadata={}
)
]
total_time = time.time() - start_time
log.info(
f"Sync OCR workflow completed in {total_time:.2f}s, produced {len(documents)} documents"
)
return documents
except Exception as e:
log.error(f"An error occurred during the loading process: {e}")
# Return an empty list or a specific error document on failure
return [Document(page_content=f"Error during processing: {e}", metadata={})]
total_time = time.time() - start_time
log.error(
f"An error occurred during the loading process after {total_time:.2f}s: {e}"
)
# Return an error document on failure
return [
Document(
page_content=f"Error during processing: {e}",
metadata={
"error": "processing_failed",
"file_name": self.file_name,
},
)
]
finally:
# 5. Delete file (attempt even if prior steps failed after upload)
if file_id:
@@ -223,3 +645,124 @@ class MistralLoader:
log.error(
f"Cleanup error: Could not delete file ID {file_id}. Reason: {del_e}"
)
async def load_async(self) -> List[Document]:
"""
Asynchronous OCR workflow execution with optimized performance.
Returns:
A list of Document objects, one for each page processed.
"""
file_id = None
start_time = time.time()
try:
async with self._get_session() as session:
# 1. Upload file with streaming
file_id = await self._upload_file_async(session)
# 2. Get signed URL
signed_url = await self._get_signed_url_async(session, file_id)
# 3. Process OCR
ocr_response = await self._process_ocr_async(session, signed_url)
# 4. Process results
documents = self._process_results(ocr_response)
total_time = time.time() - start_time
log.info(
f"Async OCR workflow completed in {total_time:.2f}s, produced {len(documents)} documents"
)
return documents
except Exception as e:
total_time = time.time() - start_time
log.error(f"Async OCR workflow failed after {total_time:.2f}s: {e}")
return [
Document(
page_content=f"Error during OCR processing: {e}",
metadata={
"error": "processing_failed",
"file_name": self.file_name,
},
)
]
finally:
# 5. Cleanup - always attempt file deletion
if file_id:
try:
async with self._get_session() as session:
await self._delete_file_async(session, file_id)
except Exception as cleanup_error:
log.error(f"Cleanup failed for file ID {file_id}: {cleanup_error}")
@staticmethod
async def load_multiple_async(
loaders: List["MistralLoader"],
max_concurrent: int = 5, # Limit concurrent requests
) -> List[List[Document]]:
"""
Process multiple files concurrently with controlled concurrency.
Args:
loaders: List of MistralLoader instances
max_concurrent: Maximum number of concurrent requests
Returns:
List of document lists, one for each loader
"""
if not loaders:
return []
log.info(
f"Starting concurrent processing of {len(loaders)} files with max {max_concurrent} concurrent"
)
start_time = time.time()
# Use semaphore to control concurrency
semaphore = asyncio.Semaphore(max_concurrent)
async def process_with_semaphore(loader: "MistralLoader") -> List[Document]:
async with semaphore:
return await loader.load_async()
# Process all files with controlled concurrency
tasks = [process_with_semaphore(loader) for loader in loaders]
results = await asyncio.gather(*tasks, return_exceptions=True)
# Handle any exceptions in results
processed_results = []
for i, result in enumerate(results):
if isinstance(result, Exception):
log.error(f"File {i} failed: {result}")
processed_results.append(
[
Document(
page_content=f"Error processing file: {result}",
metadata={
"error": "batch_processing_failed",
"file_index": i,
},
)
]
)
else:
processed_results.append(result)
# MONITORING: Log comprehensive batch processing statistics
total_time = time.time() - start_time
total_docs = sum(len(docs) for docs in processed_results)
success_count = sum(
1 for result in results if not isinstance(result, Exception)
)
failure_count = len(results) - success_count
log.info(
f"Batch processing completed in {total_time:.2f}s: "
f"{success_count} files succeeded, {failure_count} files failed, "
f"produced {total_docs} total documents"
)
return processed_results
+57 -14
View File
@@ -1,4 +1,5 @@
import logging
from xml.etree.ElementTree import ParseError
from typing import Any, Dict, Generator, List, Optional, Sequence, Union
from urllib.parse import parse_qs, urlparse
@@ -62,12 +63,17 @@ class YoutubeLoader:
_video_id = _parse_video_id(video_id)
self.video_id = _video_id if _video_id is not None else video_id
self._metadata = {"source": video_id}
self.language = language
self.proxy_url = proxy_url
# Ensure language is a list
if isinstance(language, str):
self.language = [language]
else:
self.language = language
self.language = list(language)
# Add English as fallback if not already in the list
if "en" not in self.language:
self.language.append("en")
def load(self) -> List[Document]:
"""Load YouTube transcripts into `Document` objects."""
@@ -88,7 +94,6 @@ class YoutubeLoader:
"http": self.proxy_url,
"https": self.proxy_url,
}
# Don't log complete URL because it might contain secrets
log.debug(f"Using proxy URL: {self.proxy_url[:14]}...")
else:
youtube_proxies = None
@@ -101,17 +106,55 @@ class YoutubeLoader:
log.exception("Loading YouTube transcript failed")
return []
try:
transcript = transcript_list.find_transcript(self.language)
except NoTranscriptFound:
transcript = transcript_list.find_transcript(["en"])
# Try each language in order of priority
for lang in self.language:
try:
transcript = transcript_list.find_transcript([lang])
if transcript.is_generated:
log.debug(f"Found generated transcript for language '{lang}'")
try:
transcript = transcript_list.find_manually_created_transcript(
[lang]
)
log.debug(f"Found manual transcript for language '{lang}'")
except NoTranscriptFound:
log.debug(
f"No manual transcript found for language '{lang}', using generated"
)
pass
transcript_pieces: List[Dict[str, Any]] = transcript.fetch()
log.debug(f"Found transcript for language '{lang}'")
try:
transcript_pieces: List[Dict[str, Any]] = transcript.fetch()
except ParseError:
log.debug(f"Empty or invalid transcript for language '{lang}'")
continue
transcript = " ".join(
map(
lambda transcript_piece: transcript_piece["text"].strip(" "),
transcript_pieces,
)
if not transcript_pieces:
log.debug(f"Empty transcript for language '{lang}'")
continue
transcript_text = " ".join(
map(
lambda transcript_piece: (
transcript_piece.text.strip(" ")
if hasattr(transcript_piece, "text")
else ""
),
transcript_pieces,
)
)
return [Document(page_content=transcript_text, metadata=self._metadata)]
except NoTranscriptFound:
log.debug(f"No transcript found for language '{lang}'")
continue
except Exception as e:
log.info(f"Error finding transcript for language '{lang}'")
raise e
# If we get here, all languages failed
languages_tried = ", ".join(self.language)
log.warning(
f"No transcript found for any of the specified languages: {languages_tried}. Verify if the video has transcripts, add more languages if needed."
)
return [Document(page_content=transcript, metadata=self._metadata)]
raise NoTranscriptFound(self.video_id, self.language, list(transcript_list))
@@ -0,0 +1,8 @@
from abc import ABC, abstractmethod
from typing import Optional, List, Tuple
class BaseReranker(ABC):
@abstractmethod
def predict(self, sentences: List[Tuple[str, str]]) -> Optional[List[float]]:
pass
@@ -7,11 +7,13 @@ 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:
class ColBERT(BaseReranker):
def __init__(self, name, **kwargs) -> None:
log.info("ColBERT: Loading model", name)
self.device = "cuda" if torch.cuda.is_available() else "cpu"
@@ -0,0 +1,74 @@
import logging
import requests
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.retrieval.models.base_reranker import BaseReranker
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class ExternalReranker(BaseReranker):
def __init__(
self,
api_key: str,
url: str = "http://localhost:8080/v1/rerank",
model: str = "reranker",
):
self.api_key = api_key
self.url = url
self.model = model
def predict(
self, sentences: List[Tuple[str, str]], user=None
) -> Optional[List[float]]:
query = sentences[0][0]
docs = [i[1] for i in sentences]
payload = {
"model": self.model,
"query": query,
"documents": docs,
"top_n": len(docs),
}
try:
log.info(f"ExternalReranker:predict:model {self.model}")
log.info(f"ExternalReranker:predict:query {query}")
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 {}
),
},
json=payload,
)
r.raise_for_status()
data = r.json()
if "results" in data:
sorted_results = sorted(data["results"], key=lambda x: x["index"])
return [result["relevance_score"] for result in sorted_results]
else:
log.error("No results found in external reranking response")
return None
except Exception as e:
log.exception(f"Error in external reranking: {e}")
return None
+350 -173
View File
@@ -5,19 +5,24 @@ from typing import Optional, Union
import requests
import hashlib
from concurrent.futures import ThreadPoolExecutor
import time
from urllib.parse import quote
from huggingface_hub import snapshot_download
from langchain.retrievers import ContextualCompressionRetriever, EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
from langchain_core.documents import Document
from open_webui.config import VECTOR_DB
from open_webui.retrieval.vector.connector import VECTOR_DB_CLIENT
from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT
from open_webui.models.users import UserModel
from open_webui.models.files import Files
from open_webui.models.knowledge import Knowledges
from open_webui.models.notes import Notes
from open_webui.retrieval.vector.main import GetResult
from open_webui.utils.access_control import has_access
from open_webui.env import (
@@ -77,6 +82,7 @@ def query_doc(
collection_name: str, query_embedding: list[float], k: int, user: UserModel = None
):
try:
log.debug(f"query_doc:doc {collection_name}")
result = VECTOR_DB_CLIENT.search(
collection_name=collection_name,
vectors=[query_embedding],
@@ -94,6 +100,7 @@ def query_doc(
def get_doc(collection_name: str, user: UserModel = None):
try:
log.debug(f"get_doc:doc {collection_name}")
result = VECTOR_DB_CLIENT.get(collection_name=collection_name)
if result:
@@ -114,8 +121,10 @@ def query_doc_with_hybrid_search(
reranking_function,
k_reranker: int,
r: float,
hybrid_bm25_weight: float,
) -> dict:
try:
log.debug(f"query_doc_with_hybrid_search:doc {collection_name}")
bm25_retriever = BM25Retriever.from_texts(
texts=collection_result.documents[0],
metadatas=collection_result.metadatas[0],
@@ -128,9 +137,20 @@ def query_doc_with_hybrid_search(
top_k=k,
)
ensemble_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, vector_search_retriever], weights=[0.5, 0.5]
)
if hybrid_bm25_weight <= 0:
ensemble_retriever = EnsembleRetriever(
retrievers=[vector_search_retriever], weights=[1.0]
)
elif hybrid_bm25_weight >= 1:
ensemble_retriever = EnsembleRetriever(
retrievers=[bm25_retriever], weights=[1.0]
)
else:
ensemble_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, vector_search_retriever],
weights=[hybrid_bm25_weight, 1.0 - hybrid_bm25_weight],
)
compressor = RerankCompressor(
embedding_function=embedding_function,
top_n=k_reranker,
@@ -168,6 +188,7 @@ def query_doc_with_hybrid_search(
)
return result
except Exception as e:
log.exception(f"Error querying doc {collection_name} with hybrid search: {e}")
raise e
@@ -203,7 +224,7 @@ def merge_and_sort_query_results(query_results: list[dict], k: int) -> dict:
for distance, document, metadata in zip(distances, documents, metadatas):
if isinstance(document, str):
doc_hash = hashlib.md5(
doc_hash = hashlib.sha256(
document.encode()
).hexdigest() # Compute a hash for uniqueness
@@ -256,22 +277,47 @@ def query_collection(
k: int,
) -> dict:
results = []
for query in queries:
query_embedding = embedding_function(query, prefix=RAG_EMBEDDING_QUERY_PREFIX)
for collection_name in collection_names:
error = False
def process_query_collection(collection_name, query_embedding):
try:
if collection_name:
try:
result = query_doc(
collection_name=collection_name,
k=k,
query_embedding=query_embedding,
)
if result is not None:
results.append(result.model_dump())
except Exception as e:
log.exception(f"Error when querying the collection: {e}")
else:
pass
result = query_doc(
collection_name=collection_name,
k=k,
query_embedding=query_embedding,
)
if result is not None:
return result.model_dump(), None
return None, None
except Exception as e:
log.exception(f"Error when querying the collection: {e}")
return None, e
# Generate all query embeddings (in one call)
query_embeddings = embedding_function(queries, prefix=RAG_EMBEDDING_QUERY_PREFIX)
log.debug(
f"query_collection: processing {len(queries)} queries across {len(collection_names)} collections"
)
with ThreadPoolExecutor() as executor:
future_results = []
for query_embedding in query_embeddings:
for collection_name in collection_names:
result = executor.submit(
process_query_collection, collection_name, query_embedding
)
future_results.append(result)
task_results = [future.result() for future in future_results]
for result, err in task_results:
if err is not None:
error = True
elif result is not None:
results.append(result)
if error and not results:
log.warning("All collection queries failed. No results returned.")
return merge_and_sort_query_results(results, k=k)
@@ -284,6 +330,7 @@ def query_collection_with_hybrid_search(
reranking_function,
k_reranker: int,
r: float,
hybrid_bm25_weight: float,
) -> dict:
results = []
error = False
@@ -292,6 +339,9 @@ def query_collection_with_hybrid_search(
collection_results = {}
for collection_name in collection_names:
try:
log.debug(
f"query_collection_with_hybrid_search:VECTOR_DB_CLIENT.get:collection {collection_name}"
)
collection_results[collection_name] = VECTOR_DB_CLIENT.get(
collection_name=collection_name
)
@@ -314,6 +364,7 @@ def query_collection_with_hybrid_search(
reranking_function=reranking_function,
k_reranker=k_reranker,
r=r,
hybrid_bm25_weight=hybrid_bm25_weight,
)
return result, None
except Exception as e:
@@ -354,12 +405,13 @@ def get_embedding_function(
url,
key,
embedding_batch_size,
azure_api_version=None,
):
if embedding_engine == "":
return lambda query, prefix=None, user=None: embedding_function.encode(
query, **({"prompt": prefix} if prefix else {})
).tolist()
elif embedding_engine in ["ollama", "openai"]:
elif embedding_engine in ["ollama", "openai", "azure_openai"]:
func = lambda query, prefix=None, user=None: generate_embeddings(
engine=embedding_engine,
model=embedding_model,
@@ -368,6 +420,7 @@ def get_embedding_function(
url=url,
key=key,
user=user,
azure_api_version=azure_api_version,
)
def generate_multiple(query, prefix, user, func):
@@ -392,169 +445,236 @@ def get_embedding_function(
raise ValueError(f"Unknown embedding engine: {embedding_engine}")
def get_sources_from_files(
def get_reranking_function(reranking_engine, reranking_model, reranking_function):
if reranking_function is None:
return None
if reranking_engine == "external":
return lambda sentences, user=None: reranking_function.predict(
sentences, user=user
)
else:
return lambda sentences, user=None: reranking_function.predict(sentences)
def get_sources_from_items(
request,
files,
items,
queries,
embedding_function,
k,
reranking_function,
k_reranker,
r,
hybrid_bm25_weight,
hybrid_search,
full_context=False,
user: Optional[UserModel] = None,
):
log.debug(
f"files: {files} {queries} {embedding_function} {reranking_function} {full_context}"
f"items: {items} {queries} {embedding_function} {reranking_function} {full_context}"
)
extracted_collections = []
relevant_contexts = []
query_results = []
for file in files:
for item in items:
query_result = None
collection_names = []
context = None
if file.get("docs"):
# BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL
context = {
"documents": [[doc.get("content") for doc in file.get("docs")]],
"metadatas": [[doc.get("metadata") for doc in file.get("docs")]],
}
elif file.get("context") == "full":
# Manual Full Mode Toggle
context = {
"documents": [[file.get("file").get("data", {}).get("content")]],
"metadatas": [[{"file_id": file.get("id"), "name": file.get("name")}]],
}
elif (
file.get("type") != "web_search"
and request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL
):
# BYPASS_EMBEDDING_AND_RETRIEVAL
if file.get("type") == "collection":
file_ids = file.get("data", {}).get("file_ids", [])
if item.get("type") == "text":
# Raw Text
# Used during temporary chat file uploads
documents = []
metadatas = []
for file_id in file_ids:
file_object = Files.get_file_by_id(file_id)
if file_object:
documents.append(file_object.data.get("content", ""))
metadatas.append(
{
"file_id": file_id,
"name": file_object.filename,
"source": file_object.filename,
}
)
context = {
"documents": [documents],
"metadatas": [metadatas],
if item.get("file"):
# if item has file data, use it
query_result = {
"documents": [
[item.get("file", {}).get("data", {}).get("content")]
],
"metadatas": [
[item.get("file", {}).get("data", {}).get("meta", {})]
],
}
else:
# Fallback to item content
query_result = {
"documents": [[item.get("content")]],
"metadatas": [
[{"file_id": item.get("id"), "name": item.get("name")}]
],
}
elif file.get("id"):
file_object = Files.get_file_by_id(file.get("id"))
if file_object:
context = {
"documents": [[file_object.data.get("content", "")]],
elif item.get("type") == "note":
# Note Attached
note = Notes.get_note_by_id(item.get("id"))
if user.role == "admin" or has_access(user.id, "read", note.access_control):
# User has access to the note
query_result = {
"documents": [[note.data.get("content", {}).get("md", "")]],
"metadatas": [[{"file_id": note.id, "name": note.title}]],
}
elif item.get("type") == "file":
if (
item.get("context") == "full"
or request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL
):
if item.get("file", {}).get("data", {}).get("content", ""):
# Manual Full Mode Toggle
# Used from chat file modal, we can assume that the file content will be available from item.get("file").get("data", {}).get("content")
query_result = {
"documents": [
[item.get("file", {}).get("data", {}).get("content", "")]
],
"metadatas": [
[
{
"file_id": file.get("id"),
"name": file_object.filename,
"source": file_object.filename,
"file_id": item.get("id"),
"name": item.get("name"),
**item.get("file")
.get("data", {})
.get("metadata", {}),
}
]
],
}
elif file.get("file").get("data"):
context = {
"documents": [[file.get("file").get("data", {}).get("content")]],
"metadatas": [
[file.get("file").get("data", {}).get("metadata", {})]
],
}
else:
collection_names = []
if file.get("type") == "collection":
if file.get("legacy"):
collection_names = file.get("collection_names", [])
elif item.get("id"):
file_object = Files.get_file_by_id(item.get("id"))
if file_object:
query_result = {
"documents": [[file_object.data.get("content", "")]],
"metadatas": [
[
{
"file_id": item.get("id"),
"name": file_object.filename,
"source": file_object.filename,
}
]
],
}
else:
# Fallback to collection names
if item.get("legacy"):
collection_names.append(f"{item['id']}")
else:
collection_names.append(file["id"])
elif file.get("collection_name"):
collection_names.append(file["collection_name"])
elif file.get("id"):
if file.get("legacy"):
collection_names.append(f"{file['id']}")
else:
collection_names.append(f"file-{file['id']}")
collection_names.append(f"file-{item['id']}")
elif item.get("type") == "collection":
if (
item.get("context") == "full"
or request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL
):
# Manual Full Mode Toggle for Collection
knowledge_base = Knowledges.get_knowledge_by_id(item.get("id"))
if knowledge_base and (
user.role == "admin"
or has_access(user.id, "read", knowledge_base.access_control)
):
file_ids = knowledge_base.data.get("file_ids", [])
documents = []
metadatas = []
for file_id in file_ids:
file_object = Files.get_file_by_id(file_id)
if file_object:
documents.append(file_object.data.get("content", ""))
metadatas.append(
{
"file_id": file_id,
"name": file_object.filename,
"source": file_object.filename,
}
)
query_result = {
"documents": [documents],
"metadatas": [metadatas],
}
else:
# Fallback to collection names
if item.get("legacy"):
collection_names = item.get("collection_names", [])
else:
collection_names.append(item["id"])
elif item.get("docs"):
# BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL
query_result = {
"documents": [[doc.get("content") for doc in item.get("docs")]],
"metadatas": [[doc.get("metadata") for doc in item.get("docs")]],
}
elif item.get("collection_name"):
# Direct Collection Name
collection_names.append(item["collection_name"])
elif item.get("collection_names"):
# Collection Names List
collection_names.extend(item["collection_names"])
# If query_result is None
# Fallback to collection names and vector search the collections
if query_result is None and collection_names:
collection_names = set(collection_names).difference(extracted_collections)
if not collection_names:
log.debug(f"skipping {file} as it has already been extracted")
log.debug(f"skipping {item} as it has already been extracted")
continue
if full_context:
try:
context = get_all_items_from_collections(collection_names)
except Exception as e:
log.exception(e)
else:
try:
context = None
if file.get("type") == "text":
context = file["content"]
else:
if hybrid_search:
try:
context = query_collection_with_hybrid_search(
collection_names=collection_names,
queries=queries,
embedding_function=embedding_function,
k=k,
reranking_function=reranking_function,
k_reranker=k_reranker,
r=r,
)
except Exception as e:
log.debug(
"Error when using hybrid search, using"
" non hybrid search as fallback."
)
if (not hybrid_search) or (context is None):
context = query_collection(
try:
if full_context:
query_result = get_all_items_from_collections(collection_names)
else:
query_result = None # Initialize to None
if hybrid_search:
try:
query_result = query_collection_with_hybrid_search(
collection_names=collection_names,
queries=queries,
embedding_function=embedding_function,
k=k,
reranking_function=reranking_function,
k_reranker=k_reranker,
r=r,
hybrid_bm25_weight=hybrid_bm25_weight,
)
except Exception as e:
log.exception(e)
except Exception as e:
log.debug(
"Error when using hybrid search, using non hybrid search as fallback."
)
# fallback to non-hybrid search
if not hybrid_search and query_result is None:
query_result = query_collection(
collection_names=collection_names,
queries=queries,
embedding_function=embedding_function,
k=k,
)
except Exception as e:
log.exception(e)
extracted_collections.extend(collection_names)
if context:
if "data" in file:
del file["data"]
relevant_contexts.append({**context, "file": file})
if query_result:
if "data" in item:
del item["data"]
query_results.append({**query_result, "file": item})
sources = []
for context in relevant_contexts:
for query_result in query_results:
try:
if "documents" in context:
if "metadatas" in context:
if "documents" in query_result:
if "metadatas" in query_result:
source = {
"source": context["file"],
"document": context["documents"][0],
"metadata": context["metadatas"][0],
"source": query_result["file"],
"document": query_result["documents"][0],
"metadata": query_result["metadatas"][0],
}
if "distances" in context and context["distances"]:
source["distances"] = context["distances"][0]
if "distances" in query_result and query_result["distances"]:
source["distances"] = query_result["distances"][0]
sources.append(source)
except Exception as e:
@@ -613,6 +733,9 @@ def generate_openai_batch_embeddings(
user: UserModel = None,
) -> Optional[list[list[float]]]:
try:
log.debug(
f"generate_openai_batch_embeddings:model {model} batch size: {len(texts)}"
)
json_data = {"input": texts, "model": model}
if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str):
json_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix
@@ -624,7 +747,7 @@ def generate_openai_batch_embeddings(
"Authorization": f"Bearer {key}",
**(
{
"X-OpenWebUI-User-Name": user.name,
"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,
@@ -646,6 +769,60 @@ def generate_openai_batch_embeddings(
return None
def generate_azure_openai_batch_embeddings(
model: str,
texts: list[str],
url: str,
key: str = "",
version: str = "",
prefix: str = None,
user: UserModel = None,
) -> Optional[list[list[float]]]:
try:
log.debug(
f"generate_azure_openai_batch_embeddings:deployment {model} batch size: {len(texts)}"
)
json_data = {"input": texts}
if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str):
json_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix
url = f"{url}/openai/deployments/{model}/embeddings?api-version={version}"
for _ in range(5):
r = requests.post(
url,
headers={
"Content-Type": "application/json",
"api-key": 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 {}
),
},
json=json_data,
)
if r.status_code == 429:
retry = float(r.headers.get("Retry-After", "1"))
time.sleep(retry)
continue
r.raise_for_status()
data = r.json()
if "data" in data:
return [elem["embedding"] for elem in data["data"]]
else:
raise Exception("Something went wrong :/")
return None
except Exception as e:
log.exception(f"Error generating azure openai batch embeddings: {e}")
return None
def generate_ollama_batch_embeddings(
model: str,
texts: list[str],
@@ -655,6 +832,9 @@ def generate_ollama_batch_embeddings(
user: UserModel = None,
) -> Optional[list[list[float]]]:
try:
log.debug(
f"generate_ollama_batch_embeddings:model {model} batch size: {len(texts)}"
)
json_data = {"input": texts, "model": model}
if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str):
json_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix
@@ -666,7 +846,7 @@ def generate_ollama_batch_embeddings(
"Authorization": f"Bearer {key}",
**(
{
"X-OpenWebUI-User-Name": user.name,
"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,
@@ -707,38 +887,33 @@ def generate_embeddings(
text = f"{prefix}{text}"
if engine == "ollama":
if isinstance(text, list):
embeddings = generate_ollama_batch_embeddings(
**{
"model": model,
"texts": text,
"url": url,
"key": key,
"prefix": prefix,
"user": user,
}
)
else:
embeddings = generate_ollama_batch_embeddings(
**{
"model": model,
"texts": [text],
"url": url,
"key": key,
"prefix": prefix,
"user": user,
}
)
embeddings = generate_ollama_batch_embeddings(
**{
"model": model,
"texts": text if isinstance(text, list) else [text],
"url": url,
"key": key,
"prefix": prefix,
"user": user,
}
)
return embeddings[0] if isinstance(text, str) else embeddings
elif engine == "openai":
if isinstance(text, list):
embeddings = generate_openai_batch_embeddings(
model, text, url, key, prefix, user
)
else:
embeddings = generate_openai_batch_embeddings(
model, [text], url, key, prefix, user
)
embeddings = generate_openai_batch_embeddings(
model, text if isinstance(text, list) else [text], url, key, prefix, user
)
return embeddings[0] if isinstance(text, str) else embeddings
elif engine == "azure_openai":
azure_api_version = kwargs.get("azure_api_version", "")
embeddings = generate_azure_openai_batch_embeddings(
model,
text if isinstance(text, list) else [text],
url,
key,
azure_api_version,
prefix,
user,
)
return embeddings[0] if isinstance(text, str) else embeddings
@@ -768,7 +943,7 @@ class RerankCompressor(BaseDocumentCompressor):
reranking = self.reranking_function is not None
if reranking:
scores = self.reranking_function.predict(
scores = self.reranking_function(
[(query, doc.page_content) for doc in documents]
)
else:
@@ -780,7 +955,9 @@ class RerankCompressor(BaseDocumentCompressor):
)
scores = util.cos_sim(query_embedding, document_embedding)[0]
docs_with_scores = list(zip(documents, scores.tolist()))
docs_with_scores = list(
zip(documents, scores.tolist() if not isinstance(scores, list) else scores)
)
if self.r_score:
docs_with_scores = [
(d, s) for d, s in docs_with_scores if s >= self.r_score
@@ -1,26 +0,0 @@
from open_webui.config import VECTOR_DB
if VECTOR_DB == "milvus":
from open_webui.retrieval.vector.dbs.milvus import MilvusClient
VECTOR_DB_CLIENT = MilvusClient()
elif VECTOR_DB == "qdrant":
from open_webui.retrieval.vector.dbs.qdrant import QdrantClient
VECTOR_DB_CLIENT = QdrantClient()
elif VECTOR_DB == "opensearch":
from open_webui.retrieval.vector.dbs.opensearch import OpenSearchClient
VECTOR_DB_CLIENT = OpenSearchClient()
elif VECTOR_DB == "pgvector":
from open_webui.retrieval.vector.dbs.pgvector import PgvectorClient
VECTOR_DB_CLIENT = PgvectorClient()
elif VECTOR_DB == "elasticsearch":
from open_webui.retrieval.vector.dbs.elasticsearch import ElasticsearchClient
VECTOR_DB_CLIENT = ElasticsearchClient()
else:
from open_webui.retrieval.vector.dbs.chroma import ChromaClient
VECTOR_DB_CLIENT = ChromaClient()
@@ -5,7 +5,12 @@ from chromadb.utils.batch_utils import create_batches
from typing import Optional
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
from open_webui.retrieval.vector.main import (
VectorDBBase,
VectorItem,
SearchResult,
GetResult,
)
from open_webui.config import (
CHROMA_DATA_PATH,
CHROMA_HTTP_HOST,
@@ -23,7 +28,7 @@ log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class ChromaClient:
class ChromaClient(VectorDBBase):
def __init__(self):
settings_dict = {
"allow_reset": True,
@@ -2,7 +2,12 @@ from elasticsearch import Elasticsearch, BadRequestError
from typing import Optional
import ssl
from elasticsearch.helpers import bulk, scan
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
from open_webui.retrieval.vector.main import (
VectorDBBase,
VectorItem,
SearchResult,
GetResult,
)
from open_webui.config import (
ELASTICSEARCH_URL,
ELASTICSEARCH_CA_CERTS,
@@ -15,7 +20,7 @@ from open_webui.config import (
)
class ElasticsearchClient:
class ElasticsearchClient(VectorDBBase):
"""
Important:
in order to reduce the number of indexes and since the embedding vector length is fixed, we avoid creating
+153 -40
View File
@@ -3,12 +3,21 @@ from pymilvus import FieldSchema, DataType
import json
import logging
from typing import Optional
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
from open_webui.retrieval.vector.main import (
VectorDBBase,
VectorItem,
SearchResult,
GetResult,
)
from open_webui.config import (
MILVUS_URI,
MILVUS_DB,
MILVUS_TOKEN,
MILVUS_INDEX_TYPE,
MILVUS_METRIC_TYPE,
MILVUS_HNSW_M,
MILVUS_HNSW_EFCONSTRUCTION,
MILVUS_IVF_FLAT_NLIST,
)
from open_webui.env import SRC_LOG_LEVELS
@@ -16,7 +25,7 @@ log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class MilvusClient:
class MilvusClient(VectorDBBase):
def __init__(self):
self.collection_prefix = "open_webui"
if MILVUS_TOKEN is None:
@@ -28,7 +37,6 @@ class MilvusClient:
ids = []
documents = []
metadatas = []
for match in result:
_ids = []
_documents = []
@@ -37,11 +45,9 @@ class MilvusClient:
_ids.append(item.get("id"))
_documents.append(item.get("data", {}).get("text"))
_metadatas.append(item.get("metadata"))
ids.append(_ids)
documents.append(_documents)
metadatas.append(_metadatas)
return GetResult(
**{
"ids": ids,
@@ -55,13 +61,11 @@ class MilvusClient:
distances = []
documents = []
metadatas = []
for match in result:
_ids = []
_distances = []
_documents = []
_metadatas = []
for item in match:
_ids.append(item.get("id"))
# normalize milvus score from [-1, 1] to [0, 1] range
@@ -70,12 +74,10 @@ class MilvusClient:
_distances.append(_dist)
_documents.append(item.get("entity", {}).get("data", {}).get("text"))
_metadatas.append(item.get("entity", {}).get("metadata"))
ids.append(_ids)
distances.append(_distances)
documents.append(_documents)
metadatas.append(_metadatas)
return SearchResult(
**{
"ids": ids,
@@ -108,11 +110,39 @@ class MilvusClient:
)
index_params = self.client.prepare_index_params()
# Use configurations from config.py
index_type = MILVUS_INDEX_TYPE.upper()
metric_type = MILVUS_METRIC_TYPE.upper()
log.info(f"Using Milvus index type: {index_type}, metric type: {metric_type}")
index_creation_params = {}
if index_type == "HNSW":
index_creation_params = {
"M": MILVUS_HNSW_M,
"efConstruction": MILVUS_HNSW_EFCONSTRUCTION,
}
log.info(f"HNSW params: {index_creation_params}")
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 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"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.
# If Milvus errors out, the user needs to correct the MILVUS_INDEX_TYPE env var.
index_params.add_index(
field_name="vector",
index_type="HNSW",
metric_type="COSINE",
params={"M": 16, "efConstruction": 100},
index_type=index_type,
metric_type=metric_type,
params=index_creation_params,
)
self.client.create_collection(
@@ -120,6 +150,9 @@ class MilvusClient:
schema=schema,
index_params=index_params,
)
log.info(
f"Successfully created collection '{self.collection_prefix}_{collection_name}' with index type '{index_type}' and metric '{metric_type}'."
)
def has_collection(self, collection_name: str) -> bool:
# Check if the collection exists based on the collection name.
@@ -140,84 +173,113 @@ class MilvusClient:
) -> Optional[SearchResult]:
# Search for the nearest neighbor items based on the vectors and return 'limit' number of results.
collection_name = collection_name.replace("-", "_")
# For some index types like IVF_FLAT, search params like nprobe can be set.
# Example: search_params = {"nprobe": 10} if using IVF_FLAT
# For simplicity, not adding configurable search_params here, but could be extended.
result = self.client.search(
collection_name=f"{self.collection_prefix}_{collection_name}",
data=vectors,
limit=limit,
output_fields=["data", "metadata"],
# search_params=search_params # Potentially add later if needed
)
return self._result_to_search_result(result)
def query(self, collection_name: str, filter: dict, limit: Optional[int] = None):
# 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()
]
)
max_limit = 16383 # The maximum number of records per request
all_results = []
if limit is None:
limit = float("inf") # Use infinity as a placeholder for no limit
# Milvus default limit for query if not specified is 16384, but docs mention iteration.
# Let's set a practical high number if "all" is intended, or handle true pagination.
# For now, if limit is None, we'll fetch in batches up to a very large number.
# This part could be refined based on expected use cases for "get all".
# For this function signature, None implies "as many as possible" up to Milvus limits.
limit = (
16384 * 10
) # A large number to signify fetching many, will be capped by actual data or max_limit per call.
log.info(
f"Limit not specified for query, fetching up to {limit} results in batches."
)
# Initialize offset and remaining to handle pagination
offset = 0
remaining = limit
try:
log.info(
f"Querying collection {self.collection_prefix}_{collection_name} with filter: '{filter_string}', limit: {limit}"
)
# Loop until there are no more items to fetch or the desired limit is reached
while remaining > 0:
log.info(f"remaining: {remaining}")
current_fetch = min(
max_limit, remaining
) # Determine how many items to fetch in this iteration
max_limit, remaining if isinstance(remaining, int) else max_limit
)
log.debug(
f"Querying with offset: {offset}, current_fetch: {current_fetch}"
)
results = self.client.query(
collection_name=f"{self.collection_prefix}_{collection_name}",
filter=filter_string,
output_fields=["*"],
output_fields=[
"id",
"data",
"metadata",
], # Explicitly list needed fields. Vector not usually needed in query.
limit=current_fetch,
offset=offset,
)
if not results:
log.debug("No more results from query.")
break
all_results.extend(results)
results_count = len(results)
remaining -= (
results_count # Decrease remaining by the number of items fetched
)
log.debug(f"Fetched {results_count} results in this batch.")
if isinstance(remaining, int):
remaining -= results_count
offset += results_count
# Break the loop if the results returned are less than the requested fetch count
# Break the loop if the results returned are less than the requested fetch count (means end of data)
if results_count < current_fetch:
log.debug(
"Fetched less than requested, assuming end of results for this query."
)
break
log.debug(all_results)
log.info(f"Total results from query: {len(all_results)}")
return self._result_to_get_result([all_results])
except Exception as e:
log.exception(
f"Error querying collection {collection_name} with limit {limit}: {e}"
f"Error querying collection {self.collection_prefix}_{collection_name} with filter '{filter_string}' and limit {limit}: {e}"
)
return None
def get(self, collection_name: str) -> Optional[GetResult]:
# Get all the items in the collection.
# Get all the items in the collection. This can be very resource-intensive for large collections.
collection_name = collection_name.replace("-", "_")
result = self.client.query(
collection_name=f"{self.collection_prefix}_{collection_name}",
filter='id != ""',
log.warning(
f"Fetching ALL items from collection '{self.collection_prefix}_{collection_name}'. This might be slow for large collections."
)
return self._result_to_get_result([result])
# 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)
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.
@@ -225,10 +287,23 @@ class MilvusClient:
if not self.client.has_collection(
collection_name=f"{self.collection_prefix}_{collection_name}"
):
log.info(
f"Collection {self.collection_prefix}_{collection_name} does not exist. Creating now."
)
if not items:
log.error(
f"Cannot create collection {self.collection_prefix}_{collection_name} without items to determine dimension."
)
raise ValueError(
"Cannot create Milvus collection without items to determine vector dimension."
)
self._create_collection(
collection_name=collection_name, dimension=len(items[0]["vector"])
)
log.info(
f"Inserting {len(items)} items into collection {self.collection_prefix}_{collection_name}."
)
return self.client.insert(
collection_name=f"{self.collection_prefix}_{collection_name}",
data=[
@@ -248,10 +323,23 @@ class MilvusClient:
if not self.client.has_collection(
collection_name=f"{self.collection_prefix}_{collection_name}"
):
log.info(
f"Collection {self.collection_prefix}_{collection_name} does not exist for upsert. Creating now."
)
if not items:
log.error(
f"Cannot create collection {self.collection_prefix}_{collection_name} for upsert without items to determine dimension."
)
raise ValueError(
"Cannot create Milvus collection for upsert without items to determine vector dimension."
)
self._create_collection(
collection_name=collection_name, dimension=len(items[0]["vector"])
)
log.info(
f"Upserting {len(items)} items into collection {self.collection_prefix}_{collection_name}."
)
return self.client.upsert(
collection_name=f"{self.collection_prefix}_{collection_name}",
data=[
@@ -271,30 +359,55 @@ class MilvusClient:
ids: Optional[list[str]] = None,
filter: Optional[dict] = None,
):
# Delete the items from the collection based on the ids.
# Delete the items from the collection based on the ids or filter.
collection_name = collection_name.replace("-", "_")
if not self.has_collection(collection_name):
log.warning(
f"Delete attempted on non-existent collection: {self.collection_prefix}_{collection_name}"
)
return None
if ids:
log.info(
f"Deleting items by IDs from {self.collection_prefix}_{collection_name}. IDs: {ids}"
)
return self.client.delete(
collection_name=f"{self.collection_prefix}_{collection_name}",
ids=ids,
)
elif filter:
# Convert the filter dictionary to a string using JSON_CONTAINS.
filter_string = " && ".join(
[
f'metadata["{key}"] == {json.dumps(value)}'
for key, value in filter.items()
]
)
log.info(
f"Deleting items by filter from {self.collection_prefix}_{collection_name}. Filter: {filter_string}"
)
return self.client.delete(
collection_name=f"{self.collection_prefix}_{collection_name}",
filter=filter_string,
)
else:
log.warning(
f"Delete operation on {self.collection_prefix}_{collection_name} called without IDs or filter. No action taken."
)
return None
def reset(self):
# Resets the database. This will delete all collections and item entries.
# Resets the database. This will delete all collections and item entries that match the prefix.
log.warning(
f"Resetting Milvus: Deleting all collections with prefix '{self.collection_prefix}'."
)
collection_names = self.client.list_collections()
for collection_name in collection_names:
if collection_name.startswith(self.collection_prefix):
self.client.drop_collection(collection_name=collection_name)
deleted_collections = []
for collection_name_full in collection_names:
if collection_name_full.startswith(self.collection_prefix):
try:
self.client.drop_collection(collection_name=collection_name_full)
deleted_collections.append(collection_name_full)
log.info(f"Deleted collection: {collection_name_full}")
except Exception as e:
log.error(f"Error deleting collection {collection_name_full}: {e}")
log.info(f"Milvus reset complete. Deleted collections: {deleted_collections}")
@@ -2,7 +2,12 @@ from opensearchpy import OpenSearch
from opensearchpy.helpers import bulk
from typing import Optional
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
from open_webui.retrieval.vector.main import (
VectorDBBase,
VectorItem,
SearchResult,
GetResult,
)
from open_webui.config import (
OPENSEARCH_URI,
OPENSEARCH_SSL,
@@ -12,7 +17,7 @@ from open_webui.config import (
)
class OpenSearchClient:
class OpenSearchClient(VectorDBBase):
def __init__(self):
self.index_prefix = "open_webui"
self.client = OpenSearch(
@@ -152,10 +157,10 @@ class OpenSearchClient:
for field, value in filter.items():
query_body["query"]["bool"]["filter"].append(
{"match": {"metadata." + str(field): value}}
{"term": {"metadata." + str(field) + ".keyword": value}}
)
size = limit if limit else 10
size = limit if limit else 10000
try:
result = self.client.search(
@@ -201,6 +206,7 @@ class OpenSearchClient:
for item in batch
]
bulk(self.client, actions)
self.client.indices.refresh(self._get_index_name(collection_name))
def upsert(self, collection_name: str, items: list[VectorItem]):
self._create_index_if_not_exists(
@@ -223,6 +229,7 @@ class OpenSearchClient:
for item in batch
]
bulk(self.client, actions)
self.client.indices.refresh(self._get_index_name(collection_name))
def delete(
self,
@@ -246,11 +253,12 @@ class OpenSearchClient:
}
for field, value in filter.items():
query_body["query"]["bool"]["filter"].append(
{"match": {"metadata." + str(field): value}}
{"term": {"metadata." + str(field) + ".keyword": value}}
)
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))
def reset(self):
indices = self.client.indices.get(index=f"{self.index_prefix}_*")
@@ -1,12 +1,16 @@
from typing import Optional, List, Dict, Any
import logging
import json
from sqlalchemy import (
func,
literal,
cast,
column,
create_engine,
Column,
Integer,
MetaData,
LargeBinary,
select,
text,
Text,
@@ -14,7 +18,7 @@ from sqlalchemy import (
values,
)
from sqlalchemy.sql import true
from sqlalchemy.pool import NullPool
from sqlalchemy.pool import NullPool, QueuePool
from sqlalchemy.orm import declarative_base, scoped_session, sessionmaker
from sqlalchemy.dialects.postgresql import JSONB, array
@@ -22,8 +26,22 @@ from pgvector.sqlalchemy import Vector
from sqlalchemy.ext.mutable import MutableDict
from sqlalchemy.exc import NoSuchTableError
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
from open_webui.config import PGVECTOR_DB_URL, PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH
from open_webui.retrieval.vector.main import (
VectorDBBase,
VectorItem,
SearchResult,
GetResult,
)
from open_webui.config import (
PGVECTOR_DB_URL,
PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH,
PGVECTOR_PGCRYPTO,
PGVECTOR_PGCRYPTO_KEY,
PGVECTOR_POOL_SIZE,
PGVECTOR_POOL_MAX_OVERFLOW,
PGVECTOR_POOL_TIMEOUT,
PGVECTOR_POOL_RECYCLE,
)
from open_webui.env import SRC_LOG_LEVELS
@@ -34,17 +52,30 @@ log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def pgcrypto_encrypt(val, key):
return func.pgp_sym_encrypt(val, literal(key))
def pgcrypto_decrypt(col, key, outtype="text"):
return func.cast(func.pgp_sym_decrypt(col, literal(key)), outtype)
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)
if PGVECTOR_PGCRYPTO:
text = Column(LargeBinary, nullable=True)
vmetadata = Column(LargeBinary, nullable=True)
else:
text = Column(Text, nullable=True)
vmetadata = Column(MutableDict.as_mutable(JSONB), nullable=True)
class PgvectorClient:
class PgvectorClient(VectorDBBase):
def __init__(self) -> None:
# if no pgvector uri, use the existing database connection
@@ -53,9 +84,24 @@ class PgvectorClient:
self.session = Session
else:
engine = create_engine(
PGVECTOR_DB_URL, pool_pre_ping=True, poolclass=NullPool
)
if isinstance(PGVECTOR_POOL_SIZE, int):
if PGVECTOR_POOL_SIZE > 0:
engine = create_engine(
PGVECTOR_DB_URL,
pool_size=PGVECTOR_POOL_SIZE,
max_overflow=PGVECTOR_POOL_MAX_OVERFLOW,
pool_timeout=PGVECTOR_POOL_TIMEOUT,
pool_recycle=PGVECTOR_POOL_RECYCLE,
pool_pre_ping=True,
poolclass=QueuePool,
)
else:
engine = create_engine(
PGVECTOR_DB_URL, pool_pre_ping=True, poolclass=NullPool
)
else:
engine = create_engine(PGVECTOR_DB_URL, pool_pre_ping=True)
SessionLocal = sessionmaker(
autocommit=False, autoflush=False, bind=engine, expire_on_commit=False
)
@@ -65,6 +111,15 @@ class PgvectorClient:
# Ensure the pgvector extension is available
self.session.execute(text("CREATE EXTENSION IF NOT EXISTS vector;"))
if PGVECTOR_PGCRYPTO:
# Ensure the pgcrypto extension is available for encryption
self.session.execute(text("CREATE EXTENSION IF NOT EXISTS pgcrypto;"))
if not PGVECTOR_PGCRYPTO_KEY:
raise ValueError(
"PGVECTOR_PGCRYPTO_KEY must be set when PGVECTOR_PGCRYPTO is enabled."
)
# Check vector length consistency
self.check_vector_length()
@@ -136,51 +191,45 @@ class PgvectorClient:
# Pad the vector with zeros
vector += [0.0] * (VECTOR_LENGTH - current_length)
elif current_length > VECTOR_LENGTH:
raise Exception(
f"Vector length {current_length} not supported. Max length must be <= {VECTOR_LENGTH}"
)
# Truncate the vector to 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=item["metadata"],
)
new_items.append(new_chunk)
self.session.bulk_save_objects(new_items)
self.session.commit()
log.info(
f"Inserted {len(new_items)} items into collection '{collection_name}'."
)
except Exception as e:
self.session.rollback()
log.exception(f"Error during insert: {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 = item["metadata"]
existing.collection_name = (
collection_name # Update collection_name if necessary
if PGVECTOR_PGCRYPTO:
for item in items:
vector = self.adjust_vector_length(item["vector"])
# Use raw SQL for BYTEA/pgcrypto
self.session.execute(
text(
"""
INSERT INTO document_chunk
(id, vector, collection_name, text, vmetadata)
VALUES (
:id, :vector, :collection_name,
pgp_sym_encrypt(:text, :key),
pgp_sym_encrypt(:metadata::text, :key)
)
ON CONFLICT (id) DO NOTHING
"""
),
{
"id": item["id"],
"vector": vector,
"collection_name": collection_name,
"text": item["text"],
"metadata": json.dumps(item["metadata"]),
"key": PGVECTOR_PGCRYPTO_KEY,
},
)
else:
self.session.commit()
log.info(f"Encrypted & inserted {len(items)} into '{collection_name}'")
else:
new_items = []
for item in items:
vector = self.adjust_vector_length(item["vector"])
new_chunk = DocumentChunk(
id=item["id"],
vector=vector,
@@ -188,11 +237,78 @@ class PgvectorClient:
text=item["text"],
vmetadata=item["metadata"],
)
self.session.add(new_chunk)
self.session.commit()
log.info(
f"Upserted {len(items)} items into collection '{collection_name}'."
)
new_items.append(new_chunk)
self.session.bulk_save_objects(new_items)
self.session.commit()
log.info(
f"Inserted {len(new_items)} items into collection '{collection_name}'."
)
except Exception as e:
self.session.rollback()
log.exception(f"Error during insert: {e}")
raise
def upsert(self, collection_name: str, items: List[VectorItem]) -> None:
try:
if PGVECTOR_PGCRYPTO:
for item in items:
vector = self.adjust_vector_length(item["vector"])
self.session.execute(
text(
"""
INSERT INTO document_chunk
(id, vector, collection_name, text, vmetadata)
VALUES (
:id, :vector, :collection_name,
pgp_sym_encrypt(:text, :key),
pgp_sym_encrypt(:metadata::text, :key)
)
ON CONFLICT (id) DO UPDATE SET
vector = EXCLUDED.vector,
collection_name = EXCLUDED.collection_name,
text = EXCLUDED.text,
vmetadata = EXCLUDED.vmetadata
"""
),
{
"id": item["id"],
"vector": vector,
"collection_name": collection_name,
"text": item["text"],
"metadata": json.dumps(item["metadata"]),
"key": PGVECTOR_PGCRYPTO_KEY,
},
)
self.session.commit()
log.info(f"Encrypted & upserted {len(items)} into '{collection_name}'")
else:
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 = item["metadata"]
existing.collection_name = (
collection_name # Update collection_name if necessary
)
else:
new_chunk = DocumentChunk(
id=item["id"],
vector=vector,
collection_name=collection_name,
text=item["text"],
vmetadata=item["metadata"],
)
self.session.add(new_chunk)
self.session.commit()
log.info(
f"Upserted {len(items)} items into collection '{collection_name}'."
)
except Exception as e:
self.session.rollback()
log.exception(f"Error during upsert: {e}")
@@ -226,16 +342,32 @@ class PgvectorClient:
.alias("query_vectors")
)
result_fields = [
DocumentChunk.id,
]
if PGVECTOR_PGCRYPTO:
result_fields.append(
pgcrypto_decrypt(
DocumentChunk.text, PGVECTOR_PGCRYPTO_KEY, Text
).label("text")
)
result_fields.append(
pgcrypto_decrypt(
DocumentChunk.vmetadata, PGVECTOR_PGCRYPTO_KEY, JSONB
).label("vmetadata")
)
else:
result_fields.append(DocumentChunk.text)
result_fields.append(DocumentChunk.vmetadata)
result_fields.append(
(DocumentChunk.vector.cosine_distance(query_vectors.c.q_vector)).label(
"distance"
)
)
# Build the lateral subquery for each query vector
subq = (
select(
DocumentChunk.id,
DocumentChunk.text,
DocumentChunk.vmetadata,
(
DocumentChunk.vector.cosine_distance(query_vectors.c.q_vector)
).label("distance"),
)
select(*result_fields)
.where(DocumentChunk.collection_name == collection_name)
.order_by(
(DocumentChunk.vector.cosine_distance(query_vectors.c.q_vector))
@@ -295,17 +427,43 @@ class PgvectorClient:
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
)
if PGVECTOR_PGCRYPTO:
# Build where clause for vmetadata filter
where_clauses = [DocumentChunk.collection_name == collection_name]
for key, value in filter.items():
# decrypt then check key: JSON filter after decryption
where_clauses.append(
pgcrypto_decrypt(
DocumentChunk.vmetadata, PGVECTOR_PGCRYPTO_KEY, JSONB
)[key].astext
== str(value)
)
stmt = select(
DocumentChunk.id,
pgcrypto_decrypt(
DocumentChunk.text, PGVECTOR_PGCRYPTO_KEY, Text
).label("text"),
pgcrypto_decrypt(
DocumentChunk.vmetadata, PGVECTOR_PGCRYPTO_KEY, JSONB
).label("vmetadata"),
).where(*where_clauses)
if limit is not None:
stmt = stmt.limit(limit)
results = self.session.execute(stmt).all()
else:
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))
for key, value in filter.items():
query = query.filter(
DocumentChunk.vmetadata[key].astext == str(value)
)
if limit is not None:
query = query.limit(limit)
if limit is not None:
query = query.limit(limit)
results = query.all()
results = query.all()
if not results:
return None
@@ -327,20 +485,38 @@ class PgvectorClient:
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)
if PGVECTOR_PGCRYPTO:
stmt = select(
DocumentChunk.id,
pgcrypto_decrypt(
DocumentChunk.text, PGVECTOR_PGCRYPTO_KEY, Text
).label("text"),
pgcrypto_decrypt(
DocumentChunk.vmetadata, PGVECTOR_PGCRYPTO_KEY, JSONB
).label("vmetadata"),
).where(DocumentChunk.collection_name == collection_name)
if limit is not None:
stmt = stmt.limit(limit)
results = self.session.execute(stmt).all()
ids = [[row.id for row in results]]
documents = [[row.text for row in results]]
metadatas = [[row.vmetadata for row in results]]
else:
results = query.all()
query = self.session.query(DocumentChunk).filter(
DocumentChunk.collection_name == collection_name
)
if limit is not None:
query = query.limit(limit)
if not results:
return None
results = query.all()
ids = [[result.id for result in results]]
documents = [[result.text for result in results]]
metadatas = [[result.vmetadata for result in results]]
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]]
return GetResult(ids=ids, documents=documents, metadatas=metadatas)
except Exception as e:
@@ -354,17 +530,33 @@ class PgvectorClient:
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)
if PGVECTOR_PGCRYPTO:
wheres = [DocumentChunk.collection_name == collection_name]
if ids:
wheres.append(DocumentChunk.id.in_(ids))
if filter:
for key, value in filter.items():
wheres.append(
pgcrypto_decrypt(
DocumentChunk.vmetadata, PGVECTOR_PGCRYPTO_KEY, JSONB
)[key].astext
== str(value)
)
stmt = DocumentChunk.__table__.delete().where(*wheres)
result = self.session.execute(stmt)
deleted = result.rowcount
else:
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:
@@ -0,0 +1,581 @@
from typing import Optional, List, Dict, Any, Union
import logging
import time # for measuring elapsed time
from pinecone import Pinecone, ServerlessSpec
# Add gRPC support for better performance (Pinecone best practice)
try:
from pinecone.grpc import PineconeGRPC
GRPC_AVAILABLE = True
except ImportError:
GRPC_AVAILABLE = False
import asyncio # for async upserts
import functools # for partial binding in async tasks
import concurrent.futures # for parallel batch upserts
import random # for jitter in retry backoff
from open_webui.retrieval.vector.main import (
VectorDBBase,
VectorItem,
SearchResult,
GetResult,
)
from open_webui.config import (
PINECONE_API_KEY,
PINECONE_ENVIRONMENT,
PINECONE_INDEX_NAME,
PINECONE_DIMENSION,
PINECONE_METRIC,
PINECONE_CLOUD,
)
from open_webui.env import SRC_LOG_LEVELS
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):
def __init__(self):
self.collection_prefix = "open-webui"
# Validate required configuration
self._validate_config()
# Store configuration values
self.api_key = PINECONE_API_KEY
self.environment = PINECONE_ENVIRONMENT
self.index_name = PINECONE_INDEX_NAME
self.dimension = PINECONE_DIMENSION
self.metric = PINECONE_METRIC
self.cloud = PINECONE_CLOUD
# Initialize Pinecone client for improved performance
if GRPC_AVAILABLE:
# Use gRPC client for better performance (Pinecone recommendation)
self.client = PineconeGRPC(
api_key=self.api_key,
pool_threads=20, # Improved connection pool size
timeout=30, # Reasonable timeout for operations
)
self.using_grpc = True
log.info("Using Pinecone gRPC client for optimal performance")
else:
# Fallback to HTTP client with enhanced connection pooling
self.client = Pinecone(
api_key=self.api_key,
pool_threads=20, # Improved connection pool size
timeout=30, # Reasonable timeout for operations
)
self.using_grpc = False
log.info("Using Pinecone HTTP client (gRPC not available)")
# Persistent executor for batch operations
self._executor = concurrent.futures.ThreadPoolExecutor(max_workers=5)
# Create index if it doesn't exist
self._initialize_index()
def _validate_config(self) -> None:
"""Validate that all required configuration variables are set."""
missing_vars = []
if not PINECONE_API_KEY:
missing_vars.append("PINECONE_API_KEY")
if not PINECONE_ENVIRONMENT:
missing_vars.append("PINECONE_ENVIRONMENT")
if not PINECONE_INDEX_NAME:
missing_vars.append("PINECONE_INDEX_NAME")
if not PINECONE_DIMENSION:
missing_vars.append("PINECONE_DIMENSION")
if not PINECONE_CLOUD:
missing_vars.append("PINECONE_CLOUD")
if missing_vars:
raise ValueError(
f"Required configuration missing: {', '.join(missing_vars)}"
)
def _initialize_index(self) -> None:
"""Initialize the Pinecone index."""
try:
# Check if index exists
if self.index_name not in self.client.list_indexes().names():
log.info(f"Creating Pinecone index '{self.index_name}'...")
self.client.create_index(
name=self.index_name,
dimension=self.dimension,
metric=self.metric,
spec=ServerlessSpec(cloud=self.cloud, region=self.environment),
)
log.info(f"Successfully created Pinecone index '{self.index_name}'")
else:
log.info(f"Using existing Pinecone index '{self.index_name}'")
# Connect to the index
self.index = self.client.Index(
self.index_name,
pool_threads=20, # Enhanced connection pool for index operations
)
except Exception as e:
log.error(f"Failed to initialize Pinecone index: {e}")
raise RuntimeError(f"Failed to initialize Pinecone index: {e}")
def _retry_pinecone_operation(self, operation_func, max_retries=3):
"""Retry Pinecone operations with exponential backoff for rate limits and network issues."""
for attempt in range(max_retries):
try:
return operation_func()
except Exception as e:
error_str = str(e).lower()
# Check if it's a retryable error (rate limits, network issues, timeouts)
is_retryable = any(
keyword in error_str
for keyword in [
"rate limit",
"quota",
"timeout",
"network",
"connection",
"unavailable",
"internal error",
"429",
"500",
"502",
"503",
"504",
]
)
if not is_retryable or attempt == max_retries - 1:
# Don't retry for non-retryable errors or on final attempt
raise
# Exponential backoff with jitter
delay = (2**attempt) + random.uniform(0, 1)
log.warning(
f"Pinecone operation failed (attempt {attempt + 1}/{max_retries}), "
f"retrying in {delay:.2f}s: {e}"
)
time.sleep(delay)
def _create_points(
self, items: List[VectorItem], collection_name_with_prefix: str
) -> List[Dict[str, Any]]:
"""Convert VectorItem objects to Pinecone point format."""
points = []
for item in items:
# Start with any existing metadata or an empty dict
metadata = item.get("metadata", {}).copy() if item.get("metadata") else {}
# Add text to metadata if available
if "text" in item:
metadata["text"] = item["text"]
# Always add collection_name to metadata for filtering
metadata["collection_name"] = collection_name_with_prefix
point = {
"id": item["id"],
"values": item["vector"],
"metadata": metadata,
}
points.append(point)
return points
def _get_collection_name_with_prefix(self, collection_name: str) -> str:
"""Get the collection name with prefix."""
return f"{self.collection_prefix}_{collection_name}"
def _normalize_distance(self, score: float) -> float:
"""Normalize distance score based on the metric used."""
if self.metric.lower() == "cosine":
# Cosine similarity ranges from -1 to 1, normalize to 0 to 1
return (score + 1.0) / 2.0
elif self.metric.lower() in ["euclidean", "dotproduct"]:
# These are already suitable for ranking (smaller is better for Euclidean)
return score
else:
# For other metrics, use as is
return score
def _result_to_get_result(self, matches: list) -> GetResult:
"""Convert Pinecone matches to GetResult format."""
ids = []
documents = []
metadatas = []
for match in matches:
metadata = getattr(match, "metadata", {}) or {}
ids.append(match.id if hasattr(match, "id") else match["id"])
documents.append(metadata.get("text", ""))
metadatas.append(metadata)
return GetResult(
**{
"ids": [ids],
"documents": [documents],
"metadatas": [metadatas],
}
)
def has_collection(self, collection_name: str) -> bool:
"""Check if a collection exists by searching for at least one item."""
collection_name_with_prefix = self._get_collection_name_with_prefix(
collection_name
)
try:
# Search for at least 1 item with this collection name in metadata
response = self.index.query(
vector=[0.0] * self.dimension, # dummy vector
top_k=1,
filter={"collection_name": collection_name_with_prefix},
include_metadata=False,
)
matches = getattr(response, "matches", []) or []
return len(matches) > 0
except Exception as e:
log.exception(
f"Error checking collection '{collection_name_with_prefix}': {e}"
)
return False
def delete_collection(self, collection_name: str) -> None:
"""Delete a collection by removing all vectors with the collection name in metadata."""
collection_name_with_prefix = self._get_collection_name_with_prefix(
collection_name
)
try:
self.index.delete(filter={"collection_name": collection_name_with_prefix})
log.info(
f"Collection '{collection_name_with_prefix}' deleted (all vectors removed)."
)
except Exception as e:
log.warning(
f"Failed to delete collection '{collection_name_with_prefix}': {e}"
)
raise
def insert(self, collection_name: str, items: List[VectorItem]) -> None:
"""Insert vectors into a collection."""
if not items:
log.warning("No items to insert")
return
start_time = time.time()
collection_name_with_prefix = self._get_collection_name_with_prefix(
collection_name
)
points = self._create_points(items, collection_name_with_prefix)
# Parallelize batch inserts for performance
executor = self._executor
futures = []
for i in range(0, len(points), BATCH_SIZE):
batch = points[i : i + BATCH_SIZE]
futures.append(executor.submit(self.index.upsert, vectors=batch))
for future in concurrent.futures.as_completed(futures):
try:
future.result()
except Exception as e:
log.error(f"Error inserting batch: {e}")
raise
elapsed = time.time() - start_time
log.debug(f"Insert of {len(points)} vectors took {elapsed:.2f} seconds")
log.info(
f"Successfully inserted {len(points)} vectors in parallel batches "
f"into '{collection_name_with_prefix}'"
)
def upsert(self, collection_name: str, items: List[VectorItem]) -> None:
"""Upsert (insert or update) vectors into a collection."""
if not items:
log.warning("No items to upsert")
return
start_time = time.time()
collection_name_with_prefix = self._get_collection_name_with_prefix(
collection_name
)
points = self._create_points(items, collection_name_with_prefix)
# Parallelize batch upserts for performance
executor = self._executor
futures = []
for i in range(0, len(points), BATCH_SIZE):
batch = points[i : i + BATCH_SIZE]
futures.append(executor.submit(self.index.upsert, vectors=batch))
for future in concurrent.futures.as_completed(futures):
try:
future.result()
except Exception as e:
log.error(f"Error upserting batch: {e}")
raise
elapsed = time.time() - start_time
log.debug(f"Upsert of {len(points)} vectors took {elapsed:.2f} seconds")
log.info(
f"Successfully upserted {len(points)} vectors in parallel batches "
f"into '{collection_name_with_prefix}'"
)
async def insert_async(self, collection_name: str, items: List[VectorItem]) -> None:
"""Async version of insert using asyncio and run_in_executor for improved performance."""
if not items:
log.warning("No items to insert")
return
collection_name_with_prefix = self._get_collection_name_with_prefix(
collection_name
)
points = self._create_points(items, collection_name_with_prefix)
# Create batches
batches = [
points[i : i + BATCH_SIZE] for i in range(0, len(points), BATCH_SIZE)
]
loop = asyncio.get_event_loop()
tasks = [
loop.run_in_executor(
None, functools.partial(self.index.upsert, vectors=batch)
)
for batch in batches
]
results = await asyncio.gather(*tasks, return_exceptions=True)
for result in results:
if isinstance(result, Exception):
log.error(f"Error in async insert batch: {result}")
raise result
log.info(
f"Successfully async inserted {len(points)} vectors in batches "
f"into '{collection_name_with_prefix}'"
)
async def upsert_async(self, collection_name: str, items: List[VectorItem]) -> None:
"""Async version of upsert using asyncio and run_in_executor for improved performance."""
if not items:
log.warning("No items to upsert")
return
collection_name_with_prefix = self._get_collection_name_with_prefix(
collection_name
)
points = self._create_points(items, collection_name_with_prefix)
# Create batches
batches = [
points[i : i + BATCH_SIZE] for i in range(0, len(points), BATCH_SIZE)
]
loop = asyncio.get_event_loop()
tasks = [
loop.run_in_executor(
None, functools.partial(self.index.upsert, vectors=batch)
)
for batch in batches
]
results = await asyncio.gather(*tasks, return_exceptions=True)
for result in results:
if isinstance(result, Exception):
log.error(f"Error in async upsert batch: {result}")
raise result
log.info(
f"Successfully async upserted {len(points)} vectors in batches "
f"into '{collection_name_with_prefix}'"
)
def search(
self, collection_name: str, vectors: List[List[Union[float, int]]], limit: int
) -> Optional[SearchResult]:
"""Search for similar vectors in a collection."""
if not vectors or not vectors[0]:
log.warning("No vectors provided for search")
return None
collection_name_with_prefix = self._get_collection_name_with_prefix(
collection_name
)
if limit is None or limit <= 0:
limit = NO_LIMIT
try:
# Search using the first vector (assuming this is the intended behavior)
query_vector = vectors[0]
# Perform the search
query_response = self.index.query(
vector=query_vector,
top_k=limit,
include_metadata=True,
filter={"collection_name": collection_name_with_prefix},
)
matches = getattr(query_response, "matches", []) or []
if not matches:
# Return empty result if no matches
return SearchResult(
ids=[[]],
documents=[[]],
metadatas=[[]],
distances=[[]],
)
# Convert to GetResult format
get_result = self._result_to_get_result(matches)
# Calculate normalized distances based on metric
distances = [
[
self._normalize_distance(getattr(match, "score", 0.0))
for match in matches
]
]
return SearchResult(
ids=get_result.ids,
documents=get_result.documents,
metadatas=get_result.metadatas,
distances=distances,
)
except Exception as e:
log.error(f"Error searching in '{collection_name_with_prefix}': {e}")
return None
def query(
self, collection_name: str, filter: Dict, limit: Optional[int] = None
) -> Optional[GetResult]:
"""Query vectors by metadata filter."""
collection_name_with_prefix = self._get_collection_name_with_prefix(
collection_name
)
if limit is None or limit <= 0:
limit = NO_LIMIT
try:
# Create a zero vector for the dimension as Pinecone requires a vector
zero_vector = [0.0] * self.dimension
# Combine user filter with collection_name
pinecone_filter = {"collection_name": collection_name_with_prefix}
if filter:
pinecone_filter.update(filter)
# Perform metadata-only query
query_response = self.index.query(
vector=zero_vector,
filter=pinecone_filter,
top_k=limit,
include_metadata=True,
)
matches = getattr(query_response, "matches", []) or []
return self._result_to_get_result(matches)
except Exception as e:
log.error(f"Error querying collection '{collection_name}': {e}")
return None
def get(self, collection_name: str) -> Optional[GetResult]:
"""Get all vectors in a collection."""
collection_name_with_prefix = self._get_collection_name_with_prefix(
collection_name
)
try:
# Use a zero vector for fetching all entries
zero_vector = [0.0] * self.dimension
# Add filter to only get vectors for this collection
query_response = self.index.query(
vector=zero_vector,
top_k=NO_LIMIT,
include_metadata=True,
filter={"collection_name": collection_name_with_prefix},
)
matches = getattr(query_response, "matches", []) or []
return self._result_to_get_result(matches)
except Exception as e:
log.error(f"Error getting collection '{collection_name}': {e}")
return None
def delete(
self,
collection_name: str,
ids: Optional[List[str]] = None,
filter: Optional[Dict] = None,
) -> None:
"""Delete vectors by IDs or filter."""
collection_name_with_prefix = self._get_collection_name_with_prefix(
collection_name
)
try:
if ids:
# Delete by IDs (in batches for large deletions)
for i in range(0, len(ids), BATCH_SIZE):
batch_ids = ids[i : i + BATCH_SIZE]
# Note: When deleting by ID, we can't filter by collection_name
# This is a limitation of Pinecone - be careful with ID uniqueness
self.index.delete(ids=batch_ids)
log.debug(
f"Deleted batch of {len(batch_ids)} vectors by ID "
f"from '{collection_name_with_prefix}'"
)
log.info(
f"Successfully deleted {len(ids)} vectors by ID "
f"from '{collection_name_with_prefix}'"
)
elif filter:
# Combine user filter with collection_name
pinecone_filter = {"collection_name": collection_name_with_prefix}
if filter:
pinecone_filter.update(filter)
# Delete by metadata filter
self.index.delete(filter=pinecone_filter)
log.info(
f"Successfully deleted vectors by filter from '{collection_name_with_prefix}'"
)
else:
log.warning("No ids or filter provided for delete operation")
except Exception as e:
log.error(f"Error deleting from collection '{collection_name}': {e}")
raise
def reset(self) -> None:
"""Reset the database by deleting all collections."""
try:
self.index.delete(delete_all=True)
log.info("All vectors successfully deleted from the index.")
except Exception as e:
log.error(f"Failed to reset Pinecone index: {e}")
raise
def close(self):
"""Shut down resources."""
try:
# The new Pinecone client doesn't need explicit closing
pass
except Exception as e:
log.warning(f"Failed to clean up Pinecone resources: {e}")
self._executor.shutdown(wait=True)
def __enter__(self):
"""Enter context manager."""
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Exit context manager, ensuring resources are cleaned up."""
self.close()
@@ -1,12 +1,25 @@
from typing import Optional
import logging
from urllib.parse import urlparse
from qdrant_client import QdrantClient as Qclient
from qdrant_client.http.models import PointStruct
from qdrant_client.models import models
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
from open_webui.config import QDRANT_URI, QDRANT_API_KEY
from open_webui.retrieval.vector.main import (
VectorDBBase,
VectorItem,
SearchResult,
GetResult,
)
from open_webui.config import (
QDRANT_URI,
QDRANT_API_KEY,
QDRANT_ON_DISK,
QDRANT_GRPC_PORT,
QDRANT_PREFER_GRPC,
QDRANT_COLLECTION_PREFIX,
)
from open_webui.env import SRC_LOG_LEVELS
NO_LIMIT = 999999999
@@ -15,16 +28,34 @@ log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class QdrantClient:
class QdrantClient(VectorDBBase):
def __init__(self):
self.collection_prefix = "open-webui"
self.collection_prefix = QDRANT_COLLECTION_PREFIX
self.QDRANT_URI = QDRANT_URI
self.QDRANT_API_KEY = QDRANT_API_KEY
self.client = (
Qclient(url=self.QDRANT_URI, api_key=self.QDRANT_API_KEY)
if self.QDRANT_URI
else None
)
self.QDRANT_ON_DISK = QDRANT_ON_DISK
self.PREFER_GRPC = QDRANT_PREFER_GRPC
self.GRPC_PORT = QDRANT_GRPC_PORT
if not self.QDRANT_URI:
self.client = None
return
# Unified handling for either scheme
parsed = urlparse(self.QDRANT_URI)
host = parsed.hostname or self.QDRANT_URI
http_port = parsed.port or 6333 # default REST port
if self.PREFER_GRPC:
self.client = Qclient(
host=host,
port=http_port,
grpc_port=self.GRPC_PORT,
prefer_grpc=self.PREFER_GRPC,
api_key=self.QDRANT_API_KEY,
)
else:
self.client = Qclient(url=self.QDRANT_URI, api_key=self.QDRANT_API_KEY)
def _result_to_get_result(self, points) -> GetResult:
ids = []
@@ -50,10 +81,31 @@ class QdrantClient:
self.client.create_collection(
collection_name=collection_name_with_prefix,
vectors_config=models.VectorParams(
size=dimension, distance=models.Distance.COSINE
size=dimension,
distance=models.Distance.COSINE,
on_disk=self.QDRANT_ON_DISK,
),
)
# Create payload indexes for efficient filtering
self.client.create_payload_index(
collection_name=collection_name_with_prefix,
field_name="metadata.hash",
field_schema=models.KeywordIndexParams(
type=models.KeywordIndexType.KEYWORD,
is_tenant=False,
on_disk=self.QDRANT_ON_DISK,
),
)
self.client.create_payload_index(
collection_name=collection_name_with_prefix,
field_name="metadata.file_id",
field_schema=models.KeywordIndexParams(
type=models.KeywordIndexType.KEYWORD,
is_tenant=False,
on_disk=self.QDRANT_ON_DISK,
),
)
log.info(f"collection {collection_name_with_prefix} successfully created!")
def _create_collection_if_not_exists(self, collection_name, dimension):
@@ -0,0 +1,350 @@
import logging
from typing import Optional, Tuple, List, Dict, Any
from urllib.parse import urlparse
import grpc
from open_webui.config import (
QDRANT_API_KEY,
QDRANT_GRPC_PORT,
QDRANT_ON_DISK,
QDRANT_PREFER_GRPC,
QDRANT_URI,
QDRANT_COLLECTION_PREFIX,
)
from open_webui.env import SRC_LOG_LEVELS
from open_webui.retrieval.vector.main import (
GetResult,
SearchResult,
VectorDBBase,
VectorItem,
)
from qdrant_client import QdrantClient as Qclient
from qdrant_client.http.exceptions import UnexpectedResponse
from qdrant_client.http.models import PointStruct
from qdrant_client.models import models
NO_LIMIT = 999999999
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:
return models.FieldCondition(
key=TENANT_ID_FIELD, match=models.MatchValue(value=tenant_id)
)
def _metadata_filter(key: str, value: Any) -> models.FieldCondition:
return models.FieldCondition(
key=f"metadata.{key}", match=models.MatchValue(value=value)
)
class QdrantClient(VectorDBBase):
def __init__(self):
self.collection_prefix = QDRANT_COLLECTION_PREFIX
self.QDRANT_URI = QDRANT_URI
self.QDRANT_API_KEY = QDRANT_API_KEY
self.QDRANT_ON_DISK = QDRANT_ON_DISK
self.PREFER_GRPC = QDRANT_PREFER_GRPC
self.GRPC_PORT = QDRANT_GRPC_PORT
if not self.QDRANT_URI:
raise ValueError(
"QDRANT_URI is not set. Please configure it in the environment variables."
)
# Unified handling for either scheme
parsed = urlparse(self.QDRANT_URI)
host = parsed.hostname or self.QDRANT_URI
http_port = parsed.port or 6333 # default REST port
self.client = (
Qclient(
host=host,
port=http_port,
grpc_port=self.GRPC_PORT,
prefer_grpc=self.PREFER_GRPC,
api_key=self.QDRANT_API_KEY,
)
if self.PREFER_GRPC
else Qclient(url=self.QDRANT_URI, api_key=self.QDRANT_API_KEY)
)
# 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"
def _result_to_get_result(self, points) -> GetResult:
ids, documents, metadatas = [], [], []
for point in points:
payload = point.payload
ids.append(point.id)
documents.append(payload["text"])
metadatas.append(payload["metadata"])
return GetResult(ids=[ids], documents=[documents], metadatas=[metadatas])
def _get_collection_and_tenant_id(self, collection_name: str) -> Tuple[str, str]:
"""
Maps the traditional collection name to multi-tenant collection and tenant ID.
Returns:
tuple: (collection_name, tenant_id)
"""
# Check for user memory collections
tenant_id = collection_name
if collection_name.startswith("user-memory-"):
return self.MEMORY_COLLECTION, tenant_id
# Check for file collections
elif collection_name.startswith("file-"):
return self.FILE_COLLECTION, tenant_id
# Check for web search collections
elif collection_name.startswith("web-search-"):
return self.WEB_SEARCH_COLLECTION, tenant_id
# Handle hash-based collections (YouTube and web URLs)
elif len(collection_name) == 63 and all(
c in "0123456789abcdef" for c in collection_name
):
return self.HASH_BASED_COLLECTION, tenant_id
else:
return self.KNOWLEDGE_COLLECTION, tenant_id
def _create_multi_tenant_collection(
self, mt_collection_name: str, dimension: int = DEFAULT_DIMENSION
):
"""
Creates a collection with multi-tenancy configuration and payload indexes for tenant_id and metadata fields.
"""
self.client.create_collection(
collection_name=mt_collection_name,
vectors_config=models.VectorParams(
size=dimension,
distance=models.Distance.COSINE,
on_disk=self.QDRANT_ON_DISK,
),
)
log.info(
f"Multi-tenant collection {mt_collection_name} created with dimension {dimension}!"
)
self.client.create_payload_index(
collection_name=mt_collection_name,
field_name=TENANT_ID_FIELD,
field_schema=models.KeywordIndexParams(
type=models.KeywordIndexType.KEYWORD,
is_tenant=True,
on_disk=self.QDRANT_ON_DISK,
),
)
for field in ("metadata.hash", "metadata.file_id"):
self.client.create_payload_index(
collection_name=mt_collection_name,
field_name=field,
field_schema=models.KeywordIndexParams(
type=models.KeywordIndexType.KEYWORD,
on_disk=self.QDRANT_ON_DISK,
),
)
def _create_points(
self, items: List[VectorItem], tenant_id: str
) -> List[PointStruct]:
"""
Create point structs from vector items with tenant ID.
"""
return [
PointStruct(
id=item["id"],
vector=item["vector"],
payload={
"text": item["text"],
"metadata": item["metadata"],
TENANT_ID_FIELD: tenant_id,
},
)
for item in items
]
def _ensure_collection(
self, mt_collection_name: str, dimension: int = DEFAULT_DIMENSION
):
"""
Ensure the collection exists and payload indexes are created for tenant_id and metadata fields.
"""
if not self.client.collection_exists(collection_name=mt_collection_name):
self._create_multi_tenant_collection(mt_collection_name, dimension)
def has_collection(self, collection_name: str) -> bool:
"""
Check if a logical collection exists by checking for any points with the tenant ID.
"""
if not self.client:
return False
mt_collection, tenant_id = self._get_collection_and_tenant_id(collection_name)
if not self.client.collection_exists(collection_name=mt_collection):
return False
tenant_filter = _tenant_filter(tenant_id)
count_result = self.client.count(
collection_name=mt_collection,
count_filter=models.Filter(must=[tenant_filter]),
)
return count_result.count > 0
def delete(
self,
collection_name: str,
ids: Optional[List[str]] = None,
filter: Optional[Dict[str, Any]] = None,
):
"""
Delete vectors by ID or filter from a collection with tenant isolation.
"""
if not self.client:
return None
mt_collection, tenant_id = self._get_collection_and_tenant_id(collection_name)
if not self.client.collection_exists(collection_name=mt_collection):
log.debug(f"Collection {mt_collection} doesn't exist, nothing to delete")
return None
must_conditions = [_tenant_filter(tenant_id)]
should_conditions = []
if ids:
should_conditions = [_metadata_filter("id", id_value) for id_value in ids]
elif filter:
must_conditions += [_metadata_filter(k, v) for k, v in filter.items()]
return self.client.delete(
collection_name=mt_collection,
points_selector=models.FilterSelector(
filter=models.Filter(must=must_conditions, should=should_conditions)
),
)
def search(
self, collection_name: str, vectors: List[List[float | int]], limit: int
) -> Optional[SearchResult]:
"""
Search for the nearest neighbor items based on the vectors with tenant isolation.
"""
if not self.client or not vectors:
return None
mt_collection, tenant_id = self._get_collection_and_tenant_id(collection_name)
if not self.client.collection_exists(collection_name=mt_collection):
log.debug(f"Collection {mt_collection} doesn't exist, search returns None")
return None
tenant_filter = _tenant_filter(tenant_id)
query_response = self.client.query_points(
collection_name=mt_collection,
query=vectors[0],
limit=limit,
query_filter=models.Filter(must=[tenant_filter]),
)
get_result = self._result_to_get_result(query_response.points)
return SearchResult(
ids=get_result.ids,
documents=get_result.documents,
metadatas=get_result.metadatas,
distances=[[(point.score + 1.0) / 2.0 for point in query_response.points]],
)
def query(
self, collection_name: str, filter: Dict[str, Any], limit: Optional[int] = None
):
"""
Query points with filters and tenant isolation.
"""
if not self.client:
return None
mt_collection, tenant_id = self._get_collection_and_tenant_id(collection_name)
if not self.client.collection_exists(collection_name=mt_collection):
log.debug(f"Collection {mt_collection} doesn't exist, query returns None")
return None
if limit is None:
limit = NO_LIMIT
tenant_filter = _tenant_filter(tenant_id)
field_conditions = [_metadata_filter(k, v) for k, v in filter.items()]
combined_filter = models.Filter(must=[tenant_filter, *field_conditions])
points = self.client.query_points(
collection_name=mt_collection,
query_filter=combined_filter,
limit=limit,
)
return self._result_to_get_result(points.points)
def get(self, collection_name: str) -> Optional[GetResult]:
"""
Get all items in a collection with tenant isolation.
"""
if not self.client:
return None
mt_collection, tenant_id = self._get_collection_and_tenant_id(collection_name)
if not self.client.collection_exists(collection_name=mt_collection):
log.debug(f"Collection {mt_collection} doesn't exist, get returns None")
return None
tenant_filter = _tenant_filter(tenant_id)
points = self.client.query_points(
collection_name=mt_collection,
query_filter=models.Filter(must=[tenant_filter]),
limit=NO_LIMIT,
)
return self._result_to_get_result(points.points)
def upsert(self, collection_name: str, items: List[VectorItem]):
"""
Upsert items with tenant ID.
"""
if not self.client or not items:
return None
mt_collection, tenant_id = self._get_collection_and_tenant_id(collection_name)
dimension = len(items[0]["vector"])
self._ensure_collection(mt_collection, dimension)
points = self._create_points(items, tenant_id)
self.client.upload_points(mt_collection, points)
return None
def insert(self, collection_name: str, items: List[VectorItem]):
"""
Insert items with tenant ID.
"""
return self.upsert(collection_name, items)
def reset(self):
"""
Reset the database by deleting all collections.
"""
if not self.client:
return None
for collection in self.client.get_collections().collections:
if collection.name.startswith(self.collection_prefix):
self.client.delete_collection(collection_name=collection.name)
def delete_collection(self, collection_name: str):
"""
Delete a collection.
"""
if not self.client:
return None
mt_collection, tenant_id = self._get_collection_and_tenant_id(collection_name)
if not self.client.collection_exists(collection_name=mt_collection):
log.debug(f"Collection {mt_collection} doesn't exist, nothing to delete")
return None
self.client.delete(
collection_name=mt_collection,
points_selector=models.FilterSelector(
filter=models.Filter(must=[_tenant_filter(tenant_id)])
),
)
@@ -0,0 +1,55 @@
from open_webui.retrieval.vector.main import VectorDBBase
from open_webui.retrieval.vector.type import VectorType
from open_webui.config import VECTOR_DB, ENABLE_QDRANT_MULTITENANCY_MODE
class Vector:
@staticmethod
def get_vector(vector_type: str) -> VectorDBBase:
"""
get vector db instance by vector type
"""
match vector_type:
case VectorType.MILVUS:
from open_webui.retrieval.vector.dbs.milvus import MilvusClient
return MilvusClient()
case VectorType.QDRANT:
if ENABLE_QDRANT_MULTITENANCY_MODE:
from open_webui.retrieval.vector.dbs.qdrant_multitenancy import (
QdrantClient,
)
return QdrantClient()
else:
from open_webui.retrieval.vector.dbs.qdrant import QdrantClient
return QdrantClient()
case VectorType.PINECONE:
from open_webui.retrieval.vector.dbs.pinecone import PineconeClient
return PineconeClient()
case VectorType.OPENSEARCH:
from open_webui.retrieval.vector.dbs.opensearch import OpenSearchClient
return OpenSearchClient()
case VectorType.PGVECTOR:
from open_webui.retrieval.vector.dbs.pgvector import PgvectorClient
return PgvectorClient()
case VectorType.ELASTICSEARCH:
from open_webui.retrieval.vector.dbs.elasticsearch import (
ElasticsearchClient,
)
return ElasticsearchClient()
case VectorType.CHROMA:
from open_webui.retrieval.vector.dbs.chroma import ChromaClient
return ChromaClient()
case _:
raise ValueError(f"Unsupported vector type: {vector_type}")
VECTOR_DB_CLIENT = Vector.get_vector(VECTOR_DB)
+68 -1
View File
@@ -1,5 +1,6 @@
from pydantic import BaseModel
from typing import Optional, List, Any
from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional, Union
class VectorItem(BaseModel):
@@ -17,3 +18,69 @@ class GetResult(BaseModel):
class SearchResult(GetResult):
distances: Optional[List[List[float | int]]]
class VectorDBBase(ABC):
"""
Abstract base class for all vector database backends.
Implementations of this class provide methods for collection management,
vector insertion, deletion, similarity search, and metadata filtering.
Any custom vector database integration must inherit from this class and
implement all abstract methods.
"""
@abstractmethod
def has_collection(self, collection_name: str) -> bool:
"""Check if the collection exists in the vector DB."""
pass
@abstractmethod
def delete_collection(self, collection_name: str) -> None:
"""Delete a collection from the vector DB."""
pass
@abstractmethod
def insert(self, collection_name: str, items: List[VectorItem]) -> None:
"""Insert a list of vector items into a collection."""
pass
@abstractmethod
def upsert(self, collection_name: str, items: List[VectorItem]) -> None:
"""Insert or update vector items in a collection."""
pass
@abstractmethod
def search(
self, collection_name: str, vectors: List[List[Union[float, int]]], limit: int
) -> Optional[SearchResult]:
"""Search for similar vectors in a collection."""
pass
@abstractmethod
def query(
self, collection_name: str, filter: Dict, limit: Optional[int] = None
) -> Optional[GetResult]:
"""Query vectors from a collection using metadata filter."""
pass
@abstractmethod
def get(self, collection_name: str) -> Optional[GetResult]:
"""Retrieve all vectors from a collection."""
pass
@abstractmethod
def delete(
self,
collection_name: str,
ids: Optional[List[str]] = None,
filter: Optional[Dict] = None,
) -> None:
"""Delete vectors by ID or filter from a collection."""
pass
@abstractmethod
def reset(self) -> None:
"""Reset the vector database by removing all collections or those matching a condition."""
pass
@@ -0,0 +1,11 @@
from enum import StrEnum
class VectorType(StrEnum):
MILVUS = "milvus"
QDRANT = "qdrant"
CHROMA = "chroma"
PINECONE = "pinecone"
ELASTICSEARCH = "elasticsearch"
OPENSEARCH = "opensearch"
PGVECTOR = "pgvector"
+3 -1
View File
@@ -36,7 +36,9 @@ def search_brave(
return [
SearchResult(
link=result["url"], title=result.get("title"), snippet=result.get("snippet")
link=result["url"],
title=result.get("title"),
snippet=result.get("description"),
)
for result in results[:count]
]
@@ -2,7 +2,8 @@ import logging
from typing import Optional
from open_webui.retrieval.web.main import SearchResult, get_filtered_results
from duckduckgo_search import DDGS
from ddgs import DDGS
from ddgs.exceptions import RatelimitException
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
@@ -22,16 +23,15 @@ def search_duckduckgo(
list[SearchResult]: A list of search results
"""
# Use the DDGS context manager to create a DDGS object
search_results = []
with DDGS() as ddgs:
# Use the ddgs.text() method to perform the search
ddgs_gen = ddgs.text(
query, safesearch="moderate", max_results=count, backend="api"
)
# Check if there are search results
if ddgs_gen:
# Convert the search results into a list
search_results = [r for r in ddgs_gen]
try:
search_results = ddgs.text(
query, safesearch="moderate", max_results=count, backend="lite"
)
except RatelimitException as e:
log.error(f"RatelimitException: {e}")
if filter_list:
search_results = get_filtered_results(search_results, filter_list)
@@ -0,0 +1,47 @@
import logging
from typing import Optional, List
import requests
from open_webui.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_external(
external_url: str,
external_api_key: str,
query: str,
count: int,
filter_list: Optional[List[str]] = None,
) -> List[SearchResult]:
try:
response = requests.post(
external_url,
headers={
"User-Agent": "Open WebUI (https://github.com/open-webui/open-webui) RAG Bot",
"Authorization": f"Bearer {external_api_key}",
},
json={
"query": query,
"count": count,
},
)
response.raise_for_status()
results = response.json()
if filter_list:
results = get_filtered_results(results, filter_list)
results = [
SearchResult(
link=result.get("link"),
title=result.get("title"),
snippet=result.get("snippet"),
)
for result in results[:count]
]
log.info(f"External search results: {results}")
return results
except Exception as e:
log.error(f"Error in External search: {e}")
return []
@@ -0,0 +1,49 @@
import logging
from typing import Optional, List
from urllib.parse import urljoin
import requests
from open_webui.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_firecrawl(
firecrawl_url: str,
firecrawl_api_key: str,
query: str,
count: int,
filter_list: Optional[List[str]] = None,
) -> List[SearchResult]:
try:
firecrawl_search_url = urljoin(firecrawl_url, "/v1/search")
response = requests.post(
firecrawl_search_url,
headers={
"User-Agent": "Open WebUI (https://github.com/open-webui/open-webui) RAG Bot",
"Authorization": f"Bearer {firecrawl_api_key}",
},
json={
"query": query,
"limit": count,
},
)
response.raise_for_status()
results = response.json().get("data", [])
if filter_list:
results = get_filtered_results(results, filter_list)
results = [
SearchResult(
link=result.get("url"),
title=result.get("title"),
snippet=result.get("description"),
)
for result in results[:count]
]
log.info(f"External search results: {results}")
return results
except Exception as e:
log.error(f"Error in External search: {e}")
return []
+20 -2
View File
@@ -1,10 +1,20 @@
import logging
from typing import Optional, List
from typing import Optional, Literal
import requests
from open_webui.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.env import SRC_LOG_LEVELS
MODELS = Literal[
"sonar",
"sonar-pro",
"sonar-reasoning",
"sonar-reasoning-pro",
"sonar-deep-research",
]
SEARCH_CONTEXT_USAGE_LEVELS = Literal["low", "medium", "high"]
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
@@ -14,6 +24,8 @@ def search_perplexity(
query: str,
count: int,
filter_list: Optional[list[str]] = None,
model: MODELS = "sonar",
search_context_usage: SEARCH_CONTEXT_USAGE_LEVELS = "medium",
) -> list[SearchResult]:
"""Search using Perplexity API and return the results as a list of SearchResult objects.
@@ -21,6 +33,9 @@ def search_perplexity(
api_key (str): A Perplexity API key
query (str): The query to search for
count (int): Maximum number of results to return
filter_list (Optional[list[str]]): List of domains to filter results
model (str): The Perplexity model to use (sonar, sonar-pro)
search_context_usage (str): Search context usage level (low, medium, high)
"""
@@ -33,7 +48,7 @@ def search_perplexity(
# Create payload for the API call
payload = {
"model": "sonar",
"model": model,
"messages": [
{
"role": "system",
@@ -43,6 +58,9 @@ def search_perplexity(
],
"temperature": 0.2, # Lower temperature for more factual responses
"stream": False,
"web_search_options": {
"search_context_usage": search_context_usage,
},
}
headers = {
@@ -42,7 +42,9 @@ def search_searchapi(
results = get_filtered_results(results, filter_list)
return [
SearchResult(
link=result["link"], title=result["title"], snippet=result["snippet"]
link=result["link"],
title=result.get("title"),
snippet=result.get("snippet"),
)
for result in results[:count]
]
+3 -1
View File
@@ -42,7 +42,9 @@ def search_serpapi(
results = get_filtered_results(results, filter_list)
return [
SearchResult(
link=result["link"], title=result["title"], snippet=result["snippet"]
link=result["link"],
title=result.get("title"),
snippet=result.get("snippet"),
)
for result in results[:count]
]
@@ -0,0 +1,60 @@
import logging
import json
from typing import Optional, List
from open_webui.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_sougou(
sougou_api_sid: str,
sougou_api_sk: str,
query: str,
count: int,
filter_list: Optional[List[str]] = None,
) -> List[SearchResult]:
from tencentcloud.common.common_client import CommonClient
from tencentcloud.common import credential
from tencentcloud.common.exception.tencent_cloud_sdk_exception import (
TencentCloudSDKException,
)
from tencentcloud.common.profile.client_profile import ClientProfile
from tencentcloud.common.profile.http_profile import HttpProfile
try:
cred = credential.Credential(sougou_api_sid, sougou_api_sk)
http_profile = HttpProfile()
http_profile.endpoint = "tms.tencentcloudapi.com"
client_profile = ClientProfile()
client_profile.http_profile = http_profile
params = json.dumps({"Query": query, "Cnt": 20})
common_client = CommonClient(
"tms", "2020-12-29", cred, "", profile=client_profile
)
results = [
json.loads(page)
for page in common_client.call_json("SearchPro", json.loads(params))[
"Response"
]["Pages"]
]
sorted_results = sorted(
results, key=lambda x: x.get("scour", 0.0), reverse=True
)
if filter_list:
sorted_results = get_filtered_results(sorted_results, filter_list)
return [
SearchResult(
link=result.get("url"),
title=result.get("title"),
snippet=result.get("passage"),
)
for result in sorted_results[:count]
]
except TencentCloudSDKException as err:
log.error(f"Error in Sougou search: {err}")
return []
+12 -5
View File
@@ -2,7 +2,7 @@ import logging
from typing import Optional
import requests
from open_webui.retrieval.web.main import SearchResult
from open_webui.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
@@ -21,18 +21,25 @@ def search_tavily(
Args:
api_key (str): A Tavily Search API key
query (str): The query to search for
count (int): The maximum number of results to return
Returns:
list[SearchResult]: A list of search results
"""
url = "https://api.tavily.com/search"
data = {"query": query, "api_key": api_key}
response = requests.post(url, json=data)
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}",
}
data = {"query": query, "max_results": count}
response = requests.post(url, headers=headers, json=data)
response.raise_for_status()
json_response = response.json()
raw_search_results = json_response.get("results", [])
results = json_response.get("results", [])
if filter_list:
results = get_filtered_results(results, filter_list)
return [
SearchResult(
@@ -40,5 +47,5 @@ def search_tavily(
title=result.get("title", ""),
snippet=result.get("content"),
)
for result in raw_search_results[:count]
for result in results
]
+43 -27
View File
@@ -25,18 +25,21 @@ from langchain_community.document_loaders.firecrawl import FireCrawlLoader
from langchain_community.document_loaders.base import BaseLoader
from langchain_core.documents import Document
from open_webui.retrieval.loaders.tavily import TavilyLoader
from open_webui.retrieval.loaders.external_web import ExternalWebLoader
from open_webui.constants import ERROR_MESSAGES
from open_webui.config import (
ENABLE_RAG_LOCAL_WEB_FETCH,
PLAYWRIGHT_WS_URI,
PLAYWRIGHT_WS_URL,
PLAYWRIGHT_TIMEOUT,
RAG_WEB_LOADER_ENGINE,
WEB_LOADER_ENGINE,
FIRECRAWL_API_BASE_URL,
FIRECRAWL_API_KEY,
TAVILY_API_KEY,
TAVILY_EXTRACT_DEPTH,
EXTERNAL_WEB_LOADER_URL,
EXTERNAL_WEB_LOADER_API_KEY,
)
from open_webui.env import SRC_LOG_LEVELS
from open_webui.env import SRC_LOG_LEVELS, AIOHTTP_CLIENT_SESSION_SSL
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
@@ -167,7 +170,7 @@ class SafeFireCrawlLoader(BaseLoader, RateLimitMixin, URLProcessingMixin):
continue_on_failure: bool = True,
api_key: Optional[str] = None,
api_url: Optional[str] = None,
mode: Literal["crawl", "scrape", "map"] = "crawl",
mode: Literal["crawl", "scrape", "map"] = "scrape",
proxy: Optional[Dict[str, str]] = None,
params: Optional[Dict] = None,
):
@@ -225,7 +228,10 @@ class SafeFireCrawlLoader(BaseLoader, RateLimitMixin, URLProcessingMixin):
mode=self.mode,
params=self.params,
)
yield from loader.lazy_load()
for document in loader.lazy_load():
if not document.metadata.get("source"):
document.metadata["source"] = document.metadata.get("sourceURL")
yield document
except Exception as e:
if self.continue_on_failure:
log.exception(f"Error loading {url}: {e}")
@@ -245,6 +251,8 @@ class SafeFireCrawlLoader(BaseLoader, RateLimitMixin, URLProcessingMixin):
params=self.params,
)
async for document in loader.alazy_load():
if not document.metadata.get("source"):
document.metadata["source"] = document.metadata.get("sourceURL")
yield document
except Exception as e:
if self.continue_on_failure:
@@ -507,7 +515,8 @@ class SafeWebBaseLoader(WebBaseLoader):
kwargs["ssl"] = False
async with session.get(
url, **(self.requests_kwargs | kwargs)
url,
**(self.requests_kwargs | kwargs),
) as response:
if self.raise_for_status:
response.raise_for_status()
@@ -584,13 +593,6 @@ class SafeWebBaseLoader(WebBaseLoader):
return [document async for document in self.alazy_load()]
RAG_WEB_LOADER_ENGINES = defaultdict(lambda: SafeWebBaseLoader)
RAG_WEB_LOADER_ENGINES["playwright"] = SafePlaywrightURLLoader
RAG_WEB_LOADER_ENGINES["safe_web"] = SafeWebBaseLoader
RAG_WEB_LOADER_ENGINES["firecrawl"] = SafeFireCrawlLoader
RAG_WEB_LOADER_ENGINES["tavily"] = SafeTavilyLoader
def get_web_loader(
urls: Union[str, Sequence[str]],
verify_ssl: bool = True,
@@ -608,27 +610,41 @@ def get_web_loader(
"trust_env": trust_env,
}
if RAG_WEB_LOADER_ENGINE.value == "playwright":
if WEB_LOADER_ENGINE.value == "" or WEB_LOADER_ENGINE.value == "safe_web":
WebLoaderClass = SafeWebBaseLoader
if WEB_LOADER_ENGINE.value == "playwright":
WebLoaderClass = SafePlaywrightURLLoader
web_loader_args["playwright_timeout"] = PLAYWRIGHT_TIMEOUT.value * 1000
if PLAYWRIGHT_WS_URI.value:
web_loader_args["playwright_ws_url"] = PLAYWRIGHT_WS_URI.value
if PLAYWRIGHT_WS_URL.value:
web_loader_args["playwright_ws_url"] = PLAYWRIGHT_WS_URL.value
if RAG_WEB_LOADER_ENGINE.value == "firecrawl":
if WEB_LOADER_ENGINE.value == "firecrawl":
WebLoaderClass = SafeFireCrawlLoader
web_loader_args["api_key"] = FIRECRAWL_API_KEY.value
web_loader_args["api_url"] = FIRECRAWL_API_BASE_URL.value
if RAG_WEB_LOADER_ENGINE.value == "tavily":
if WEB_LOADER_ENGINE.value == "tavily":
WebLoaderClass = SafeTavilyLoader
web_loader_args["api_key"] = TAVILY_API_KEY.value
web_loader_args["extract_depth"] = TAVILY_EXTRACT_DEPTH.value
# Create the appropriate WebLoader based on the configuration
WebLoaderClass = RAG_WEB_LOADER_ENGINES[RAG_WEB_LOADER_ENGINE.value]
web_loader = WebLoaderClass(**web_loader_args)
if WEB_LOADER_ENGINE.value == "external":
WebLoaderClass = ExternalWebLoader
web_loader_args["external_url"] = EXTERNAL_WEB_LOADER_URL.value
web_loader_args["external_api_key"] = EXTERNAL_WEB_LOADER_API_KEY.value
log.debug(
"Using RAG_WEB_LOADER_ENGINE %s for %s URLs",
web_loader.__class__.__name__,
len(safe_urls),
)
if WebLoaderClass:
web_loader = WebLoaderClass(**web_loader_args)
return web_loader
log.debug(
"Using WEB_LOADER_ENGINE %s for %s URLs",
web_loader.__class__.__name__,
len(safe_urls),
)
return web_loader
else:
raise ValueError(
f"Invalid WEB_LOADER_ENGINE: {WEB_LOADER_ENGINE.value}. "
"Please set it to 'safe_web', 'playwright', 'firecrawl', or 'tavily'."
)
+87
View File
@@ -0,0 +1,87 @@
import logging
from typing import Optional
import requests
from requests.auth import HTTPDigestAuth
from open_webui.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_yacy(
query_url: str,
username: Optional[str],
password: Optional[str],
query: str,
count: int,
filter_list: Optional[list[str]] = None,
) -> list[SearchResult]:
"""
Search a Yacy instance for a given query and return the results as a list of SearchResult objects.
The function accepts username and password for authenticating to Yacy.
Args:
query_url (str): The base URL of the Yacy server.
username (str): Optional YaCy username.
password (str): Optional YaCy password.
query (str): The search term or question to find in the Yacy database.
count (int): The maximum number of results to retrieve from the search.
Returns:
list[SearchResult]: A list of SearchResults sorted by relevance score in descending order.
Raise:
requests.exceptions.RequestException: If a request error occurs during the search process.
"""
# Use authentication if either username or password is set
yacy_auth = None
if username or password:
yacy_auth = HTTPDigestAuth(username, password)
params = {
"query": query,
"contentdom": "text",
"resource": "global",
"maximumRecords": count,
"nav": "none",
}
# Check if provided a json API URL
if not query_url.endswith("yacysearch.json"):
# Strip all query parameters from the URL
query_url = query_url.rstrip("/") + "/yacysearch.json"
log.debug(f"searching {query_url}")
response = requests.get(
query_url,
auth=yacy_auth,
headers={
"User-Agent": "Open WebUI (https://github.com/open-webui/open-webui) RAG Bot",
"Accept": "text/html",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "en-US,en;q=0.5",
"Connection": "keep-alive",
},
params=params,
)
response.raise_for_status() # Raise an exception for HTTP errors.
json_response = response.json()
results = json_response.get("channels", [{}])[0].get("items", [])
sorted_results = sorted(results, key=lambda x: x.get("ranking", 0), reverse=True)
if filter_list:
sorted_results = get_filtered_results(sorted_results, filter_list)
return [
SearchResult(
link=result["link"],
title=result.get("title"),
snippet=result.get("description"),
)
for result in sorted_results[:count]
]
+379 -66
View File
@@ -7,16 +7,21 @@ from functools import lru_cache
from pathlib import Path
from pydub import AudioSegment
from pydub.silence import split_on_silence
from concurrent.futures import ThreadPoolExecutor
from typing import Optional
from fnmatch import fnmatch
import aiohttp
import aiofiles
import requests
import mimetypes
from urllib.parse import quote
from fastapi import (
Depends,
FastAPI,
File,
Form,
HTTPException,
Request,
UploadFile,
@@ -33,10 +38,12 @@ from open_webui.config import (
WHISPER_MODEL_AUTO_UPDATE,
WHISPER_MODEL_DIR,
CACHE_DIR,
WHISPER_LANGUAGE,
)
from open_webui.constants import ERROR_MESSAGES
from open_webui.env import (
AIOHTTP_CLIENT_SESSION_SSL,
AIOHTTP_CLIENT_TIMEOUT,
ENV,
SRC_LOG_LEVELS,
@@ -48,8 +55,10 @@ from open_webui.env import (
router = APIRouter()
# Constants
MAX_FILE_SIZE_MB = 25
MAX_FILE_SIZE_MB = 20
MAX_FILE_SIZE = MAX_FILE_SIZE_MB * 1024 * 1024 # Convert MB to bytes
AZURE_MAX_FILE_SIZE_MB = 200
AZURE_MAX_FILE_SIZE = AZURE_MAX_FILE_SIZE_MB * 1024 * 1024 # Convert MB to bytes
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["AUDIO"])
@@ -68,27 +77,47 @@ from pydub import AudioSegment
from pydub.utils import mediainfo
def is_mp4_audio(file_path):
"""Check if the given file is an MP4 audio file."""
def is_audio_conversion_required(file_path):
"""
Check if the given audio file needs conversion to mp3.
"""
SUPPORTED_FORMATS = {"flac", "m4a", "mp3", "mp4", "mpeg", "wav", "webm"}
if not os.path.isfile(file_path):
log.error(f"File not found: {file_path}")
return False
info = mediainfo(file_path)
if (
info.get("codec_name") == "aac"
and info.get("codec_type") == "audio"
and info.get("codec_tag_string") == "mp4a"
):
try:
info = mediainfo(file_path)
codec_name = info.get("codec_name", "").lower()
codec_type = info.get("codec_type", "").lower()
codec_tag_string = info.get("codec_tag_string", "").lower()
if codec_name == "aac" and codec_type == "audio" and codec_tag_string == "mp4a":
# File is AAC/mp4a audio, recommend mp3 conversion
return True
# If the codec name is in the supported formats
if codec_name in SUPPORTED_FORMATS:
return False
return True
return False
except Exception as e:
log.error(f"Error getting audio format: {e}")
return False
def convert_mp4_to_wav(file_path, output_path):
"""Convert MP4 audio file to WAV format."""
audio = AudioSegment.from_file(file_path, format="mp4")
audio.export(output_path, format="wav")
log.info(f"Converted {file_path} to {output_path}")
def convert_audio_to_mp3(file_path):
"""Convert audio file to mp3 format."""
try:
output_path = os.path.splitext(file_path)[0] + ".mp3"
audio = AudioSegment.from_file(file_path)
audio.export(output_path, format="mp3")
log.info(f"Converted {file_path} to {output_path}")
return output_path
except Exception as e:
log.error(f"Error converting audio file: {e}")
return None
def set_faster_whisper_model(model: str, auto_update: bool = False):
@@ -131,6 +160,7 @@ class TTSConfigForm(BaseModel):
VOICE: str
SPLIT_ON: str
AZURE_SPEECH_REGION: str
AZURE_SPEECH_BASE_URL: str
AZURE_SPEECH_OUTPUT_FORMAT: str
@@ -139,8 +169,14 @@ class STTConfigForm(BaseModel):
OPENAI_API_KEY: str
ENGINE: str
MODEL: str
SUPPORTED_CONTENT_TYPES: list[str] = []
WHISPER_MODEL: str
DEEPGRAM_API_KEY: str
AZURE_API_KEY: str
AZURE_REGION: str
AZURE_LOCALES: str
AZURE_BASE_URL: str
AZURE_MAX_SPEAKERS: str
class AudioConfigUpdateForm(BaseModel):
@@ -160,6 +196,7 @@ async def get_audio_config(request: Request, user=Depends(get_admin_user)):
"VOICE": request.app.state.config.TTS_VOICE,
"SPLIT_ON": request.app.state.config.TTS_SPLIT_ON,
"AZURE_SPEECH_REGION": request.app.state.config.TTS_AZURE_SPEECH_REGION,
"AZURE_SPEECH_BASE_URL": request.app.state.config.TTS_AZURE_SPEECH_BASE_URL,
"AZURE_SPEECH_OUTPUT_FORMAT": request.app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT,
},
"stt": {
@@ -167,8 +204,14 @@ async def get_audio_config(request: Request, user=Depends(get_admin_user)):
"OPENAI_API_KEY": request.app.state.config.STT_OPENAI_API_KEY,
"ENGINE": request.app.state.config.STT_ENGINE,
"MODEL": request.app.state.config.STT_MODEL,
"SUPPORTED_CONTENT_TYPES": request.app.state.config.STT_SUPPORTED_CONTENT_TYPES,
"WHISPER_MODEL": request.app.state.config.WHISPER_MODEL,
"DEEPGRAM_API_KEY": request.app.state.config.DEEPGRAM_API_KEY,
"AZURE_API_KEY": request.app.state.config.AUDIO_STT_AZURE_API_KEY,
"AZURE_REGION": request.app.state.config.AUDIO_STT_AZURE_REGION,
"AZURE_LOCALES": request.app.state.config.AUDIO_STT_AZURE_LOCALES,
"AZURE_BASE_URL": request.app.state.config.AUDIO_STT_AZURE_BASE_URL,
"AZURE_MAX_SPEAKERS": request.app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS,
},
}
@@ -185,6 +228,9 @@ async def update_audio_config(
request.app.state.config.TTS_VOICE = form_data.tts.VOICE
request.app.state.config.TTS_SPLIT_ON = form_data.tts.SPLIT_ON
request.app.state.config.TTS_AZURE_SPEECH_REGION = form_data.tts.AZURE_SPEECH_REGION
request.app.state.config.TTS_AZURE_SPEECH_BASE_URL = (
form_data.tts.AZURE_SPEECH_BASE_URL
)
request.app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT = (
form_data.tts.AZURE_SPEECH_OUTPUT_FORMAT
)
@@ -193,13 +239,26 @@ async def update_audio_config(
request.app.state.config.STT_OPENAI_API_KEY = form_data.stt.OPENAI_API_KEY
request.app.state.config.STT_ENGINE = form_data.stt.ENGINE
request.app.state.config.STT_MODEL = form_data.stt.MODEL
request.app.state.config.STT_SUPPORTED_CONTENT_TYPES = (
form_data.stt.SUPPORTED_CONTENT_TYPES
)
request.app.state.config.WHISPER_MODEL = form_data.stt.WHISPER_MODEL
request.app.state.config.DEEPGRAM_API_KEY = form_data.stt.DEEPGRAM_API_KEY
request.app.state.config.AUDIO_STT_AZURE_API_KEY = form_data.stt.AZURE_API_KEY
request.app.state.config.AUDIO_STT_AZURE_REGION = form_data.stt.AZURE_REGION
request.app.state.config.AUDIO_STT_AZURE_LOCALES = form_data.stt.AZURE_LOCALES
request.app.state.config.AUDIO_STT_AZURE_BASE_URL = form_data.stt.AZURE_BASE_URL
request.app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS = (
form_data.stt.AZURE_MAX_SPEAKERS
)
if request.app.state.config.STT_ENGINE == "":
request.app.state.faster_whisper_model = set_faster_whisper_model(
form_data.stt.WHISPER_MODEL, WHISPER_MODEL_AUTO_UPDATE
)
else:
request.app.state.faster_whisper_model = None
return {
"tts": {
@@ -211,6 +270,7 @@ async def update_audio_config(
"VOICE": request.app.state.config.TTS_VOICE,
"SPLIT_ON": request.app.state.config.TTS_SPLIT_ON,
"AZURE_SPEECH_REGION": request.app.state.config.TTS_AZURE_SPEECH_REGION,
"AZURE_SPEECH_BASE_URL": request.app.state.config.TTS_AZURE_SPEECH_BASE_URL,
"AZURE_SPEECH_OUTPUT_FORMAT": request.app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT,
},
"stt": {
@@ -218,8 +278,14 @@ async def update_audio_config(
"OPENAI_API_KEY": request.app.state.config.STT_OPENAI_API_KEY,
"ENGINE": request.app.state.config.STT_ENGINE,
"MODEL": request.app.state.config.STT_MODEL,
"SUPPORTED_CONTENT_TYPES": request.app.state.config.STT_SUPPORTED_CONTENT_TYPES,
"WHISPER_MODEL": request.app.state.config.WHISPER_MODEL,
"DEEPGRAM_API_KEY": request.app.state.config.DEEPGRAM_API_KEY,
"AZURE_API_KEY": request.app.state.config.AUDIO_STT_AZURE_API_KEY,
"AZURE_REGION": request.app.state.config.AUDIO_STT_AZURE_REGION,
"AZURE_LOCALES": request.app.state.config.AUDIO_STT_AZURE_LOCALES,
"AZURE_BASE_URL": request.app.state.config.AUDIO_STT_AZURE_BASE_URL,
"AZURE_MAX_SPEAKERS": request.app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS,
},
}
@@ -262,6 +328,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
log.exception(e)
raise HTTPException(status_code=400, detail="Invalid JSON payload")
r = None
if request.app.state.config.TTS_ENGINE == "openai":
payload["model"] = request.app.state.config.TTS_MODEL
@@ -270,7 +337,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
async with aiohttp.ClientSession(
timeout=timeout, trust_env=True
) as session:
async with session.post(
r = await session.post(
url=f"{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech",
json=payload,
headers={
@@ -278,7 +345,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
"Authorization": f"Bearer {request.app.state.config.TTS_OPENAI_API_KEY}",
**(
{
"X-OpenWebUI-User-Name": user.name,
"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,
@@ -287,14 +354,16 @@ async def speech(request: Request, user=Depends(get_verified_user)):
else {}
),
},
) as r:
r.raise_for_status()
ssl=AIOHTTP_CLIENT_SESSION_SSL,
)
async with aiofiles.open(file_path, "wb") as f:
await f.write(await r.read())
r.raise_for_status()
async with aiofiles.open(file_body_path, "w") as f:
await f.write(json.dumps(payload))
async with aiofiles.open(file_path, "wb") as f:
await f.write(await r.read())
async with aiofiles.open(file_body_path, "w") as f:
await f.write(json.dumps(payload))
return FileResponse(file_path)
@@ -302,18 +371,22 @@ async def speech(request: Request, user=Depends(get_verified_user)):
log.exception(e)
detail = None
try:
if r.status != 200:
res = await r.json()
status_code = 500
detail = f"Open WebUI: Server Connection Error"
if r is not None:
status_code = r.status
try:
res = await r.json()
if "error" in res:
detail = f"External: {res['error'].get('message', '')}"
except Exception:
detail = f"External: {e}"
detail = f"External: {res['error']}"
except Exception:
detail = f"External: {e}"
raise HTTPException(
status_code=getattr(r, "status", 500),
detail=detail if detail else "Open WebUI: Server Connection Error",
status_code=status_code,
detail=detail,
)
elif request.app.state.config.TTS_ENGINE == "elevenlabs":
@@ -342,6 +415,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
"Content-Type": "application/json",
"xi-api-key": request.app.state.config.TTS_API_KEY,
},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as r:
r.raise_for_status()
@@ -366,7 +440,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
detail = f"External: {e}"
raise HTTPException(
status_code=getattr(r, "status", 500),
status_code=getattr(r, "status", 500) if r else 500,
detail=detail if detail else "Open WebUI: Server Connection Error",
)
@@ -377,7 +451,8 @@ async def speech(request: Request, user=Depends(get_verified_user)):
log.exception(e)
raise HTTPException(status_code=400, detail="Invalid JSON payload")
region = request.app.state.config.TTS_AZURE_SPEECH_REGION
region = request.app.state.config.TTS_AZURE_SPEECH_REGION or "eastus"
base_url = request.app.state.config.TTS_AZURE_SPEECH_BASE_URL
language = request.app.state.config.TTS_VOICE
locale = "-".join(request.app.state.config.TTS_VOICE.split("-")[:1])
output_format = request.app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT
@@ -391,13 +466,15 @@ async def speech(request: Request, user=Depends(get_verified_user)):
timeout=timeout, trust_env=True
) as session:
async with session.post(
f"https://{region}.tts.speech.microsoft.com/cognitiveservices/v1",
(base_url or f"https://{region}.tts.speech.microsoft.com")
+ "/cognitiveservices/v1",
headers={
"Ocp-Apim-Subscription-Key": request.app.state.config.TTS_API_KEY,
"Content-Type": "application/ssml+xml",
"X-Microsoft-OutputFormat": output_format,
},
data=data,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as r:
r.raise_for_status()
@@ -422,7 +499,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
detail = f"External: {e}"
raise HTTPException(
status_code=getattr(r, "status", 500),
status_code=getattr(r, "status", 500) if r else 500,
detail=detail if detail else "Open WebUI: Server Connection Error",
)
@@ -466,12 +543,13 @@ async def speech(request: Request, user=Depends(get_verified_user)):
return FileResponse(file_path)
def transcribe(request: Request, file_path):
log.info(f"transcribe: {file_path}")
def transcription_handler(request, file_path, metadata):
filename = os.path.basename(file_path)
file_dir = os.path.dirname(file_path)
id = filename.split(".")[0]
metadata = metadata or {}
if request.app.state.config.STT_ENGINE == "":
if request.app.state.faster_whisper_model is None:
request.app.state.faster_whisper_model = set_faster_whisper_model(
@@ -479,7 +557,12 @@ def transcribe(request: Request, file_path):
)
model = request.app.state.faster_whisper_model
segments, info = model.transcribe(file_path, beam_size=5)
segments, info = model.transcribe(
file_path,
beam_size=5,
vad_filter=request.app.state.config.WHISPER_VAD_FILTER,
language=metadata.get("language") or WHISPER_LANGUAGE,
)
log.info(
"Detected language '%s' with probability %f"
% (info.language, info.language_probability)
@@ -496,11 +579,6 @@ def transcribe(request: Request, file_path):
log.debug(data)
return data
elif request.app.state.config.STT_ENGINE == "openai":
if is_mp4_audio(file_path):
os.rename(file_path, file_path.replace(".wav", ".mp4"))
# Convert MP4 audio file to WAV format
convert_mp4_to_wav(file_path.replace(".wav", ".mp4"), file_path)
r = None
try:
r = requests.post(
@@ -509,7 +587,14 @@ def transcribe(request: Request, file_path):
"Authorization": f"Bearer {request.app.state.config.STT_OPENAI_API_KEY}"
},
files={"file": (filename, open(file_path, "rb"))},
data={"model": request.app.state.config.STT_MODEL},
data={
"model": request.app.state.config.STT_MODEL,
**(
{"language": metadata.get("language")}
if metadata.get("language")
else {}
),
},
)
r.raise_for_status()
@@ -559,7 +644,7 @@ def transcribe(request: Request, file_path):
# Make request to Deepgram API
r = requests.post(
"https://api.deepgram.com/v1/listen",
"https://api.deepgram.com/v1/listen?smart_format=true",
headers=headers,
params=params,
data=file_data,
@@ -598,36 +683,262 @@ def transcribe(request: Request, file_path):
detail = f"External: {e}"
raise Exception(detail if detail else "Open WebUI: Server Connection Error")
elif request.app.state.config.STT_ENGINE == "azure":
# Check file exists and size
if not os.path.exists(file_path):
raise HTTPException(status_code=400, detail="Audio file not found")
# Check file size (Azure has a larger limit of 200MB)
file_size = os.path.getsize(file_path)
if file_size > AZURE_MAX_FILE_SIZE:
raise HTTPException(
status_code=400,
detail=f"File size exceeds Azure's limit of {AZURE_MAX_FILE_SIZE_MB}MB",
)
api_key = request.app.state.config.AUDIO_STT_AZURE_API_KEY
region = request.app.state.config.AUDIO_STT_AZURE_REGION or "eastus"
locales = request.app.state.config.AUDIO_STT_AZURE_LOCALES
base_url = request.app.state.config.AUDIO_STT_AZURE_BASE_URL
max_speakers = request.app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS or 3
# IF NO LOCALES, USE DEFAULTS
if len(locales) < 2:
locales = [
"en-US",
"es-ES",
"es-MX",
"fr-FR",
"hi-IN",
"it-IT",
"de-DE",
"en-GB",
"en-IN",
"ja-JP",
"ko-KR",
"pt-BR",
"zh-CN",
]
locales = ",".join(locales)
if not api_key or not region:
raise HTTPException(
status_code=400,
detail="Azure API key is required for Azure STT",
)
r = None
try:
# Prepare the request
data = {
"definition": json.dumps(
{
"locales": locales.split(","),
"diarization": {"maxSpeakers": max_speakers, "enabled": True},
}
if locales
else {}
)
}
url = (
base_url or f"https://{region}.api.cognitive.microsoft.com"
) + "/speechtotext/transcriptions:transcribe?api-version=2024-11-15"
# Use context manager to ensure file is properly closed
with open(file_path, "rb") as audio_file:
r = requests.post(
url=url,
files={"audio": audio_file},
data=data,
headers={
"Ocp-Apim-Subscription-Key": api_key,
},
)
r.raise_for_status()
response = r.json()
# Extract transcript from response
if not response.get("combinedPhrases"):
raise ValueError("No transcription found in response")
# Get the full transcript from combinedPhrases
transcript = response["combinedPhrases"][0].get("text", "").strip()
if not transcript:
raise ValueError("Empty transcript in response")
data = {"text": transcript}
# Save transcript to json file (consistent with other providers)
transcript_file = f"{file_dir}/{id}.json"
with open(transcript_file, "w") as f:
json.dump(data, f)
log.debug(data)
return data
except (KeyError, IndexError, ValueError) as e:
log.exception("Error parsing Azure response")
raise HTTPException(
status_code=500,
detail=f"Failed to parse Azure response: {str(e)}",
)
except requests.exceptions.RequestException as e:
log.exception(e)
detail = None
try:
if r is not None and r.status_code != 200:
res = r.json()
if "error" in res:
detail = f"External: {res['error'].get('message', '')}"
except Exception:
detail = f"External: {e}"
raise HTTPException(
status_code=getattr(r, "status_code", 500) if r else 500,
detail=detail if detail else "Open WebUI: Server Connection Error",
)
def transcribe(request: Request, file_path: str, metadata: Optional[dict] = None):
log.info(f"transcribe: {file_path} {metadata}")
if is_audio_conversion_required(file_path):
file_path = convert_audio_to_mp3(file_path)
try:
file_path = compress_audio(file_path)
except Exception as e:
log.exception(e)
# Always produce a list of chunk paths (could be one entry if small)
try:
chunk_paths = split_audio(file_path, MAX_FILE_SIZE)
print(f"Chunk paths: {chunk_paths}")
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
results = []
try:
with ThreadPoolExecutor() as executor:
# Submit tasks for each chunk_path
futures = [
executor.submit(transcription_handler, request, chunk_path, metadata)
for chunk_path in chunk_paths
]
# Gather results as they complete
for future in futures:
try:
results.append(future.result())
except Exception as transcribe_exc:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error transcribing chunk: {transcribe_exc}",
)
finally:
# Clean up only the temporary chunks, never the original file
for chunk_path in chunk_paths:
if chunk_path != file_path and os.path.isfile(chunk_path):
try:
os.remove(chunk_path)
except Exception:
pass
return {
"text": " ".join([result["text"] for result in results]),
}
def compress_audio(file_path):
if os.path.getsize(file_path) > MAX_FILE_SIZE:
id = os.path.splitext(os.path.basename(file_path))[
0
] # Handles names with multiple dots
file_dir = os.path.dirname(file_path)
audio = AudioSegment.from_file(file_path)
audio = audio.set_frame_rate(16000).set_channels(1) # Compress audio
compressed_path = f"{file_dir}/{id}_compressed.opus"
audio.export(compressed_path, format="opus", bitrate="32k")
log.debug(f"Compressed audio to {compressed_path}")
if (
os.path.getsize(compressed_path) > MAX_FILE_SIZE
): # Still larger than MAX_FILE_SIZE after compression
raise Exception(ERROR_MESSAGES.FILE_TOO_LARGE(size=f"{MAX_FILE_SIZE_MB}MB"))
compressed_path = os.path.join(file_dir, f"{id}_compressed.mp3")
audio.export(compressed_path, format="mp3", bitrate="32k")
# log.debug(f"Compressed audio to {compressed_path}") # Uncomment if log is defined
return compressed_path
else:
return file_path
def split_audio(file_path, max_bytes, format="mp3", bitrate="32k"):
"""
Splits audio into chunks not exceeding max_bytes.
Returns a list of chunk file paths. If audio fits, returns list with original path.
"""
file_size = os.path.getsize(file_path)
if file_size <= max_bytes:
return [file_path] # Nothing to split
audio = AudioSegment.from_file(file_path)
duration_ms = len(audio)
orig_size = file_size
approx_chunk_ms = max(int(duration_ms * (max_bytes / orig_size)) - 1000, 1000)
chunks = []
start = 0
i = 0
base, _ = os.path.splitext(file_path)
while start < duration_ms:
end = min(start + approx_chunk_ms, duration_ms)
chunk = audio[start:end]
chunk_path = f"{base}_chunk_{i}.{format}"
chunk.export(chunk_path, format=format, bitrate=bitrate)
# Reduce chunk duration if still too large
while os.path.getsize(chunk_path) > max_bytes and (end - start) > 5000:
end = start + ((end - start) // 2)
chunk = audio[start:end]
chunk.export(chunk_path, format=format, bitrate=bitrate)
if os.path.getsize(chunk_path) > max_bytes:
os.remove(chunk_path)
raise Exception("Audio chunk cannot be reduced below max file size.")
chunks.append(chunk_path)
start = end
i += 1
return chunks
@router.post("/transcriptions")
def transcription(
request: Request,
file: UploadFile = File(...),
language: Optional[str] = Form(None),
user=Depends(get_verified_user),
):
log.info(f"file.content_type: {file.content_type}")
supported_filetypes = ("audio/mpeg", "audio/wav", "audio/ogg", "audio/x-m4a")
stt_supported_content_types = getattr(
request.app.state.config, "STT_SUPPORTED_CONTENT_TYPES", []
)
if not file.content_type.startswith(supported_filetypes):
if not any(
fnmatch(file.content_type, content_type)
for content_type in (
stt_supported_content_types
if stt_supported_content_types
and any(t.strip() for t in stt_supported_content_types)
else ["audio/*", "video/webm"]
)
):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.FILE_NOT_SUPPORTED,
@@ -648,19 +959,18 @@ def transcription(
f.write(contents)
try:
try:
file_path = compress_audio(file_path)
except Exception as e:
log.exception(e)
metadata = None
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
if language:
metadata = {"language": language}
result = transcribe(request, file_path, metadata)
return {
**result,
"filename": os.path.basename(file_path),
}
data = transcribe(request, file_path)
file_path = file_path.split("/")[-1]
return {**data, "filename": file_path}
except Exception as e:
log.exception(e)
@@ -769,7 +1079,10 @@ def get_available_voices(request) -> dict:
elif request.app.state.config.TTS_ENGINE == "azure":
try:
region = request.app.state.config.TTS_AZURE_SPEECH_REGION
url = f"https://{region}.tts.speech.microsoft.com/cognitiveservices/voices/list"
base_url = request.app.state.config.TTS_AZURE_SPEECH_BASE_URL
url = (
base_url or f"https://{region}.tts.speech.microsoft.com"
) + "/cognitiveservices/voices/list"
headers = {
"Ocp-Apim-Subscription-Key": request.app.state.config.TTS_API_KEY
}
+249 -58
View File
@@ -19,39 +19,44 @@ from open_webui.models.auths import (
UserResponse,
)
from open_webui.models.users import Users
from open_webui.models.groups import Groups
from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
from open_webui.env import (
WEBUI_AUTH,
WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
WEBUI_AUTH_TRUSTED_NAME_HEADER,
WEBUI_AUTH_TRUSTED_GROUPS_HEADER,
WEBUI_AUTH_COOKIE_SAME_SITE,
WEBUI_AUTH_COOKIE_SECURE,
WEBUI_AUTH_SIGNOUT_REDIRECT_URL,
SRC_LOG_LEVELS,
)
from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.responses import RedirectResponse, Response
from fastapi.responses import RedirectResponse, Response, JSONResponse
from open_webui.config import OPENID_PROVIDER_URL, ENABLE_OAUTH_SIGNUP, ENABLE_LDAP
from pydantic import BaseModel
from open_webui.utils.misc import parse_duration, validate_email_format
from open_webui.utils.auth import (
decode_token,
create_api_key,
create_token,
get_admin_user,
get_verified_user,
get_current_user,
get_password_hash,
get_http_authorization_cred,
)
from open_webui.utils.webhook import post_webhook
from open_webui.utils.access_control import get_permissions
from typing import Optional, List
from ssl import CERT_REQUIRED, PROTOCOL_TLS
from ssl import CERT_NONE, CERT_REQUIRED, PROTOCOL_TLS
if ENABLE_LDAP.value:
from ldap3 import Server, Connection, NONE, Tls
from ldap3.utils.conv import escape_filter_chars
from ldap3 import Server, Connection, NONE, Tls
from ldap3.utils.conv import escape_filter_chars
router = APIRouter()
@@ -72,31 +77,36 @@ class SessionUserResponse(Token, UserResponse):
async def get_session_user(
request: Request, response: Response, user=Depends(get_current_user)
):
expires_delta = parse_duration(request.app.state.config.JWT_EXPIRES_IN)
auth_header = request.headers.get("Authorization")
auth_token = get_http_authorization_cred(auth_header)
token = auth_token.credentials
data = decode_token(token)
expires_at = None
if expires_delta:
expires_at = int(time.time()) + int(expires_delta.total_seconds())
token = create_token(
data={"id": user.id},
expires_delta=expires_delta,
)
if data:
expires_at = data.get("exp")
datetime_expires_at = (
datetime.datetime.fromtimestamp(expires_at, datetime.timezone.utc)
if expires_at
else None
)
if (expires_at is not None) and int(time.time()) > expires_at:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.INVALID_TOKEN,
)
# Set the cookie token
response.set_cookie(
key="token",
value=token,
expires=datetime_expires_at,
httponly=True, # Ensures the cookie is not accessible via JavaScript
samesite=WEBUI_AUTH_COOKIE_SAME_SITE,
secure=WEBUI_AUTH_COOKIE_SECURE,
)
# Set the cookie token
response.set_cookie(
key="token",
value=token,
expires=(
datetime.datetime.fromtimestamp(expires_at, datetime.timezone.utc)
if expires_at
else None
),
httponly=True, # Ensures the cookie is not accessible via JavaScript
samesite=WEBUI_AUTH_COOKIE_SAME_SITE,
secure=WEBUI_AUTH_COOKIE_SECURE,
)
user_permissions = get_permissions(
user.id, request.app.state.config.USER_PERMISSIONS
@@ -177,6 +187,9 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
LDAP_APP_PASSWORD = request.app.state.config.LDAP_APP_PASSWORD
LDAP_USE_TLS = request.app.state.config.LDAP_USE_TLS
LDAP_CA_CERT_FILE = request.app.state.config.LDAP_CA_CERT_FILE
LDAP_VALIDATE_CERT = (
CERT_REQUIRED if request.app.state.config.LDAP_VALIDATE_CERT else CERT_NONE
)
LDAP_CIPHERS = (
request.app.state.config.LDAP_CIPHERS
if request.app.state.config.LDAP_CIPHERS
@@ -188,7 +201,7 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
try:
tls = Tls(
validate=CERT_REQUIRED,
validate=LDAP_VALIDATE_CERT,
version=PROTOCOL_TLS,
ca_certs_file=LDAP_CA_CERT_FILE,
ciphers=LDAP_CIPHERS,
@@ -215,30 +228,115 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
if not connection_app.bind():
raise HTTPException(400, detail="Application account bind failed")
ENABLE_LDAP_GROUP_MANAGEMENT = (
request.app.state.config.ENABLE_LDAP_GROUP_MANAGEMENT
)
ENABLE_LDAP_GROUP_CREATION = request.app.state.config.ENABLE_LDAP_GROUP_CREATION
LDAP_ATTRIBUTE_FOR_GROUPS = request.app.state.config.LDAP_ATTRIBUTE_FOR_GROUPS
search_attributes = [
f"{LDAP_ATTRIBUTE_FOR_USERNAME}",
f"{LDAP_ATTRIBUTE_FOR_MAIL}",
"cn",
]
if ENABLE_LDAP_GROUP_MANAGEMENT:
search_attributes.append(f"{LDAP_ATTRIBUTE_FOR_GROUPS}")
log.info(
f"LDAP Group Management enabled. Adding {LDAP_ATTRIBUTE_FOR_GROUPS} to search attributes"
)
log.info(f"LDAP search attributes: {search_attributes}")
search_success = connection_app.search(
search_base=LDAP_SEARCH_BASE,
search_filter=f"(&({LDAP_ATTRIBUTE_FOR_USERNAME}={escape_filter_chars(form_data.user.lower())}){LDAP_SEARCH_FILTERS})",
attributes=[
f"{LDAP_ATTRIBUTE_FOR_USERNAME}",
f"{LDAP_ATTRIBUTE_FOR_MAIL}",
"cn",
],
attributes=search_attributes,
)
if not search_success:
if not search_success or not connection_app.entries:
raise HTTPException(400, detail="User not found in the LDAP server")
entry = connection_app.entries[0]
username = str(entry[f"{LDAP_ATTRIBUTE_FOR_USERNAME}"]).lower()
email = str(entry[f"{LDAP_ATTRIBUTE_FOR_MAIL}"])
if not email or email == "" or email == "[]":
email = entry[
f"{LDAP_ATTRIBUTE_FOR_MAIL}"
].value # retrieve the Attribute value
if not email:
raise HTTPException(400, "User does not have a valid email address.")
else:
elif isinstance(email, str):
email = email.lower()
elif isinstance(email, list):
email = email[0].lower()
else:
email = str(email).lower()
cn = str(entry["cn"])
user_dn = entry.entry_dn
user_groups = []
if ENABLE_LDAP_GROUP_MANAGEMENT and LDAP_ATTRIBUTE_FOR_GROUPS in entry:
group_dns = entry[LDAP_ATTRIBUTE_FOR_GROUPS]
log.info(f"LDAP raw group DNs for user {username}: {group_dns}")
if group_dns:
log.info(f"LDAP group_dns original: {group_dns}")
log.info(f"LDAP group_dns type: {type(group_dns)}")
log.info(f"LDAP group_dns length: {len(group_dns)}")
if hasattr(group_dns, "value"):
group_dns = group_dns.value
log.info(f"Extracted .value property: {group_dns}")
elif hasattr(group_dns, "__iter__") and not isinstance(
group_dns, (str, bytes)
):
group_dns = list(group_dns)
log.info(f"Converted to list: {group_dns}")
if isinstance(group_dns, list):
group_dns = [str(item) for item in group_dns]
else:
group_dns = [str(group_dns)]
log.info(
f"LDAP group_dns after processing - type: {type(group_dns)}, length: {len(group_dns)}"
)
for group_idx, group_dn in enumerate(group_dns):
group_dn = str(group_dn)
log.info(f"Processing group DN #{group_idx + 1}: {group_dn}")
try:
group_cn = None
for item in group_dn.split(","):
item = item.strip()
if item.upper().startswith("CN="):
group_cn = item[3:]
break
if group_cn:
user_groups.append(group_cn)
else:
log.warning(
f"Could not extract CN from group DN: {group_dn}"
)
except Exception as e:
log.warning(
f"Failed to extract group name from DN {group_dn}: {e}"
)
log.info(
f"LDAP groups for user {username}: {user_groups} (total: {len(user_groups)})"
)
else:
log.info(f"No groups found for user {username}")
elif ENABLE_LDAP_GROUP_MANAGEMENT:
log.warning(
f"LDAP Group Management enabled but {LDAP_ATTRIBUTE_FOR_GROUPS} attribute not found in user entry"
)
if username == form_data.user.lower():
connection_user = Connection(
server,
@@ -281,30 +379,59 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
500, detail="Internal error occurred during LDAP user creation."
)
user = Auths.authenticate_user_by_trusted_header(email)
user = Auths.authenticate_user_by_email(email)
if user:
expires_delta = parse_duration(request.app.state.config.JWT_EXPIRES_IN)
expires_at = None
if expires_delta:
expires_at = int(time.time()) + int(expires_delta.total_seconds())
token = create_token(
data={"id": user.id},
expires_delta=parse_duration(
request.app.state.config.JWT_EXPIRES_IN
),
expires_delta=expires_delta,
)
# Set the cookie token
response.set_cookie(
key="token",
value=token,
expires=(
datetime.datetime.fromtimestamp(
expires_at, datetime.timezone.utc
)
if expires_at
else None
),
httponly=True, # Ensures the cookie is not accessible via JavaScript
samesite=WEBUI_AUTH_COOKIE_SAME_SITE,
secure=WEBUI_AUTH_COOKIE_SECURE,
)
user_permissions = get_permissions(
user.id, request.app.state.config.USER_PERMISSIONS
)
if (
user.role != "admin"
and ENABLE_LDAP_GROUP_MANAGEMENT
and user_groups
):
if ENABLE_LDAP_GROUP_CREATION:
Groups.create_groups_by_group_names(user.id, user_groups)
try:
Groups.sync_groups_by_group_names(user.id, user_groups)
log.info(
f"Successfully synced groups for user {user.id}: {user_groups}"
)
except Exception as e:
log.error(f"Failed to sync groups for user {user.id}: {e}")
return {
"token": token,
"token_type": "Bearer",
"expires_at": expires_at,
"id": user.id,
"email": user.email,
"name": user.name,
@@ -332,21 +459,29 @@ async def signin(request: Request, response: Response, form_data: SigninForm):
if WEBUI_AUTH_TRUSTED_EMAIL_HEADER not in request.headers:
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_TRUSTED_HEADER)
trusted_email = request.headers[WEBUI_AUTH_TRUSTED_EMAIL_HEADER].lower()
trusted_name = trusted_email
email = request.headers[WEBUI_AUTH_TRUSTED_EMAIL_HEADER].lower()
name = email
if WEBUI_AUTH_TRUSTED_NAME_HEADER:
trusted_name = request.headers.get(
WEBUI_AUTH_TRUSTED_NAME_HEADER, trusted_email
)
if not Users.get_user_by_email(trusted_email.lower()):
name = request.headers.get(WEBUI_AUTH_TRUSTED_NAME_HEADER, email)
if not Users.get_user_by_email(email.lower()):
await signup(
request,
response,
SignupForm(
email=trusted_email, password=str(uuid.uuid4()), name=trusted_name
),
SignupForm(email=email, password=str(uuid.uuid4()), name=name),
)
user = Auths.authenticate_user_by_trusted_header(trusted_email)
user = Auths.authenticate_user_by_email(email)
if WEBUI_AUTH_TRUSTED_GROUPS_HEADER and user and user.role != "admin":
group_names = request.headers.get(
WEBUI_AUTH_TRUSTED_GROUPS_HEADER, ""
).split(",")
group_names = [name.strip() for name in group_names if name.strip()]
if group_names:
Groups.sync_groups_by_group_names(user.id, group_names)
elif WEBUI_AUTH == False:
admin_email = "admin@localhost"
admin_password = "admin"
@@ -450,9 +585,12 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
"admin" if user_count == 0 else request.app.state.config.DEFAULT_USER_ROLE
)
if user_count == 0:
# Disable signup after the first user is created
request.app.state.config.ENABLE_SIGNUP = False
# The password passed to bcrypt must be 72 bytes or fewer. If it is longer, it will be truncated before hashing.
if len(form_data.password.encode("utf-8")) > 72:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.PASSWORD_TOO_LONG,
)
hashed = get_password_hash(form_data.password)
user = Auths.insert_new_auth(
@@ -506,6 +644,10 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
user.id, request.app.state.config.USER_PERMISSIONS
)
if user_count == 0:
# Disable signup after the first user is created
request.app.state.config.ENABLE_SIGNUP = False
return {
"token": token,
"token_type": "Bearer",
@@ -527,21 +669,32 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
@router.get("/signout")
async def signout(request: Request, response: Response):
response.delete_cookie("token")
response.delete_cookie("oui-session")
if ENABLE_OAUTH_SIGNUP.value:
oauth_id_token = request.cookies.get("oauth_id_token")
if oauth_id_token:
try:
async with ClientSession() as session:
async with ClientSession(trust_env=True) as session:
async with session.get(OPENID_PROVIDER_URL.value) as resp:
if resp.status == 200:
openid_data = await resp.json()
logout_url = openid_data.get("end_session_endpoint")
if logout_url:
response.delete_cookie("oauth_id_token")
return RedirectResponse(
return JSONResponse(
status_code=200,
content={
"status": True,
"redirect_url": f"{logout_url}?id_token_hint={oauth_id_token}"
+ (
f"&post_logout_redirect_uri={WEBUI_AUTH_SIGNOUT_REDIRECT_URL}"
if WEBUI_AUTH_SIGNOUT_REDIRECT_URL
else ""
),
},
headers=response.headers,
url=f"{logout_url}?id_token_hint={oauth_id_token}",
)
else:
raise HTTPException(
@@ -555,7 +708,19 @@ async def signout(request: Request, response: Response):
detail="Failed to sign out from the OpenID provider.",
)
return {"status": True}
if WEBUI_AUTH_SIGNOUT_REDIRECT_URL:
return JSONResponse(
status_code=200,
content={
"status": True,
"redirect_url": WEBUI_AUTH_SIGNOUT_REDIRECT_URL,
},
headers=response.headers,
)
return JSONResponse(
status_code=200, content={"status": True}, headers=response.headers
)
############################
@@ -653,7 +818,11 @@ async def get_admin_config(request: Request, user=Depends(get_admin_user)):
"ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING,
"ENABLE_MESSAGE_RATING": request.app.state.config.ENABLE_MESSAGE_RATING,
"ENABLE_CHANNELS": request.app.state.config.ENABLE_CHANNELS,
"ENABLE_NOTES": request.app.state.config.ENABLE_NOTES,
"ENABLE_USER_WEBHOOKS": request.app.state.config.ENABLE_USER_WEBHOOKS,
"PENDING_USER_OVERLAY_TITLE": request.app.state.config.PENDING_USER_OVERLAY_TITLE,
"PENDING_USER_OVERLAY_CONTENT": request.app.state.config.PENDING_USER_OVERLAY_CONTENT,
"RESPONSE_WATERMARK": request.app.state.config.RESPONSE_WATERMARK,
}
@@ -669,7 +838,11 @@ class AdminConfig(BaseModel):
ENABLE_COMMUNITY_SHARING: bool
ENABLE_MESSAGE_RATING: bool
ENABLE_CHANNELS: bool
ENABLE_NOTES: bool
ENABLE_USER_WEBHOOKS: bool
PENDING_USER_OVERLAY_TITLE: Optional[str] = None
PENDING_USER_OVERLAY_CONTENT: Optional[str] = None
RESPONSE_WATERMARK: Optional[str] = None
@router.post("/admin/config")
@@ -689,6 +862,7 @@ async def update_admin_config(
)
request.app.state.config.ENABLE_CHANNELS = form_data.ENABLE_CHANNELS
request.app.state.config.ENABLE_NOTES = form_data.ENABLE_NOTES
if form_data.DEFAULT_USER_ROLE in ["pending", "user", "admin"]:
request.app.state.config.DEFAULT_USER_ROLE = form_data.DEFAULT_USER_ROLE
@@ -706,6 +880,15 @@ async def update_admin_config(
request.app.state.config.ENABLE_USER_WEBHOOKS = form_data.ENABLE_USER_WEBHOOKS
request.app.state.config.PENDING_USER_OVERLAY_TITLE = (
form_data.PENDING_USER_OVERLAY_TITLE
)
request.app.state.config.PENDING_USER_OVERLAY_CONTENT = (
form_data.PENDING_USER_OVERLAY_CONTENT
)
request.app.state.config.RESPONSE_WATERMARK = form_data.RESPONSE_WATERMARK
return {
"SHOW_ADMIN_DETAILS": request.app.state.config.SHOW_ADMIN_DETAILS,
"WEBUI_URL": request.app.state.config.WEBUI_URL,
@@ -713,12 +896,16 @@ async def update_admin_config(
"ENABLE_API_KEY": request.app.state.config.ENABLE_API_KEY,
"ENABLE_API_KEY_ENDPOINT_RESTRICTIONS": request.app.state.config.ENABLE_API_KEY_ENDPOINT_RESTRICTIONS,
"API_KEY_ALLOWED_ENDPOINTS": request.app.state.config.API_KEY_ALLOWED_ENDPOINTS,
"ENABLE_CHANNELS": request.app.state.config.ENABLE_CHANNELS,
"DEFAULT_USER_ROLE": request.app.state.config.DEFAULT_USER_ROLE,
"JWT_EXPIRES_IN": request.app.state.config.JWT_EXPIRES_IN,
"ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING,
"ENABLE_MESSAGE_RATING": request.app.state.config.ENABLE_MESSAGE_RATING,
"ENABLE_CHANNELS": request.app.state.config.ENABLE_CHANNELS,
"ENABLE_NOTES": request.app.state.config.ENABLE_NOTES,
"ENABLE_USER_WEBHOOKS": request.app.state.config.ENABLE_USER_WEBHOOKS,
"PENDING_USER_OVERLAY_TITLE": request.app.state.config.PENDING_USER_OVERLAY_TITLE,
"PENDING_USER_OVERLAY_CONTENT": request.app.state.config.PENDING_USER_OVERLAY_CONTENT,
"RESPONSE_WATERMARK": request.app.state.config.RESPONSE_WATERMARK,
}
@@ -734,6 +921,7 @@ class LdapServerConfig(BaseModel):
search_filters: str = ""
use_tls: bool = True
certificate_path: Optional[str] = None
validate_cert: bool = True
ciphers: Optional[str] = "ALL"
@@ -751,6 +939,7 @@ async def get_ldap_server(request: Request, user=Depends(get_admin_user)):
"search_filters": request.app.state.config.LDAP_SEARCH_FILTERS,
"use_tls": request.app.state.config.LDAP_USE_TLS,
"certificate_path": request.app.state.config.LDAP_CA_CERT_FILE,
"validate_cert": request.app.state.config.LDAP_VALIDATE_CERT,
"ciphers": request.app.state.config.LDAP_CIPHERS,
}
@@ -786,6 +975,7 @@ async def update_ldap_server(
request.app.state.config.LDAP_SEARCH_FILTERS = form_data.search_filters
request.app.state.config.LDAP_USE_TLS = form_data.use_tls
request.app.state.config.LDAP_CA_CERT_FILE = form_data.certificate_path
request.app.state.config.LDAP_VALIDATE_CERT = form_data.validate_cert
request.app.state.config.LDAP_CIPHERS = form_data.ciphers
return {
@@ -800,6 +990,7 @@ async def update_ldap_server(
"search_filters": request.app.state.config.LDAP_SEARCH_FILTERS,
"use_tls": request.app.state.config.LDAP_USE_TLS,
"certificate_path": request.app.state.config.LDAP_CA_CERT_FILE,
"validate_cert": request.app.state.config.LDAP_VALIDATE_CERT,
"ciphers": request.app.state.config.LDAP_CIPHERS,
}
+6 -2
View File
@@ -40,10 +40,14 @@ router = APIRouter()
@router.get("/", response_model=list[ChannelModel])
async def get_channels(user=Depends(get_verified_user)):
return Channels.get_channels_by_user_id(user.id)
@router.get("/list", response_model=list[ChannelModel])
async def get_all_channels(user=Depends(get_verified_user)):
if user.role == "admin":
return Channels.get_channels()
else:
return Channels.get_channels_by_user_id(user.id)
return Channels.get_channels_by_user_id(user.id)
############################
+84 -15
View File
@@ -39,13 +39,21 @@ router = APIRouter()
async def get_session_user_chat_list(
user=Depends(get_verified_user), page: Optional[int] = None
):
if page is not None:
limit = 60
skip = (page - 1) * limit
try:
if page is not None:
limit = 60
skip = (page - 1) * limit
return Chats.get_chat_title_id_list_by_user_id(user.id, skip=skip, limit=limit)
else:
return Chats.get_chat_title_id_list_by_user_id(user.id)
return Chats.get_chat_title_id_list_by_user_id(
user.id, skip=skip, limit=limit
)
else:
return Chats.get_chat_title_id_list_by_user_id(user.id)
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
)
############################
@@ -76,17 +84,34 @@ async def delete_all_user_chats(request: Request, user=Depends(get_verified_user
@router.get("/list/user/{user_id}", response_model=list[ChatTitleIdResponse])
async def get_user_chat_list_by_user_id(
user_id: str,
page: Optional[int] = None,
query: Optional[str] = None,
order_by: Optional[str] = None,
direction: Optional[str] = None,
user=Depends(get_admin_user),
skip: int = 0,
limit: int = 50,
):
if not ENABLE_ADMIN_CHAT_ACCESS:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
if page is None:
page = 1
limit = 60
skip = (page - 1) * limit
filter = {}
if query:
filter["query"] = query
if order_by:
filter["order_by"] = order_by
if direction:
filter["direction"] = direction
return Chats.get_chat_list_by_user_id(
user_id, include_archived=True, skip=skip, limit=limit
user_id, include_archived=True, filter=filter, skip=skip, limit=limit
)
@@ -194,10 +219,10 @@ async def get_chats_by_folder_id(folder_id: str, user=Depends(get_verified_user)
############################
@router.get("/pinned", response_model=list[ChatResponse])
@router.get("/pinned", response_model=list[ChatTitleIdResponse])
async def get_user_pinned_chats(user=Depends(get_verified_user)):
return [
ChatResponse(**chat.model_dump())
ChatTitleIdResponse(**chat.model_dump())
for chat in Chats.get_pinned_chats_by_user_id(user.id)
]
@@ -267,9 +292,37 @@ async def get_all_user_chats_in_db(user=Depends(get_admin_user)):
@router.get("/archived", response_model=list[ChatTitleIdResponse])
async def get_archived_session_user_chat_list(
user=Depends(get_verified_user), skip: int = 0, limit: int = 50
page: Optional[int] = None,
query: Optional[str] = None,
order_by: Optional[str] = None,
direction: Optional[str] = None,
user=Depends(get_verified_user),
):
return Chats.get_archived_chat_list_by_user_id(user.id, skip, limit)
if page is None:
page = 1
limit = 60
skip = (page - 1) * limit
filter = {}
if query:
filter["query"] = query
if order_by:
filter["order_by"] = order_by
if direction:
filter["direction"] = direction
chat_list = [
ChatTitleIdResponse(**chat.model_dump())
for chat in Chats.get_archived_chat_list_by_user_id(
user.id,
filter=filter,
skip=skip,
limit=limit,
)
]
return chat_list
############################
@@ -579,7 +632,12 @@ async def clone_chat_by_id(
@router.post("/{id}/clone/shared", response_model=Optional[ChatResponse])
async def clone_shared_chat_by_id(id: str, user=Depends(get_verified_user)):
chat = Chats.get_chat_by_share_id(id)
if user.role == "admin":
chat = Chats.get_chat_by_id(id)
else:
chat = Chats.get_chat_by_share_id(id)
if chat:
updated_chat = {
**chat.chat,
@@ -633,8 +691,19 @@ async def archive_chat_by_id(id: str, user=Depends(get_verified_user)):
@router.post("/{id}/share", response_model=Optional[ChatResponse])
async def share_chat_by_id(id: str, user=Depends(get_verified_user)):
async def share_chat_by_id(request: Request, id: str, user=Depends(get_verified_user)):
if (user.role != "admin") and (
not has_permission(
user.id, "chat.share", request.app.state.config.USER_PERMISSIONS
)
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
if chat:
if chat.share_id:
shared_chat = Chats.update_shared_chat_by_chat_id(chat.id)
+14 -7
View File
@@ -39,32 +39,39 @@ async def export_config(user=Depends(get_admin_user)):
############################
# Direct Connections Config
# Connections Config
############################
class DirectConnectionsConfigForm(BaseModel):
class ConnectionsConfigForm(BaseModel):
ENABLE_DIRECT_CONNECTIONS: bool
ENABLE_BASE_MODELS_CACHE: bool
@router.get("/direct_connections", response_model=DirectConnectionsConfigForm)
async def get_direct_connections_config(request: Request, user=Depends(get_admin_user)):
@router.get("/connections", response_model=ConnectionsConfigForm)
async def get_connections_config(request: Request, user=Depends(get_admin_user)):
return {
"ENABLE_DIRECT_CONNECTIONS": request.app.state.config.ENABLE_DIRECT_CONNECTIONS,
"ENABLE_BASE_MODELS_CACHE": request.app.state.config.ENABLE_BASE_MODELS_CACHE,
}
@router.post("/direct_connections", response_model=DirectConnectionsConfigForm)
async def set_direct_connections_config(
@router.post("/connections", response_model=ConnectionsConfigForm)
async def set_connections_config(
request: Request,
form_data: DirectConnectionsConfigForm,
form_data: ConnectionsConfigForm,
user=Depends(get_admin_user),
):
request.app.state.config.ENABLE_DIRECT_CONNECTIONS = (
form_data.ENABLE_DIRECT_CONNECTIONS
)
request.app.state.config.ENABLE_BASE_MODELS_CACHE = (
form_data.ENABLE_BASE_MODELS_CACHE
)
return {
"ENABLE_DIRECT_CONNECTIONS": request.app.state.config.ENABLE_DIRECT_CONNECTIONS,
"ENABLE_BASE_MODELS_CACHE": request.app.state.config.ENABLE_BASE_MODELS_CACHE,
}
+13 -16
View File
@@ -56,7 +56,7 @@ async def update_config(
}
class FeedbackUserReponse(BaseModel):
class UserResponse(BaseModel):
id: str
name: str
email: str
@@ -68,21 +68,23 @@ class FeedbackUserReponse(BaseModel):
class FeedbackUserResponse(FeedbackResponse):
user: Optional[FeedbackUserReponse] = None
user: Optional[UserResponse] = None
@router.get("/feedbacks/all", response_model=list[FeedbackUserResponse])
async def get_all_feedbacks(user=Depends(get_admin_user)):
feedbacks = Feedbacks.get_all_feedbacks()
return [
FeedbackUserResponse(
**feedback.model_dump(),
user=FeedbackUserReponse(
**Users.get_user_by_id(feedback.user_id).model_dump()
),
feedback_list = []
for feedback in feedbacks:
user = Users.get_user_by_id(feedback.user_id)
feedback_list.append(
FeedbackUserResponse(
**feedback.model_dump(),
user=UserResponse(**user.model_dump()) if user else None,
)
)
for feedback in feedbacks
]
return feedback_list
@router.delete("/feedbacks/all")
@@ -94,12 +96,7 @@ async def delete_all_feedbacks(user=Depends(get_admin_user)):
@router.get("/feedbacks/all/export", response_model=list[FeedbackModel])
async def get_all_feedbacks(user=Depends(get_admin_user)):
feedbacks = Feedbacks.get_all_feedbacks()
return [
FeedbackModel(
**feedback.model_dump(), user=Users.get_user_by_id(feedback.user_id)
)
for feedback in feedbacks
]
return feedbacks
@router.get("/feedbacks/user", response_model=list[FeedbackUserResponse])
+126 -19
View File
@@ -1,6 +1,8 @@
import logging
import os
import uuid
import json
from fnmatch import fnmatch
from pathlib import Path
from typing import Optional
from urllib.parse import quote
@@ -9,6 +11,7 @@ from fastapi import (
APIRouter,
Depends,
File,
Form,
HTTPException,
Request,
UploadFile,
@@ -18,6 +21,9 @@ from fastapi import (
from fastapi.responses import FileResponse, StreamingResponse
from open_webui.constants import ERROR_MESSAGES
from open_webui.env import SRC_LOG_LEVELS
from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT
from open_webui.models.users import Users
from open_webui.models.files import (
FileForm,
FileModel,
@@ -81,20 +87,55 @@ def has_access_to_file(
def upload_file(
request: Request,
file: UploadFile = File(...),
user=Depends(get_verified_user),
file_metadata: dict = {},
metadata: Optional[dict | str] = Form(None),
process: bool = Query(True),
internal: bool = False,
user=Depends(get_verified_user),
):
log.info(f"file.content_type: {file.content_type}")
if isinstance(metadata, str):
try:
metadata = json.loads(metadata)
except json.JSONDecodeError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Invalid metadata format"),
)
file_metadata = metadata if metadata else {}
try:
unsanitized_filename = file.filename
filename = os.path.basename(unsanitized_filename)
file_extension = os.path.splitext(filename)[1]
# Remove the leading dot from the file extension
file_extension = file_extension[1:] if file_extension else ""
if (not internal) and request.app.state.config.ALLOWED_FILE_EXTENSIONS:
request.app.state.config.ALLOWED_FILE_EXTENSIONS = [
ext for ext in request.app.state.config.ALLOWED_FILE_EXTENSIONS if ext
]
if file_extension not in request.app.state.config.ALLOWED_FILE_EXTENSIONS:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(
f"File type {file_extension} is not allowed"
),
)
# replace filename with uuid
id = str(uuid.uuid4())
name = filename
filename = f"{id}_{filename}"
contents, file_path = Storage.upload_file(file.file, filename)
tags = {
"OpenWebUI-User-Email": user.email,
"OpenWebUI-User-Id": user.id,
"OpenWebUI-User-Name": user.name,
"OpenWebUI-File-Id": id,
}
contents, file_path = Storage.upload_file(file.file, filename, tags)
file_item = Files.insert_new_file(
user.id,
@@ -114,21 +155,36 @@ def upload_file(
)
if process:
try:
if file.content_type in [
"audio/mpeg",
"audio/wav",
"audio/ogg",
"audio/x-m4a",
]:
file_path = Storage.get_file(file_path)
result = transcribe(request, file_path)
if file.content_type:
stt_supported_content_types = getattr(
request.app.state.config, "STT_SUPPORTED_CONTENT_TYPES", []
)
process_file(
request,
ProcessFileForm(file_id=id, content=result.get("text", "")),
user=user,
if any(
fnmatch(file.content_type, content_type)
for content_type in (
stt_supported_content_types
if stt_supported_content_types
and any(t.strip() for t in stt_supported_content_types)
else ["audio/*", "video/webm"]
)
):
file_path = Storage.get_file(file_path)
result = transcribe(request, file_path, file_metadata)
process_file(
request,
ProcessFileForm(file_id=id, content=result.get("text", "")),
user=user,
)
elif (not file.content_type.startswith(("image/", "video/"))) or (
request.app.state.config.CONTENT_EXTRACTION_ENGINE == "external"
):
process_file(request, ProcessFileForm(file_id=id), user=user)
else:
log.info(
f"File type {file.content_type} is not provided, but trying to process anyway"
)
elif file.content_type not in ["image/png", "image/jpeg", "image/gif"]:
process_file(request, ProcessFileForm(file_id=id), user=user)
file_item = Files.get_file_by_id(id=id)
@@ -154,7 +210,7 @@ def upload_file(
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
detail=ERROR_MESSAGES.DEFAULT("Error uploading file"),
)
@@ -172,11 +228,54 @@ async def list_files(user=Depends(get_verified_user), content: bool = Query(True
if not content:
for file in files:
del file.data["content"]
if "content" in file.data:
del file.data["content"]
return files
############################
# Search Files
############################
@router.get("/search", response_model=list[FileModelResponse])
async def search_files(
filename: str = Query(
...,
description="Filename pattern to search for. Supports wildcards such as '*.txt'",
),
content: bool = Query(True),
user=Depends(get_verified_user),
):
"""
Search for files by filename with support for wildcard patterns.
"""
# Get files according to user role
if user.role == "admin":
files = Files.get_files()
else:
files = Files.get_files_by_user_id(user.id)
# Get matching files
matching_files = [
file for file in files if fnmatch(file.filename.lower(), filename.lower())
]
if not matching_files:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="No files found matching the pattern.",
)
if not content:
for file in matching_files:
if "content" in file.data:
del file.data["content"]
return matching_files
############################
# Delete All Files
############################
@@ -188,6 +287,7 @@ async def delete_all_files(user=Depends(get_admin_user)):
if result:
try:
Storage.delete_all_files()
VECTOR_DB_CLIENT.reset()
except Exception as e:
log.exception(e)
log.error("Error deleting files")
@@ -389,6 +489,13 @@ async def get_html_file_content_by_id(id: str, user=Depends(get_verified_user)):
detail=ERROR_MESSAGES.NOT_FOUND,
)
file_user = Users.get_user_by_id(file.user_id)
if not file_user.role == "admin":
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
if (
file.user_id == user.id
or user.role == "admin"
@@ -498,12 +605,12 @@ async def delete_file_by_id(id: str, user=Depends(get_verified_user)):
or user.role == "admin"
or has_access_to_file(id, "write", user)
):
# We should add Chroma cleanup here
result = Files.delete_file_by_id(id)
if result:
try:
Storage.delete_file(file.path)
VECTOR_DB_CLIENT.delete(collection_name=f"file-{id}")
except Exception as e:
log.exception(e)
log.error("Error deleting files")
+4 -6
View File
@@ -49,7 +49,7 @@ async def get_folders(user=Depends(get_verified_user)):
**folder.model_dump(),
"items": {
"chats": [
{"title": chat.title, "id": chat.id}
{"title": chat.title, "id": chat.id, "updated_at": chat.updated_at}
for chat in Chats.get_chats_by_folder_id_and_user_id(
folder.id, user.id
)
@@ -78,7 +78,7 @@ def create_folder(form_data: FolderForm, user=Depends(get_verified_user)):
)
try:
folder = Folders.insert_new_folder(user.id, form_data.name)
folder = Folders.insert_new_folder(user.id, form_data)
return folder
except Exception as e:
log.exception(e)
@@ -120,16 +120,14 @@ async def update_folder_name_by_id(
existing_folder = Folders.get_folder_by_parent_id_and_user_id_and_name(
folder.parent_id, user.id, form_data.name
)
if existing_folder:
if existing_folder and existing_folder.id != id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Folder already exists"),
)
try:
folder = Folders.update_folder_name_by_id_and_user_id(
id, user.id, form_data.name
)
folder = Folders.update_folder_by_id_and_user_id(id, user.id, form_data)
return folder
except Exception as e:
+113 -21
View File
@@ -1,5 +1,8 @@
import os
import re
import logging
import aiohttp
from pathlib import Path
from typing import Optional
@@ -9,12 +12,18 @@ from open_webui.models.functions import (
FunctionResponse,
Functions,
)
from open_webui.utils.plugin import load_function_module_by_id, replace_imports
from open_webui.utils.plugin import (
load_function_module_by_id,
replace_imports,
get_function_module_from_cache,
)
from open_webui.config import CACHE_DIR
from open_webui.constants import ERROR_MESSAGES
from fastapi import APIRouter, Depends, HTTPException, Request, status
from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.env import SRC_LOG_LEVELS
from pydantic import BaseModel, HttpUrl
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MAIN"])
@@ -42,6 +51,97 @@ async def get_functions(user=Depends(get_admin_user)):
return Functions.get_functions()
############################
# LoadFunctionFromLink
############################
class LoadUrlForm(BaseModel):
url: HttpUrl
def github_url_to_raw_url(url: str) -> str:
# Handle 'tree' (folder) URLs (add main.py at the end)
m1 = re.match(r"https://github\.com/([^/]+)/([^/]+)/tree/([^/]+)/(.*)", url)
if m1:
org, repo, branch, path = m1.groups()
return f"https://raw.githubusercontent.com/{org}/{repo}/refs/heads/{branch}/{path.rstrip('/')}/main.py"
# Handle 'blob' (file) URLs
m2 = re.match(r"https://github\.com/([^/]+)/([^/]+)/blob/([^/]+)/(.*)", url)
if m2:
org, repo, branch, path = m2.groups()
return (
f"https://raw.githubusercontent.com/{org}/{repo}/refs/heads/{branch}/{path}"
)
# No match; return as-is
return url
@router.post("/load/url", response_model=Optional[dict])
async def load_function_from_url(
request: Request, form_data: LoadUrlForm, user=Depends(get_admin_user)
):
# NOTE: This is NOT a SSRF vulnerability:
# This endpoint is admin-only (see get_admin_user), meant for *trusted* internal use,
# and does NOT accept untrusted user input. Access is enforced by authentication.
url = str(form_data.url)
if not url:
raise HTTPException(status_code=400, detail="Please enter a valid URL")
url = github_url_to_raw_url(url)
url_parts = url.rstrip("/").split("/")
file_name = url_parts[-1]
function_name = (
file_name[:-3]
if (
file_name.endswith(".py")
and (not file_name.startswith(("main.py", "index.py", "__init__.py")))
)
else url_parts[-2] if len(url_parts) > 1 else "function"
)
try:
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get(
url, headers={"Content-Type": "application/json"}
) as resp:
if resp.status != 200:
raise HTTPException(
status_code=resp.status, detail="Failed to fetch the function"
)
data = await resp.text()
if not data:
raise HTTPException(
status_code=400, detail="No data received from the URL"
)
return {
"name": function_name,
"content": data,
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error importing function: {e}")
############################
# SyncFunctions
############################
class SyncFunctionsForm(FunctionForm):
functions: list[FunctionModel] = []
@router.post("/sync", response_model=Optional[FunctionModel])
async def sync_functions(
request: Request, form_data: SyncFunctionsForm, user=Depends(get_admin_user)
):
return Functions.sync_functions(user.id, form_data.functions)
############################
# CreateNewFunction
############################
@@ -262,11 +362,9 @@ async def get_function_valves_spec_by_id(
):
function = Functions.get_function_by_id(id)
if function:
if id in request.app.state.FUNCTIONS:
function_module = request.app.state.FUNCTIONS[id]
else:
function_module, function_type, frontmatter = load_function_module_by_id(id)
request.app.state.FUNCTIONS[id] = function_module
function_module, function_type, frontmatter = get_function_module_from_cache(
request, id
)
if hasattr(function_module, "Valves"):
Valves = function_module.Valves
@@ -290,11 +388,9 @@ async def update_function_valves_by_id(
):
function = Functions.get_function_by_id(id)
if function:
if id in request.app.state.FUNCTIONS:
function_module = request.app.state.FUNCTIONS[id]
else:
function_module, function_type, frontmatter = load_function_module_by_id(id)
request.app.state.FUNCTIONS[id] = function_module
function_module, function_type, frontmatter = get_function_module_from_cache(
request, id
)
if hasattr(function_module, "Valves"):
Valves = function_module.Valves
@@ -353,11 +449,9 @@ async def get_function_user_valves_spec_by_id(
):
function = Functions.get_function_by_id(id)
if function:
if id in request.app.state.FUNCTIONS:
function_module = request.app.state.FUNCTIONS[id]
else:
function_module, function_type, frontmatter = load_function_module_by_id(id)
request.app.state.FUNCTIONS[id] = function_module
function_module, function_type, frontmatter = get_function_module_from_cache(
request, id
)
if hasattr(function_module, "UserValves"):
UserValves = function_module.UserValves
@@ -377,11 +471,9 @@ async def update_function_user_valves_by_id(
function = Functions.get_function_by_id(id)
if function:
if id in request.app.state.FUNCTIONS:
function_module = request.app.state.FUNCTIONS[id]
else:
function_module, function_type, frontmatter = load_function_module_by_id(id)
request.app.state.FUNCTIONS[id] = function_module
function_module, function_type, frontmatter = get_function_module_from_cache(
request, id
)
if hasattr(function_module, "UserValves"):
UserValves = function_module.UserValves
+51
View File
@@ -9,6 +9,7 @@ from open_webui.models.groups import (
GroupForm,
GroupUpdateForm,
GroupResponse,
UserIdsForm,
)
from open_webui.config import CACHE_DIR
@@ -107,6 +108,56 @@ async def update_group_by_id(
)
############################
# AddUserToGroupByUserIdAndGroupId
############################
@router.post("/id/{id}/users/add", response_model=Optional[GroupResponse])
async def add_user_to_group(
id: str, form_data: UserIdsForm, user=Depends(get_admin_user)
):
try:
if form_data.user_ids:
form_data.user_ids = Users.get_valid_user_ids(form_data.user_ids)
group = Groups.add_users_to_group(id, form_data.user_ids)
if group:
return group
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error adding users to group"),
)
except Exception as e:
log.exception(f"Error adding users to group {id}: {e}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
@router.post("/id/{id}/users/remove", response_model=Optional[GroupResponse])
async def remove_users_from_group(
id: str, form_data: UserIdsForm, user=Depends(get_admin_user)
):
try:
group = Groups.remove_users_from_group(id, form_data.user_ids)
if group:
return group
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error removing users from group"),
)
except Exception as e:
log.exception(f"Error removing users from group {id}: {e}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
############################
# DeleteGroupById
############################
+35 -14
View File
@@ -8,6 +8,7 @@ import re
from pathlib import Path
from typing import Optional
from urllib.parse import quote
import requests
from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile
from open_webui.config import CACHE_DIR
@@ -302,8 +303,16 @@ async def update_image_config(
):
set_image_model(request, form_data.MODEL)
if form_data.IMAGE_SIZE == "auto" and form_data.MODEL != "gpt-image-1":
raise HTTPException(
status_code=400,
detail=ERROR_MESSAGES.INCORRECT_FORMAT(
" (auto is only allowed with gpt-image-1)."
),
)
pattern = r"^\d+x\d+$"
if re.match(pattern, form_data.IMAGE_SIZE):
if form_data.IMAGE_SIZE == "auto" or re.match(pattern, form_data.IMAGE_SIZE):
request.app.state.config.IMAGE_SIZE = form_data.IMAGE_SIZE
else:
raise HTTPException(
@@ -333,10 +342,11 @@ def get_models(request: Request, user=Depends(get_verified_user)):
return [
{"id": "dall-e-2", "name": "DALL·E 2"},
{"id": "dall-e-3", "name": "DALL·E 3"},
{"id": "gpt-image-1", "name": "GPT-IMAGE 1"},
]
elif request.app.state.config.IMAGE_GENERATION_ENGINE == "gemini":
return [
{"id": "imagen-3-0-generate-002", "name": "imagen-3.0 generate-002"},
{"id": "imagen-3.0-generate-002", "name": "imagen-3.0 generate-002"},
]
elif request.app.state.config.IMAGE_GENERATION_ENGINE == "comfyui":
# TODO - get models from comfyui
@@ -419,7 +429,7 @@ def load_b64_image_data(b64_str):
try:
if "," in b64_str:
header, encoded = b64_str.split(",", 1)
mime_type = header.split(";")[0]
mime_type = header.split(";")[0].lstrip("data:")
img_data = base64.b64decode(encoded)
else:
mime_type = "image/png"
@@ -427,7 +437,7 @@ def load_b64_image_data(b64_str):
return img_data, mime_type
except Exception as e:
log.exception(f"Error loading image data: {e}")
return None
return None, None
def load_url_image_data(url, headers=None):
@@ -450,7 +460,7 @@ def load_url_image_data(url, headers=None):
return None
def upload_image(request, image_metadata, image_data, content_type, user):
def upload_image(request, image_data, content_type, metadata, user):
image_format = mimetypes.guess_extension(content_type)
file = UploadFile(
file=io.BytesIO(image_data),
@@ -459,7 +469,7 @@ def upload_image(request, image_metadata, image_data, content_type, user):
"content-type": content_type,
},
)
file_item = upload_file(request, file, user, file_metadata=image_metadata)
file_item = upload_file(request, file, metadata=metadata, internal=True, user=user)
url = request.app.url_path_for("get_file_content_by_id", id=file_item.id)
return url
@@ -470,7 +480,14 @@ async def image_generations(
form_data: GenerateImageForm,
user=Depends(get_verified_user),
):
width, height = tuple(map(int, request.app.state.config.IMAGE_SIZE.split("x")))
# if IMAGE_SIZE = 'auto', default WidthxHeight to the 512x512 default
# This is only relevant when the user has set IMAGE_SIZE to 'auto' with an
# image model other than gpt-image-1, which is warned about on settings save
width, height = (
tuple(map(int, request.app.state.config.IMAGE_SIZE.split("x")))
if "x" in request.app.state.config.IMAGE_SIZE
else (512, 512)
)
r = None
try:
@@ -482,7 +499,7 @@ async def image_generations(
headers["Content-Type"] = "application/json"
if ENABLE_FORWARD_USER_INFO_HEADERS:
headers["X-OpenWebUI-User-Name"] = user.name
headers["X-OpenWebUI-User-Name"] = quote(user.name, safe=" ")
headers["X-OpenWebUI-User-Id"] = user.id
headers["X-OpenWebUI-User-Email"] = user.email
headers["X-OpenWebUI-User-Role"] = user.role
@@ -500,7 +517,11 @@ async def image_generations(
if form_data.size
else request.app.state.config.IMAGE_SIZE
),
"response_format": "b64_json",
**(
{}
if "gpt-image-1" in request.app.state.config.IMAGE_GENERATION_MODEL
else {"response_format": "b64_json"}
),
}
# Use asyncio.to_thread for the requests.post call
@@ -522,7 +543,7 @@ async def image_generations(
else:
image_data, content_type = load_b64_image_data(image["b64_json"])
url = upload_image(request, data, image_data, content_type, user)
url = upload_image(request, image_data, content_type, data, user)
images.append({"url": url})
return images
@@ -556,7 +577,7 @@ async def image_generations(
image_data, content_type = load_b64_image_data(
image["bytesBase64Encoded"]
)
url = upload_image(request, data, image_data, content_type, user)
url = upload_image(request, image_data, content_type, data, user)
images.append({"url": url})
return images
@@ -607,9 +628,9 @@ async def image_generations(
image_data, content_type = load_url_image_data(image["url"], headers)
url = upload_image(
request,
form_data.model_dump(exclude_none=True),
image_data,
content_type,
form_data.model_dump(exclude_none=True),
user,
)
images.append({"url": url})
@@ -619,7 +640,7 @@ async def image_generations(
or request.app.state.config.IMAGE_GENERATION_ENGINE == ""
):
if form_data.model:
set_image_model(form_data.model)
set_image_model(request, form_data.model)
data = {
"prompt": form_data.prompt,
@@ -660,9 +681,9 @@ async def image_generations(
image_data, content_type = load_b64_image_data(image)
url = upload_image(
request,
{**data, "info": res["info"]},
image_data,
content_type,
{**data, "info": res["info"]},
user,
)
images.append({"url": url})
+91 -9
View File
@@ -9,8 +9,8 @@ from open_webui.models.knowledge import (
KnowledgeResponse,
KnowledgeUserResponse,
)
from open_webui.models.files import Files, FileModel
from open_webui.retrieval.vector.connector import VECTOR_DB_CLIENT
from open_webui.models.files import Files, FileModel, FileMetadataResponse
from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT
from open_webui.routers.retrieval import (
process_file,
ProcessFileForm,
@@ -161,13 +161,94 @@ async def create_new_knowledge(
)
############################
# ReindexKnowledgeFiles
############################
@router.post("/reindex", response_model=bool)
async def reindex_knowledge_files(request: Request, user=Depends(get_verified_user)):
if user.role != "admin":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.UNAUTHORIZED,
)
knowledge_bases = Knowledges.get_knowledge_bases()
log.info(f"Starting reindexing for {len(knowledge_bases)} knowledge bases")
deleted_knowledge_bases = []
for knowledge_base in knowledge_bases:
# -- Robust error handling for missing or invalid data
if not knowledge_base.data or not isinstance(knowledge_base.data, dict):
log.warning(
f"Knowledge base {knowledge_base.id} has no data or invalid data ({knowledge_base.data!r}). Deleting."
)
try:
Knowledges.delete_knowledge_by_id(id=knowledge_base.id)
deleted_knowledge_bases.append(knowledge_base.id)
except Exception as e:
log.error(
f"Failed to delete invalid knowledge base {knowledge_base.id}: {e}"
)
continue
try:
file_ids = knowledge_base.data.get("file_ids", [])
files = Files.get_files_by_ids(file_ids)
try:
if VECTOR_DB_CLIENT.has_collection(collection_name=knowledge_base.id):
VECTOR_DB_CLIENT.delete_collection(
collection_name=knowledge_base.id
)
except Exception as e:
log.error(f"Error deleting collection {knowledge_base.id}: {str(e)}")
continue # Skip, don't raise
failed_files = []
for file in files:
try:
process_file(
request,
ProcessFileForm(
file_id=file.id, collection_name=knowledge_base.id
),
user=user,
)
except Exception as e:
log.error(
f"Error processing file {file.filename} (ID: {file.id}): {str(e)}"
)
failed_files.append({"file_id": file.id, "error": str(e)})
continue
except Exception as e:
log.error(f"Error processing knowledge base {knowledge_base.id}: {str(e)}")
# Don't raise, just continue
continue
if failed_files:
log.warning(
f"Failed to process {len(failed_files)} files in knowledge base {knowledge_base.id}"
)
for failed in failed_files:
log.warning(f"File ID: {failed['file_id']}, Error: {failed['error']}")
log.info(
f"Reindexing completed. Deleted {len(deleted_knowledge_bases)} invalid knowledge bases: {deleted_knowledge_bases}"
)
return True
############################
# GetKnowledgeById
############################
class KnowledgeFilesResponse(KnowledgeResponse):
files: list[FileModel]
files: list[FileMetadataResponse]
@router.get("/{id}", response_model=Optional[KnowledgeFilesResponse])
@@ -183,7 +264,7 @@ async def get_knowledge_by_id(id: str, user=Depends(get_verified_user)):
):
file_ids = knowledge.data.get("file_ids", []) if knowledge.data else []
files = Files.get_files_by_ids(file_ids)
files = Files.get_file_metadatas_by_ids(file_ids)
return KnowledgeFilesResponse(
**knowledge.model_dump(),
@@ -311,7 +392,7 @@ def add_file_to_knowledge_by_id(
knowledge = Knowledges.update_knowledge_data_by_id(id=id, data=data)
if knowledge:
files = Files.get_files_by_ids(file_ids)
files = Files.get_file_metadatas_by_ids(file_ids)
return KnowledgeFilesResponse(
**knowledge.model_dump(),
@@ -388,7 +469,7 @@ def update_file_from_knowledge_by_id(
data = knowledge.data or {}
file_ids = data.get("file_ids", [])
files = Files.get_files_by_ids(file_ids)
files = Files.get_file_metadatas_by_ids(file_ids)
return KnowledgeFilesResponse(
**knowledge.model_dump(),
@@ -470,7 +551,7 @@ def remove_file_from_knowledge_by_id(
knowledge = Knowledges.update_knowledge_data_by_id(id=id, data=data)
if knowledge:
files = Files.get_files_by_ids(file_ids)
files = Files.get_file_metadatas_by_ids(file_ids)
return KnowledgeFilesResponse(
**knowledge.model_dump(),
@@ -666,7 +747,7 @@ def add_files_to_knowledge_batch(
error_details = [f"{err.file_id}: {err.error}" for err in result.errors]
return KnowledgeFilesResponse(
**knowledge.model_dump(),
files=Files.get_files_by_ids(existing_file_ids),
files=Files.get_file_metadatas_by_ids(existing_file_ids),
warnings={
"message": "Some files failed to process",
"errors": error_details,
@@ -674,5 +755,6 @@ def add_files_to_knowledge_batch(
)
return KnowledgeFilesResponse(
**knowledge.model_dump(), files=Files.get_files_by_ids(existing_file_ids)
**knowledge.model_dump(),
files=Files.get_file_metadatas_by_ids(existing_file_ids),
)
+4 -2
View File
@@ -4,7 +4,7 @@ import logging
from typing import Optional
from open_webui.models.memories import Memories, MemoryModel
from open_webui.retrieval.vector.connector import VECTOR_DB_CLIENT
from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT
from open_webui.utils.auth import get_verified_user
from open_webui.env import SRC_LOG_LEVELS
@@ -153,7 +153,9 @@ async def update_memory_by_id(
form_data: MemoryUpdateModel,
user=Depends(get_verified_user),
):
memory = Memories.update_memory_by_id(memory_id, form_data.content)
memory = Memories.update_memory_by_id_and_user_id(
memory_id, user.id, form_data.content
)
if memory is None:
raise HTTPException(status_code=404, detail="Memory not found")
+226
View File
@@ -0,0 +1,226 @@
import json
import logging
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Request, status, BackgroundTasks
from pydantic import BaseModel
from open_webui.socket.main import sio
from open_webui.models.users import Users, UserResponse
from open_webui.models.notes import Notes, NoteModel, NoteForm, NoteUserResponse
from open_webui.config import ENABLE_ADMIN_CHAT_ACCESS, ENABLE_ADMIN_EXPORT
from open_webui.constants import ERROR_MESSAGES
from open_webui.env import SRC_LOG_LEVELS
from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.utils.access_control import has_access, has_permission
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
router = APIRouter()
############################
# GetNotes
############################
@router.get("/", response_model=list[NoteUserResponse])
async def get_notes(request: Request, user=Depends(get_verified_user)):
if user.role != "admin" and not has_permission(
user.id, "features.notes", request.app.state.config.USER_PERMISSIONS
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.UNAUTHORIZED,
)
notes = [
NoteUserResponse(
**{
**note.model_dump(),
"user": UserResponse(**Users.get_user_by_id(note.user_id).model_dump()),
}
)
for note in Notes.get_notes_by_user_id(user.id, "write")
]
return notes
class NoteTitleIdResponse(BaseModel):
id: str
title: str
updated_at: int
created_at: int
@router.get("/list", response_model=list[NoteTitleIdResponse])
async def get_note_list(request: Request, user=Depends(get_verified_user)):
if user.role != "admin" and not has_permission(
user.id, "features.notes", request.app.state.config.USER_PERMISSIONS
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.UNAUTHORIZED,
)
notes = [
NoteTitleIdResponse(**note.model_dump())
for note in Notes.get_notes_by_user_id(user.id, "write")
]
return notes
############################
# CreateNewNote
############################
@router.post("/create", response_model=Optional[NoteModel])
async def create_new_note(
request: Request, form_data: NoteForm, user=Depends(get_verified_user)
):
if user.role != "admin" and not has_permission(
user.id, "features.notes", request.app.state.config.USER_PERMISSIONS
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.UNAUTHORIZED,
)
try:
note = Notes.insert_new_note(form_data, user.id)
return note
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
)
############################
# GetNoteById
############################
@router.get("/{id}", response_model=Optional[NoteModel])
async def get_note_by_id(request: Request, id: str, user=Depends(get_verified_user)):
if user.role != "admin" and not has_permission(
user.id, "features.notes", request.app.state.config.USER_PERMISSIONS
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.UNAUTHORIZED,
)
note = Notes.get_note_by_id(id)
if not note:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
)
if user.role != "admin" and (
user.id != note.user_id
and (not has_access(user.id, type="read", access_control=note.access_control))
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
)
return note
############################
# UpdateNoteById
############################
@router.post("/{id}/update", response_model=Optional[NoteModel])
async def update_note_by_id(
request: Request, id: str, form_data: NoteForm, user=Depends(get_verified_user)
):
if user.role != "admin" and not has_permission(
user.id, "features.notes", request.app.state.config.USER_PERMISSIONS
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.UNAUTHORIZED,
)
note = Notes.get_note_by_id(id)
if not note:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
)
if user.role != "admin" and (
user.id != note.user_id
and not has_access(user.id, type="write", access_control=note.access_control)
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
)
try:
note = Notes.update_note_by_id(id, form_data)
await sio.emit(
"note-events",
note.model_dump(),
to=f"note:{note.id}",
)
return note
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
)
############################
# DeleteNoteById
############################
@router.delete("/{id}/delete", response_model=bool)
async def delete_note_by_id(request: Request, id: str, user=Depends(get_verified_user)):
if user.role != "admin" and not has_permission(
user.id, "features.notes", request.app.state.config.USER_PERMISSIONS
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.UNAUTHORIZED,
)
note = Notes.get_note_by_id(id)
if not note:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
)
if user.role != "admin" and (
user.id != note.user_id
and not has_access(user.id, type="write", access_control=note.access_control)
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
)
try:
note = Notes.delete_note_by_id(id)
return True
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
)
+292 -95
View File
@@ -9,11 +9,16 @@ import os
import random
import re
import time
from datetime import datetime
from typing import Optional, Union
from urllib.parse import urlparse
import aiohttp
from aiocache import cached
import requests
from urllib.parse import quote
from open_webui.models.chats import Chats
from open_webui.models.users import UserModel
from open_webui.env import (
@@ -54,6 +59,8 @@ from open_webui.config import (
from open_webui.env import (
ENV,
SRC_LOG_LEVELS,
MODELS_CACHE_TTL,
AIOHTTP_CLIENT_SESSION_SSL,
AIOHTTP_CLIENT_TIMEOUT,
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST,
BYPASS_MODEL_ACCESS_CONTROL,
@@ -82,7 +89,7 @@ async def send_get_request(url, key=None, user: UserModel = None):
**({"Authorization": f"Bearer {key}"} if key else {}),
**(
{
"X-OpenWebUI-User-Name": user.name,
"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,
@@ -91,6 +98,7 @@ async def send_get_request(url, key=None, user: UserModel = None):
else {}
),
},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as response:
return await response.json()
except Exception as e:
@@ -116,6 +124,7 @@ async def send_post_request(
key: Optional[str] = None,
content_type: Optional[str] = None,
user: UserModel = None,
metadata: Optional[dict] = None,
):
r = None
@@ -132,18 +141,39 @@ async def send_post_request(
**({"Authorization": f"Bearer {key}"} if key else {}),
**(
{
"X-OpenWebUI-User-Name": user.name,
"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,
**(
{"X-OpenWebUI-Chat-Id": metadata.get("chat_id")}
if metadata and metadata.get("chat_id")
else {}
),
}
if ENABLE_FORWARD_USER_INFO_HEADERS and user
else {}
),
},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
)
r.raise_for_status()
if r.ok is False:
try:
res = await r.json()
await cleanup_response(r, session)
if "error" in res:
raise HTTPException(status_code=r.status, detail=res["error"])
except HTTPException as e:
raise e # Re-raise HTTPException to be handled by FastAPI
except Exception as e:
log.error(f"Failed to parse error response: {e}")
raise HTTPException(
status_code=r.status,
detail=f"Open WebUI: Server Connection Error",
)
r.raise_for_status() # Raises an error for bad responses (4xx, 5xx)
if stream:
response_headers = dict(r.headers)
@@ -160,24 +190,20 @@ async def send_post_request(
)
else:
res = await r.json()
await cleanup_response(r, session)
return res
except HTTPException as e:
raise e # Re-raise HTTPException to be handled by FastAPI
except Exception as e:
detail = None
if r is not None:
try:
res = await r.json()
if "error" in res:
detail = f"Ollama: {res.get('error', 'Unknown error')}"
except Exception:
detail = f"Ollama: {e}"
detail = f"Ollama: {e}"
raise HTTPException(
status_code=r.status if r else 500,
detail=detail if detail else "Open WebUI: Server Connection Error",
detail=detail if e else "Open WebUI: Server Connection Error",
)
finally:
if not stream:
await cleanup_response(r, session)
def get_api_key(idx, url, configs):
@@ -216,7 +242,8 @@ async def verify_connection(
key = form_data.key
async with aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
trust_env=True,
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST),
) as session:
try:
async with session.get(
@@ -225,7 +252,7 @@ async def verify_connection(
**({"Authorization": f"Bearer {key}"} if key else {}),
**(
{
"X-OpenWebUI-User-Name": user.name,
"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,
@@ -234,6 +261,7 @@ async def verify_connection(
else {}
),
},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as r:
if r.status != 200:
detail = f"HTTP Error: {r.status}"
@@ -295,7 +323,23 @@ async def update_config(
}
@cached(ttl=1)
def merge_ollama_models_lists(model_lists):
merged_models = {}
for idx, model_list in enumerate(model_lists):
if model_list is not None:
for model in model_list:
id = model["model"]
if id not in merged_models:
model["urls"] = [idx]
merged_models[id] = model
else:
merged_models[id]["urls"].append(idx)
return list(merged_models.values())
@cached(ttl=MODELS_CACHE_TTL)
async def get_all_models(request: Request, user: UserModel = None):
log.info("get_all_models()")
if request.app.state.config.ENABLE_OLLAMA_API:
@@ -335,6 +379,8 @@ async def get_all_models(request: Request, user: UserModel = None):
), # Legacy support
)
connection_type = api_config.get("connection_type", "local")
prefix_id = api_config.get("prefix_id", None)
tags = api_config.get("tags", [])
model_ids = api_config.get("model_ids", [])
@@ -347,31 +393,18 @@ async def get_all_models(request: Request, user: UserModel = None):
)
)
if prefix_id:
for model in response.get("models", []):
for model in response.get("models", []):
if prefix_id:
model["model"] = f"{prefix_id}.{model['model']}"
if tags:
for model in response.get("models", []):
if tags:
model["tags"] = tags
def merge_models_lists(model_lists):
merged_models = {}
for idx, model_list in enumerate(model_lists):
if model_list is not None:
for model in model_list:
id = model["model"]
if id not in merged_models:
model["urls"] = [idx]
merged_models[id] = model
else:
merged_models[id]["urls"].append(idx)
return list(merged_models.values())
if connection_type:
model["connection_type"] = connection_type
models = {
"models": merge_models_lists(
"models": merge_ollama_models_lists(
map(
lambda response: response.get("models", []) if response else None,
responses,
@@ -379,6 +412,22 @@ async def get_all_models(request: Request, user: UserModel = None):
)
}
try:
loaded_models = await get_ollama_loaded_models(request, user=user)
expires_map = {
m["name"]: m["expires_at"]
for m in loaded_models["models"]
if "expires_at" in m
}
for m in models["models"]:
if m["name"] in expires_map:
# Parse ISO8601 datetime with offset, get unix timestamp as int
dt = datetime.fromisoformat(expires_map[m["name"]])
m["expires_at"] = int(dt.timestamp())
except Exception as e:
log.debug(f"Failed to get loaded models: {e}")
else:
models = {"models": []}
@@ -423,7 +472,7 @@ async def get_ollama_tags(
**({"Authorization": f"Bearer {key}"} if key else {}),
**(
{
"X-OpenWebUI-User-Name": user.name,
"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,
@@ -459,6 +508,68 @@ async def get_ollama_tags(
return models
@router.get("/api/ps")
async def get_ollama_loaded_models(request: Request, user=Depends(get_admin_user)):
"""
List models that are currently loaded into Ollama memory, and which node they are loaded on.
"""
if request.app.state.config.ENABLE_OLLAMA_API:
request_tasks = []
for idx, url in enumerate(request.app.state.config.OLLAMA_BASE_URLS):
if (str(idx) not in request.app.state.config.OLLAMA_API_CONFIGS) and (
url not in request.app.state.config.OLLAMA_API_CONFIGS # Legacy support
):
request_tasks.append(send_get_request(f"{url}/api/ps", user=user))
else:
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(
str(idx),
request.app.state.config.OLLAMA_API_CONFIGS.get(
url, {}
), # Legacy support
)
enable = api_config.get("enable", True)
key = api_config.get("key", None)
if enable:
request_tasks.append(
send_get_request(f"{url}/api/ps", key, user=user)
)
else:
request_tasks.append(asyncio.ensure_future(asyncio.sleep(0, None)))
responses = await asyncio.gather(*request_tasks)
for idx, response in enumerate(responses):
if response:
url = request.app.state.config.OLLAMA_BASE_URLS[idx]
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(
str(idx),
request.app.state.config.OLLAMA_API_CONFIGS.get(
url, {}
), # Legacy support
)
prefix_id = api_config.get("prefix_id", None)
for model in response.get("models", []):
if prefix_id:
model["model"] = f"{prefix_id}.{model['model']}"
models = {
"models": merge_ollama_models_lists(
map(
lambda response: response.get("models", []) if response else None,
responses,
)
)
}
else:
models = {"models": []}
return models
@router.get("/api/version")
@router.get("/api/version/{url_idx}")
async def get_ollama_versions(request: Request, url_idx: Optional[int] = None):
@@ -532,34 +643,77 @@ async def get_ollama_versions(request: Request, url_idx: Optional[int] = None):
return {"version": False}
@router.get("/api/ps")
async def get_ollama_loaded_models(request: Request, user=Depends(get_verified_user)):
"""
List models that are currently loaded into Ollama memory, and which node they are loaded on.
"""
if request.app.state.config.ENABLE_OLLAMA_API:
request_tasks = [
send_get_request(
f"{url}/api/ps",
request.app.state.config.OLLAMA_API_CONFIGS.get(
str(idx),
request.app.state.config.OLLAMA_API_CONFIGS.get(
url, {}
), # Legacy support
).get("key", None),
class ModelNameForm(BaseModel):
model: Optional[str] = None
model_config = ConfigDict(
extra="allow",
)
@router.post("/api/unload")
async def unload_model(
request: Request,
form_data: ModelNameForm,
user=Depends(get_admin_user),
):
form_data = form_data.model_dump(exclude_none=True)
model_name = form_data.get("model", form_data.get("name"))
if not model_name:
raise HTTPException(
status_code=400, detail="Missing name of the model to unload."
)
# Refresh/load models if needed, get mapping from name to URLs
await get_all_models(request, user=user)
models = request.app.state.OLLAMA_MODELS
# Canonicalize model name (if not supplied with version)
if ":" not in model_name:
model_name = f"{model_name}:latest"
if model_name not in models:
raise HTTPException(
status_code=400, detail=ERROR_MESSAGES.MODEL_NOT_FOUND(model_name)
)
url_indices = models[model_name]["urls"]
# Send unload to ALL url_indices
results = []
errors = []
for idx in url_indices:
url = request.app.state.config.OLLAMA_BASE_URLS[idx]
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(
str(idx), request.app.state.config.OLLAMA_API_CONFIGS.get(url, {})
)
key = get_api_key(idx, url, request.app.state.config.OLLAMA_API_CONFIGS)
prefix_id = api_config.get("prefix_id", None)
if prefix_id and model_name.startswith(f"{prefix_id}."):
model_name = model_name[len(f"{prefix_id}.") :]
payload = {"model": model_name, "keep_alive": 0, "prompt": ""}
try:
res = await send_post_request(
url=f"{url}/api/generate",
payload=json.dumps(payload),
stream=False,
key=key,
user=user,
)
for idx, url in enumerate(request.app.state.config.OLLAMA_BASE_URLS)
]
responses = await asyncio.gather(*request_tasks)
results.append({"url_idx": idx, "success": True, "response": res})
except Exception as e:
log.exception(f"Failed to unload model on node {idx}: {e}")
errors.append({"url_idx": idx, "success": False, "error": str(e)})
return dict(zip(request.app.state.config.OLLAMA_BASE_URLS, responses))
else:
return {}
if len(errors) > 0:
raise HTTPException(
status_code=500,
detail=f"Failed to unload model on {len(errors)} nodes: {errors}",
)
class ModelNameForm(BaseModel):
name: str
return {"status": True}
@router.post("/api/pull")
@@ -570,11 +724,14 @@ async def pull_model(
url_idx: int = 0,
user=Depends(get_admin_user),
):
form_data = form_data.model_dump(exclude_none=True)
form_data["model"] = form_data.get("model", form_data.get("name"))
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
log.info(f"url: {url}")
# Admin should be able to pull models from any source
payload = {**form_data.model_dump(exclude_none=True), "insecure": True}
payload = {**form_data, "insecure": True}
return await send_post_request(
url=f"{url}/api/pull",
@@ -585,7 +742,7 @@ async def pull_model(
class PushModelForm(BaseModel):
name: str
model: str
insecure: Optional[bool] = None
stream: Optional[bool] = None
@@ -602,12 +759,12 @@ async def push_model(
await get_all_models(request, user=user)
models = request.app.state.OLLAMA_MODELS
if form_data.name in models:
url_idx = models[form_data.name]["urls"][0]
if form_data.model in models:
url_idx = models[form_data.model]["urls"][0]
else:
raise HTTPException(
status_code=400,
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.name),
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
)
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
@@ -685,7 +842,7 @@ async def copy_model(
**({"Authorization": f"Bearer {key}"} if key else {}),
**(
{
"X-OpenWebUI-User-Name": user.name,
"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,
@@ -726,16 +883,21 @@ async def delete_model(
url_idx: Optional[int] = None,
user=Depends(get_admin_user),
):
form_data = form_data.model_dump(exclude_none=True)
form_data["model"] = form_data.get("model", form_data.get("name"))
model = form_data.get("model")
if url_idx is None:
await get_all_models(request, user=user)
models = request.app.state.OLLAMA_MODELS
if form_data.name in models:
url_idx = models[form_data.name]["urls"][0]
if model in models:
url_idx = models[model]["urls"][0]
else:
raise HTTPException(
status_code=400,
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.name),
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(model),
)
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
@@ -745,13 +907,13 @@ async def delete_model(
r = requests.request(
method="DELETE",
url=f"{url}/api/delete",
data=form_data.model_dump_json(exclude_none=True).encode(),
data=json.dumps(form_data).encode(),
headers={
"Content-Type": "application/json",
**({"Authorization": f"Bearer {key}"} if key else {}),
**(
{
"X-OpenWebUI-User-Name": user.name,
"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,
@@ -787,16 +949,21 @@ async def delete_model(
async def show_model_info(
request: Request, form_data: ModelNameForm, user=Depends(get_verified_user)
):
form_data = form_data.model_dump(exclude_none=True)
form_data["model"] = form_data.get("model", form_data.get("name"))
await get_all_models(request, user=user)
models = request.app.state.OLLAMA_MODELS
if form_data.name not in models:
model = form_data.get("model")
if model not in models:
raise HTTPException(
status_code=400,
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.name),
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(model),
)
url_idx = random.choice(models[form_data.name]["urls"])
url_idx = random.choice(models[model]["urls"])
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS)
@@ -810,7 +977,7 @@ async def show_model_info(
**({"Authorization": f"Bearer {key}"} if key else {}),
**(
{
"X-OpenWebUI-User-Name": user.name,
"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,
@@ -819,7 +986,7 @@ async def show_model_info(
else {}
),
},
data=form_data.model_dump_json(exclude_none=True).encode(),
data=json.dumps(form_data).encode(),
)
r.raise_for_status()
@@ -878,8 +1045,16 @@ async def embed(
)
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(
str(url_idx),
request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), # Legacy support
)
key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS)
prefix_id = api_config.get("prefix_id", None)
if prefix_id:
form_data.model = form_data.model.replace(f"{prefix_id}.", "")
try:
r = requests.request(
method="POST",
@@ -889,7 +1064,7 @@ async def embed(
**({"Authorization": f"Bearer {key}"} if key else {}),
**(
{
"X-OpenWebUI-User-Name": user.name,
"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,
@@ -957,8 +1132,16 @@ async def embeddings(
)
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(
str(url_idx),
request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), # Legacy support
)
key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS)
prefix_id = api_config.get("prefix_id", None)
if prefix_id:
form_data.model = form_data.model.replace(f"{prefix_id}.", "")
try:
r = requests.request(
method="POST",
@@ -968,7 +1151,7 @@ async def embeddings(
**({"Authorization": f"Bearer {key}"} if key else {}),
**(
{
"X-OpenWebUI-User-Name": user.name,
"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,
@@ -1006,7 +1189,7 @@ class GenerateCompletionForm(BaseModel):
prompt: str
suffix: Optional[str] = None
images: Optional[list[str]] = None
format: Optional[str] = None
format: Optional[Union[dict, str]] = None
options: Optional[dict] = None
system: Optional[str] = None
template: Optional[str] = None
@@ -1088,6 +1271,9 @@ class GenerateChatCompletionForm(BaseModel):
stream: Optional[bool] = True
keep_alive: Optional[Union[int, str]] = None
tools: Optional[list[dict]] = None
model_config = ConfigDict(
extra="allow",
)
async def get_ollama_url(request: Request, model: str, url_idx: Optional[int] = None):
@@ -1125,7 +1311,9 @@ async def generate_chat_completion(
detail=str(e),
)
payload = {**form_data.model_dump(exclude_none=True)}
if isinstance(form_data, BaseModel):
payload = {**form_data.model_dump(exclude_none=True)}
if "metadata" in payload:
del payload["metadata"]
@@ -1139,13 +1327,10 @@ async def generate_chat_completion(
params = model_info.params.model_dump()
if params:
if payload.get("options") is None:
payload["options"] = {}
system = params.pop("system", None)
payload["options"] = apply_model_params_to_body_ollama(
params, payload["options"]
)
payload = apply_model_system_prompt_to_body(params, payload, metadata, user)
payload = apply_model_params_to_body_ollama(params, payload)
payload = apply_model_system_prompt_to_body(system, payload, metadata, user)
# Check if user has access to the model
if not bypass_filter and user.role == "user":
@@ -1178,7 +1363,7 @@ async def generate_chat_completion(
prefix_id = api_config.get("prefix_id", None)
if prefix_id:
payload["model"] = payload["model"].replace(f"{prefix_id}.", "")
# payload["keep_alive"] = -1 # keep alive forever
return await send_post_request(
url=f"{url}/api/chat",
payload=json.dumps(payload),
@@ -1186,6 +1371,7 @@ async def generate_chat_completion(
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
content_type="application/x-ndjson",
user=user,
metadata=metadata,
)
@@ -1224,6 +1410,8 @@ async def generate_openai_completion(
url_idx: Optional[int] = None,
user=Depends(get_verified_user),
):
metadata = form_data.pop("metadata", None)
try:
form_data = OpenAICompletionForm(**form_data)
except Exception as e:
@@ -1289,6 +1477,7 @@ async def generate_openai_completion(
stream=payload.get("stream", False),
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
user=user,
metadata=metadata,
)
@@ -1327,8 +1516,10 @@ async def generate_openai_chat_completion(
params = model_info.params.model_dump()
if params:
system = params.pop("system", None)
payload = apply_model_params_to_body_openai(params, payload)
payload = apply_model_system_prompt_to_body(params, payload, metadata, user)
payload = apply_model_system_prompt_to_body(system, payload, metadata, user)
# Check if user has access to the model
if user.role == "user":
@@ -1368,6 +1559,7 @@ async def generate_openai_chat_completion(
stream=payload.get("stream", False),
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
user=user,
metadata=metadata,
)
@@ -1482,7 +1674,9 @@ async def download_file_stream(
timeout = aiohttp.ClientTimeout(total=600) # Set the timeout
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
async with session.get(file_url, headers=headers) as response:
async with session.get(
file_url, headers=headers, ssl=AIOHTTP_CLIENT_SESSION_SSL
) as response:
total_size = int(response.headers.get("content-length", 0)) + current_size
with open(file_path, "ab+") as file:
@@ -1497,7 +1691,8 @@ async def download_file_stream(
if done:
file.seek(0)
hashed = calculate_sha256(file)
chunk_size = 1024 * 1024 * 2
hashed = calculate_sha256(file, chunk_size)
file.seek(0)
url = f"{ollama_url}/api/blobs/sha256:{hashed}"
@@ -1561,7 +1756,9 @@ async def upload_model(
if url_idx is None:
url_idx = 0
ollama_url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
file_path = os.path.join(UPLOAD_DIR, file.filename)
filename = os.path.basename(file.filename)
file_path = os.path.join(UPLOAD_DIR, filename)
os.makedirs(UPLOAD_DIR, exist_ok=True)
# --- P1: save file locally ---
@@ -1606,13 +1803,13 @@ async def upload_model(
os.remove(file_path)
# Create model in ollama
model_name, ext = os.path.splitext(file.filename)
model_name, ext = os.path.splitext(filename)
log.info(f"Created Model: {model_name}") # DEBUG
create_payload = {
"model": model_name,
# Reference the file by its original name => the uploaded blob's digest
"files": {file.filename: f"sha256:{file_hash}"},
"files": {filename: f"sha256:{file_hash}"},
}
log.info(f"Model Payload: {create_payload}") # DEBUG
@@ -1629,7 +1826,7 @@ async def upload_model(
done_msg = {
"done": True,
"blob": f"sha256:{file_hash}",
"name": file.filename,
"name": filename,
"model_created": model_name,
}
yield f"data: {json.dumps(done_msg)}\n\n"
+378 -142
View File
@@ -8,7 +8,7 @@ from typing import Literal, Optional, overload
import aiohttp
from aiocache import cached
import requests
from urllib.parse import quote
from fastapi import Depends, FastAPI, HTTPException, Request, APIRouter
from fastapi.middleware.cors import CORSMiddleware
@@ -21,6 +21,8 @@ from open_webui.config import (
CACHE_DIR,
)
from open_webui.env import (
MODELS_CACHE_TTL,
AIOHTTP_CLIENT_SESSION_SSL,
AIOHTTP_CLIENT_TIMEOUT,
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST,
ENABLE_FORWARD_USER_INFO_HEADERS,
@@ -65,7 +67,7 @@ async def send_get_request(url, key=None, user: UserModel = None):
**({"Authorization": f"Bearer {key}"} if key else {}),
**(
{
"X-OpenWebUI-User-Name": user.name,
"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,
@@ -74,6 +76,7 @@ async def send_get_request(url, key=None, user: UserModel = None):
else {}
),
},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as response:
return await response.json()
except Exception as e:
@@ -92,20 +95,19 @@ async def cleanup_response(
await session.close()
def openai_o1_o3_handler(payload):
def openai_o_series_handler(payload):
"""
Handle o1, o3 specific parameters
Handle "o" series specific parameters
"""
if "max_tokens" in payload:
# Remove "max_tokens" from the payload
# Convert "max_tokens" to "max_completion_tokens" for all o-series models
payload["max_completion_tokens"] = payload["max_tokens"]
del payload["max_tokens"]
# Fix: o1 and o3 do not support the "system" role directly.
# For older models like "o1-mini" or "o1-preview", use role "user".
# For newer o1/o3 models, replace "system" with "developer".
# Handle system role conversion based on model type
if payload["messages"][0]["role"] == "system":
model_lower = payload["model"].lower()
# Legacy models use "user" role instead of "system"
if model_lower.startswith("o1-mini") or model_lower.startswith("o1-preview"):
payload["messages"][0]["role"] = "user"
else:
@@ -224,7 +226,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
),
**(
{
"X-OpenWebUI-User-Name": user.name,
"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,
@@ -352,21 +354,22 @@ async def get_all_models_responses(request: Request, user: UserModel) -> list:
), # Legacy support
)
connection_type = api_config.get("connection_type", "external")
prefix_id = api_config.get("prefix_id", None)
tags = api_config.get("tags", [])
if prefix_id:
for model in (
response if isinstance(response, list) else response.get("data", [])
):
for model in (
response if isinstance(response, list) else response.get("data", [])
):
if prefix_id:
model["id"] = f"{prefix_id}.{model['id']}"
if tags:
for model in (
response if isinstance(response, list) else response.get("data", [])
):
if tags:
model["tags"] = tags
if connection_type:
model["connection_type"] = connection_type
log.debug(f"get_all_models:responses() {responses}")
return responses
@@ -384,7 +387,7 @@ async def get_filtered_models(models, user):
return filtered_models
@cached(ttl=1)
@cached(ttl=MODELS_CACHE_TTL)
async def get_all_models(request: Request, user: UserModel) -> dict[str, list]:
log.info("get_all_models()")
@@ -414,6 +417,7 @@ async def get_all_models(request: Request, user: UserModel) -> dict[str, list]:
"name": model.get("name", model["id"]),
"owned_by": "openai",
"openai": model,
"connection_type": model.get("connection_type", "external"),
"urlIdx": idx,
}
for model in models
@@ -460,58 +464,74 @@ async def get_models(
url = request.app.state.config.OPENAI_API_BASE_URLS[url_idx]
key = request.app.state.config.OPENAI_API_KEYS[url_idx]
api_config = request.app.state.config.OPENAI_API_CONFIGS.get(
str(url_idx),
request.app.state.config.OPENAI_API_CONFIGS.get(url, {}), # Legacy support
)
r = None
async with aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
trust_env=True,
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST),
) as session:
try:
async with session.get(
f"{url}/models",
headers={
"Authorization": f"Bearer {key}",
"Content-Type": "application/json",
**(
{
"X-OpenWebUI-User-Name": user.name,
"X-OpenWebUI-User-Id": user.id,
"X-OpenWebUI-User-Email": user.email,
"X-OpenWebUI-User-Role": user.role,
}
if ENABLE_FORWARD_USER_INFO_HEADERS
else {}
),
},
) as r:
if r.status != 200:
# Extract response error details if available
error_detail = f"HTTP Error: {r.status}"
res = await r.json()
if "error" in res:
error_detail = f"External Error: {res['error']}"
raise Exception(error_detail)
headers = {
"Content-Type": "application/json",
**(
{
"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
else {}
),
}
response_data = await r.json()
if api_config.get("azure", False):
models = {
"data": api_config.get("model_ids", []) or [],
"object": "list",
}
else:
headers["Authorization"] = f"Bearer {key}"
# Check if we're calling OpenAI API based on the URL
if "api.openai.com" in url:
# Filter models according to the specified conditions
response_data["data"] = [
model
for model in response_data.get("data", [])
if not any(
name in model["id"]
for name in [
"babbage",
"dall-e",
"davinci",
"embedding",
"tts",
"whisper",
]
)
]
async with session.get(
f"{url}/models",
headers=headers,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as r:
if r.status != 200:
# Extract response error details if available
error_detail = f"HTTP Error: {r.status}"
res = await r.json()
if "error" in res:
error_detail = f"External Error: {res['error']}"
raise Exception(error_detail)
models = response_data
response_data = await r.json()
# Check if we're calling OpenAI API based on the URL
if "api.openai.com" in url:
# Filter models according to the specified conditions
response_data["data"] = [
model
for model in response_data.get("data", [])
if not any(
name in model["id"]
for name in [
"babbage",
"dall-e",
"davinci",
"embedding",
"tts",
"whisper",
]
)
]
models = response_data
except aiohttp.ClientError as e:
# ClientError covers all aiohttp requests issues
log.exception(f"Client error: {str(e)}")
@@ -533,6 +553,8 @@ class ConnectionVerificationForm(BaseModel):
url: str
key: str
config: Optional[dict] = None
@router.post("/verify")
async def verify_connection(
@@ -541,37 +563,64 @@ async def verify_connection(
url = form_data.url
key = form_data.key
api_config = form_data.config or {}
async with aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
trust_env=True,
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST),
) as session:
try:
async with session.get(
f"{url}/models",
headers={
"Authorization": f"Bearer {key}",
"Content-Type": "application/json",
**(
{
"X-OpenWebUI-User-Name": user.name,
"X-OpenWebUI-User-Id": user.id,
"X-OpenWebUI-User-Email": user.email,
"X-OpenWebUI-User-Role": user.role,
}
if ENABLE_FORWARD_USER_INFO_HEADERS
else {}
),
},
) as r:
if r.status != 200:
# Extract response error details if available
error_detail = f"HTTP Error: {r.status}"
res = await r.json()
if "error" in res:
error_detail = f"External Error: {res['error']}"
raise Exception(error_detail)
headers = {
"Content-Type": "application/json",
**(
{
"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
else {}
),
}
response_data = await r.json()
return response_data
if api_config.get("azure", False):
headers["api-key"] = key
api_version = api_config.get("api_version", "") or "2023-03-15-preview"
async with session.get(
url=f"{url}/openai/models?api-version={api_version}",
headers=headers,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as r:
if r.status != 200:
# Extract response error details if available
error_detail = f"HTTP Error: {r.status}"
res = await r.json()
if "error" in res:
error_detail = f"External Error: {res['error']}"
raise Exception(error_detail)
response_data = await r.json()
return response_data
else:
headers["Authorization"] = f"Bearer {key}"
async with session.get(
f"{url}/models",
headers=headers,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as r:
if r.status != 200:
# Extract response error details if available
error_detail = f"HTTP Error: {r.status}"
res = await r.json()
if "error" in res:
error_detail = f"External Error: {res['error']}"
raise Exception(error_detail)
response_data = await r.json()
return response_data
except aiohttp.ClientError as e:
# ClientError covers all aiohttp requests issues
@@ -585,6 +634,74 @@ async def verify_connection(
raise HTTPException(status_code=500, detail=error_detail)
def get_azure_allowed_params(api_version: str) -> set[str]:
allowed_params = {
"messages",
"temperature",
"role",
"content",
"contentPart",
"contentPartImage",
"enhancements",
"dataSources",
"n",
"stream",
"stop",
"max_tokens",
"presence_penalty",
"frequency_penalty",
"logit_bias",
"user",
"function_call",
"functions",
"tools",
"tool_choice",
"top_p",
"log_probs",
"top_logprobs",
"response_format",
"seed",
"max_completion_tokens",
}
try:
if api_version >= "2024-09-01-preview":
allowed_params.add("stream_options")
except ValueError:
log.debug(
f"Invalid API version {api_version} for Azure OpenAI. Defaulting to allowed parameters."
)
return allowed_params
def convert_to_azure_payload(url, payload: dict, api_version: str):
model = payload.get("model", "")
# Filter allowed parameters based on Azure OpenAI API
allowed_params = get_azure_allowed_params(api_version)
# Special handling for o-series models
if model.startswith("o") and model.endswith("-mini"):
# Convert max_tokens to max_completion_tokens for o-series models
if "max_tokens" in payload:
payload["max_completion_tokens"] = payload["max_tokens"]
del payload["max_tokens"]
# Remove temperature if not 1 for o-series models
if "temperature" in payload and payload["temperature"] != 1:
log.debug(
f"Removing temperature parameter for o-series model {model} as only default value (1) is supported"
)
del payload["temperature"]
# Filter out unsupported parameters
payload = {k: v for k, v in payload.items() if k in allowed_params}
url = f"{url}/openai/deployments/{model}"
return url, payload
@router.post("/chat/completions")
async def generate_chat_completion(
request: Request,
@@ -610,8 +727,12 @@ async def generate_chat_completion(
model_id = model_info.base_model_id
params = model_info.params.model_dump()
payload = apply_model_params_to_body_openai(params, payload)
payload = apply_model_system_prompt_to_body(params, payload, metadata, user)
if params:
system = params.pop("system", None)
payload = apply_model_params_to_body_openai(params, payload)
payload = apply_model_system_prompt_to_body(system, payload, metadata, user)
# Check if user has access to the model
if not bypass_filter and user.role == "user":
@@ -666,10 +787,10 @@ async def generate_chat_completion(
url = request.app.state.config.OPENAI_API_BASE_URLS[idx]
key = request.app.state.config.OPENAI_API_KEYS[idx]
# Fix: o1,o3 does not support the "max_tokens" parameter, Modify "max_tokens" to "max_completion_tokens"
is_o1_o3 = payload["model"].lower().startswith(("o1", "o3-"))
if is_o1_o3:
payload = openai_o1_o3_handler(payload)
# Check if model is from "o" series
is_o_series = payload["model"].lower().startswith(("o1", "o3", "o4"))
if is_o_series:
payload = openai_o_series_handler(payload)
elif "api.openai.com" not in url:
# Remove "max_completion_tokens" from the payload for backward compatibility
if "max_completion_tokens" in payload:
@@ -685,6 +806,43 @@ async def generate_chat_completion(
convert_logit_bias_input_to_json(payload["logit_bias"])
)
headers = {
"Content-Type": "application/json",
**(
{
"HTTP-Referer": "https://openwebui.com/",
"X-Title": "Open WebUI",
}
if "openrouter.ai" in url
else {}
),
**(
{
"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,
**(
{"X-OpenWebUI-Chat-Id": metadata.get("chat_id")}
if metadata and metadata.get("chat_id")
else {}
),
}
if ENABLE_FORWARD_USER_INFO_HEADERS
else {}
),
}
if api_config.get("azure", False):
api_version = api_config.get("api_version", "2023-03-15-preview")
request_url, payload = convert_to_azure_payload(url, payload, api_version)
headers["api-key"] = key
headers["api-version"] = api_version
request_url = f"{request_url}/chat/completions?api-version={api_version}"
else:
request_url = f"{url}/chat/completions"
headers["Authorization"] = f"Bearer {key}"
payload = json.dumps(payload)
r = None
@@ -699,30 +857,10 @@ async def generate_chat_completion(
r = await session.request(
method="POST",
url=f"{url}/chat/completions",
url=request_url,
data=payload,
headers={
"Authorization": f"Bearer {key}",
"Content-Type": "application/json",
**(
{
"HTTP-Referer": "https://openwebui.com/",
"X-Title": "Open WebUI",
}
if "openrouter.ai" in url
else {}
),
**(
{
"X-OpenWebUI-User-Name": user.name,
"X-OpenWebUI-User-Id": user.id,
"X-OpenWebUI-User-Email": user.email,
"X-OpenWebUI-User-Role": user.role,
}
if ENABLE_FORWARD_USER_INFO_HEADERS
else {}
),
},
headers=headers,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
)
# Check if response is SSE
@@ -760,10 +898,88 @@ async def generate_chat_completion(
detail=detail if detail else "Open WebUI: Server Connection Error",
)
finally:
if not streaming and session:
if r:
r.close()
await session.close()
if not streaming:
await cleanup_response(r, session)
async def embeddings(request: Request, form_data: dict, user):
"""
Calls the embeddings endpoint for OpenAI-compatible providers.
Args:
request (Request): The FastAPI request context.
form_data (dict): OpenAI-compatible embeddings payload.
user (UserModel): The authenticated user.
Returns:
dict: OpenAI-compatible embeddings response.
"""
idx = 0
# Prepare payload/body
body = json.dumps(form_data)
# Find correct backend url/key based on model
await get_all_models(request, user=user)
model_id = form_data.get("model")
models = request.app.state.OPENAI_MODELS
if model_id in models:
idx = models[model_id]["urlIdx"]
url = request.app.state.config.OPENAI_API_BASE_URLS[idx]
key = request.app.state.config.OPENAI_API_KEYS[idx]
r = None
session = None
streaming = False
try:
session = aiohttp.ClientSession(trust_env=True)
r = await session.request(
method="POST",
url=f"{url}/embeddings",
data=body,
headers={
"Authorization": f"Bearer {key}",
"Content-Type": "application/json",
**(
{
"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 {}
),
},
)
r.raise_for_status()
if "text/event-stream" in r.headers.get("Content-Type", ""):
streaming = True
return StreamingResponse(
r.content,
status_code=r.status,
headers=dict(r.headers),
background=BackgroundTask(
cleanup_response, response=r, session=session
),
)
else:
response_data = await r.json()
return response_data
except Exception as e:
log.exception(e)
detail = None
if r is not None:
try:
res = await r.json()
if "error" in res:
detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
except Exception:
detail = f"External: {e}"
raise HTTPException(
status_code=r.status if r else 500,
detail=detail if detail else "Open WebUI: Server Connection Error",
)
finally:
if not streaming:
await cleanup_response(r, session)
@router.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
@@ -777,31 +993,53 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
idx = 0
url = request.app.state.config.OPENAI_API_BASE_URLS[idx]
key = request.app.state.config.OPENAI_API_KEYS[idx]
api_config = request.app.state.config.OPENAI_API_CONFIGS.get(
str(idx),
request.app.state.config.OPENAI_API_CONFIGS.get(
request.app.state.config.OPENAI_API_BASE_URLS[idx], {}
), # Legacy support
)
r = None
session = None
streaming = False
try:
headers = {
"Content-Type": "application/json",
**(
{
"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
else {}
),
}
if api_config.get("azure", False):
api_version = api_config.get("api_version", "2023-03-15-preview")
headers["api-key"] = key
headers["api-version"] = api_version
payload = json.loads(body)
url, payload = convert_to_azure_payload(url, payload, api_version)
body = json.dumps(payload).encode()
request_url = f"{url}/{path}?api-version={api_version}"
else:
headers["Authorization"] = f"Bearer {key}"
request_url = f"{url}/{path}"
session = aiohttp.ClientSession(trust_env=True)
r = await session.request(
method=request.method,
url=f"{url}/{path}",
url=request_url,
data=body,
headers={
"Authorization": f"Bearer {key}",
"Content-Type": "application/json",
**(
{
"X-OpenWebUI-User-Name": user.name,
"X-OpenWebUI-User-Id": user.id,
"X-OpenWebUI-User-Email": user.email,
"X-OpenWebUI-User-Role": user.role,
}
if ENABLE_FORWARD_USER_INFO_HEADERS
else {}
),
},
headers=headers,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
)
r.raise_for_status()
@@ -837,7 +1075,5 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
detail=detail if detail else "Open WebUI: Server Connection Error",
)
finally:
if not streaming and session:
if r:
r.close()
await session.close()
if not streaming:
await cleanup_response(r, session)
+17 -7
View File
@@ -18,7 +18,7 @@ from pydantic import BaseModel
from starlette.responses import FileResponse
from typing import Optional
from open_webui.env import SRC_LOG_LEVELS
from open_webui.env import SRC_LOG_LEVELS, AIOHTTP_CLIENT_SESSION_SSL
from open_webui.config import CACHE_DIR
from open_webui.constants import ERROR_MESSAGES
@@ -66,10 +66,13 @@ async def process_pipeline_inlet_filter(request, payload, user, models):
if "pipeline" in model:
sorted_filters.append(model)
async with aiohttp.ClientSession() as session:
async with aiohttp.ClientSession(trust_env=True) as session:
for filter in sorted_filters:
urlIdx = filter.get("urlIdx")
if urlIdx is None:
try:
urlIdx = int(urlIdx)
except:
continue
url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx]
@@ -89,6 +92,7 @@ async def process_pipeline_inlet_filter(request, payload, user, models):
f"{url}/{filter['id']}/filter/inlet",
headers=headers,
json=request_data,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as response:
payload = await response.json()
response.raise_for_status()
@@ -115,10 +119,13 @@ async def process_pipeline_outlet_filter(request, payload, user, models):
if "pipeline" in model:
sorted_filters = [model] + sorted_filters
async with aiohttp.ClientSession() as session:
async with aiohttp.ClientSession(trust_env=True) as session:
for filter in sorted_filters:
urlIdx = filter.get("urlIdx")
if urlIdx is None:
try:
urlIdx = int(urlIdx)
except:
continue
url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx]
@@ -138,6 +145,7 @@ async def process_pipeline_outlet_filter(request, payload, user, models):
f"{url}/{filter['id']}/filter/outlet",
headers=headers,
json=request_data,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as response:
payload = await response.json()
response.raise_for_status()
@@ -197,8 +205,10 @@ async def upload_pipeline(
user=Depends(get_admin_user),
):
log.info(f"upload_pipeline: urlIdx={urlIdx}, filename={file.filename}")
filename = os.path.basename(file.filename)
# Check if the uploaded file is a python file
if not (file.filename and file.filename.endswith(".py")):
if not (filename and filename.endswith(".py")):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Only Python (.py) files are allowed.",
@@ -206,7 +216,7 @@ async def upload_pipeline(
upload_folder = f"{CACHE_DIR}/pipelines"
os.makedirs(upload_folder, exist_ok=True)
file_path = os.path.join(upload_folder, file.filename)
file_path = os.path.join(upload_folder, filename)
r = None
try:
File diff suppressed because it is too large Load Diff
+110 -25
View File
@@ -9,6 +9,7 @@ import re
from open_webui.utils.chat import generate_chat_completion
from open_webui.utils.task import (
title_generation_template,
follow_up_generation_template,
query_generation_template,
image_prompt_generation_template,
autocomplete_generation_template,
@@ -20,14 +21,12 @@ from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.constants import TASKS
from open_webui.routers.pipelines import process_pipeline_inlet_filter
from open_webui.utils.filter import (
get_sorted_filter_ids,
process_filter_functions,
)
from open_webui.utils.task import get_task_model_id
from open_webui.config import (
DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE,
DEFAULT_FOLLOW_UP_GENERATION_PROMPT_TEMPLATE,
DEFAULT_TAGS_GENERATION_PROMPT_TEMPLATE,
DEFAULT_IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE,
DEFAULT_QUERY_GENERATION_PROMPT_TEMPLATE,
@@ -61,6 +60,8 @@ async def get_task_config(request: Request, user=Depends(get_verified_user)):
"ENABLE_AUTOCOMPLETE_GENERATION": request.app.state.config.ENABLE_AUTOCOMPLETE_GENERATION,
"AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH": request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH,
"TAGS_GENERATION_PROMPT_TEMPLATE": request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE,
"FOLLOW_UP_GENERATION_PROMPT_TEMPLATE": request.app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE,
"ENABLE_FOLLOW_UP_GENERATION": request.app.state.config.ENABLE_FOLLOW_UP_GENERATION,
"ENABLE_TAGS_GENERATION": request.app.state.config.ENABLE_TAGS_GENERATION,
"ENABLE_TITLE_GENERATION": request.app.state.config.ENABLE_TITLE_GENERATION,
"ENABLE_SEARCH_QUERY_GENERATION": request.app.state.config.ENABLE_SEARCH_QUERY_GENERATION,
@@ -79,6 +80,8 @@ class TaskConfigForm(BaseModel):
ENABLE_AUTOCOMPLETE_GENERATION: bool
AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH: int
TAGS_GENERATION_PROMPT_TEMPLATE: str
FOLLOW_UP_GENERATION_PROMPT_TEMPLATE: str
ENABLE_FOLLOW_UP_GENERATION: bool
ENABLE_TAGS_GENERATION: bool
ENABLE_SEARCH_QUERY_GENERATION: bool
ENABLE_RETRIEVAL_QUERY_GENERATION: bool
@@ -97,6 +100,13 @@ async def update_task_config(
form_data.TITLE_GENERATION_PROMPT_TEMPLATE
)
request.app.state.config.ENABLE_FOLLOW_UP_GENERATION = (
form_data.ENABLE_FOLLOW_UP_GENERATION
)
request.app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE = (
form_data.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE
)
request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE = (
form_data.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE
)
@@ -136,6 +146,8 @@ async def update_task_config(
"AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH": request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH,
"TAGS_GENERATION_PROMPT_TEMPLATE": request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE,
"ENABLE_TAGS_GENERATION": request.app.state.config.ENABLE_TAGS_GENERATION,
"ENABLE_FOLLOW_UP_GENERATION": request.app.state.config.ENABLE_FOLLOW_UP_GENERATION,
"FOLLOW_UP_GENERATION_PROMPT_TEMPLATE": request.app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE,
"ENABLE_SEARCH_QUERY_GENERATION": request.app.state.config.ENABLE_SEARCH_QUERY_GENERATION,
"ENABLE_RETRIEVAL_QUERY_GENERATION": request.app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION,
"QUERY_GENERATION_PROMPT_TEMPLATE": request.app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE,
@@ -186,20 +198,100 @@ async def generate_title(
else:
template = DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE
messages = form_data["messages"]
# Remove reasoning details from the messages
for message in messages:
message["content"] = re.sub(
r"<details\s+type=\"reasoning\"[^>]*>.*?<\/details>",
"",
message["content"],
flags=re.S,
).strip()
content = title_generation_template(
template,
messages,
form_data["messages"],
{
"name": user.name,
"location": user.info.get("location") if user.info else None,
},
)
max_tokens = (
models[task_model_id].get("info", {}).get("params", {}).get("max_tokens", 1000)
)
payload = {
"model": task_model_id,
"messages": [{"role": "user", "content": content}],
"stream": False,
**(
{"max_tokens": max_tokens}
if models[task_model_id].get("owned_by") == "ollama"
else {
"max_completion_tokens": max_tokens,
}
),
"metadata": {
**(request.state.metadata if hasattr(request.state, "metadata") else {}),
"task": str(TASKS.TITLE_GENERATION),
"task_body": form_data,
"chat_id": form_data.get("chat_id", None),
},
}
# Process the payload through the pipeline
try:
payload = await process_pipeline_inlet_filter(request, payload, user, models)
except Exception as e:
raise e
try:
return await generate_chat_completion(request, form_data=payload, user=user)
except Exception as e:
log.error("Exception occurred", exc_info=True)
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={"detail": "An internal error has occurred."},
)
@router.post("/follow_up/completions")
async def generate_follow_ups(
request: Request, form_data: dict, user=Depends(get_verified_user)
):
if not request.app.state.config.ENABLE_FOLLOW_UP_GENERATION:
return JSONResponse(
status_code=status.HTTP_200_OK,
content={"detail": "Follow-up generation is disabled"},
)
if getattr(request.state, "direct", False) and hasattr(request.state, "model"):
models = {
request.state.model["id"]: request.state.model,
}
else:
models = request.app.state.MODELS
model_id = form_data["model"]
if model_id not in models:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Model not found",
)
# Check if the user has a custom task model
# If the user has a custom task model, use that model
task_model_id = get_task_model_id(
model_id,
request.app.state.config.TASK_MODEL,
request.app.state.config.TASK_MODEL_EXTERNAL,
models,
)
log.debug(
f"generating chat title using model {task_model_id} for user {user.email} "
)
if request.app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE != "":
template = request.app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE
else:
template = DEFAULT_FOLLOW_UP_GENERATION_PROMPT_TEMPLATE
content = follow_up_generation_template(
template,
form_data["messages"],
{
"name": user.name,
"location": user.info.get("location") if user.info else None,
@@ -210,16 +302,9 @@ async def generate_title(
"model": task_model_id,
"messages": [{"role": "user", "content": content}],
"stream": False,
**(
{"max_tokens": 1000}
if models[task_model_id].get("owned_by") == "ollama"
else {
"max_completion_tokens": 1000,
}
),
"metadata": {
**(request.state.metadata if hasattr(request.state, "metadata") else {}),
"task": str(TASKS.TITLE_GENERATION),
"task": str(TASKS.FOLLOW_UP_GENERATION),
"task_body": form_data,
"chat_id": form_data.get("chat_id", None),
},
@@ -610,11 +695,11 @@ async def generate_emoji(
"max_completion_tokens": 4,
}
),
"chat_id": form_data.get("chat_id", None),
"metadata": {
**(request.state.metadata if hasattr(request.state, "metadata") else {}),
"task": str(TASKS.EMOJI_GENERATION),
"task_body": form_data,
"chat_id": form_data.get("chat_id", None),
},
}
+95 -18
View File
@@ -2,6 +2,9 @@ import logging
from pathlib import Path
from typing import Optional
import time
import re
import aiohttp
from pydantic import BaseModel, HttpUrl
from open_webui.models.tools import (
ToolForm,
@@ -10,17 +13,18 @@ from open_webui.models.tools import (
ToolUserResponse,
Tools,
)
from open_webui.utils.plugin import load_tools_module_by_id, replace_imports
from open_webui.utils.plugin import load_tool_module_by_id, replace_imports
from open_webui.config import CACHE_DIR
from open_webui.constants import ERROR_MESSAGES
from fastapi import APIRouter, Depends, HTTPException, Request, status
from open_webui.utils.tools import get_tools_specs
from open_webui.utils.tools import get_tool_specs
from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.utils.access_control import has_access, has_permission
from open_webui.env import SRC_LOG_LEVELS
from open_webui.utils.tools import get_tool_servers_data
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MAIN"])
@@ -45,22 +49,22 @@ async def get_tools(request: Request, user=Depends(get_verified_user)):
)
tools = Tools.get_tools()
for idx, server in enumerate(request.app.state.TOOL_SERVERS):
for server in request.app.state.TOOL_SERVERS:
tools.append(
ToolUserResponse(
**{
"id": f"server:{server['idx']}",
"user_id": f"server:{server['idx']}",
"name": server["openapi"]
"name": server.get("openapi", {})
.get("info", {})
.get("title", "Tool Server"),
"meta": {
"description": server["openapi"]
"description": server.get("openapi", {})
.get("info", {})
.get("description", ""),
},
"access_control": request.app.state.config.TOOL_SERVER_CONNECTIONS[
idx
server["idx"]
]
.get("config", {})
.get("access_control", None),
@@ -95,6 +99,81 @@ async def get_tool_list(user=Depends(get_verified_user)):
return tools
############################
# LoadFunctionFromLink
############################
class LoadUrlForm(BaseModel):
url: HttpUrl
def github_url_to_raw_url(url: str) -> str:
# Handle 'tree' (folder) URLs (add main.py at the end)
m1 = re.match(r"https://github\.com/([^/]+)/([^/]+)/tree/([^/]+)/(.*)", url)
if m1:
org, repo, branch, path = m1.groups()
return f"https://raw.githubusercontent.com/{org}/{repo}/refs/heads/{branch}/{path.rstrip('/')}/main.py"
# Handle 'blob' (file) URLs
m2 = re.match(r"https://github\.com/([^/]+)/([^/]+)/blob/([^/]+)/(.*)", url)
if m2:
org, repo, branch, path = m2.groups()
return (
f"https://raw.githubusercontent.com/{org}/{repo}/refs/heads/{branch}/{path}"
)
# No match; return as-is
return url
@router.post("/load/url", response_model=Optional[dict])
async def load_tool_from_url(
request: Request, form_data: LoadUrlForm, user=Depends(get_admin_user)
):
# NOTE: This is NOT a SSRF vulnerability:
# This endpoint is admin-only (see get_admin_user), meant for *trusted* internal use,
# and does NOT accept untrusted user input. Access is enforced by authentication.
url = str(form_data.url)
if not url:
raise HTTPException(status_code=400, detail="Please enter a valid URL")
url = github_url_to_raw_url(url)
url_parts = url.rstrip("/").split("/")
file_name = url_parts[-1]
tool_name = (
file_name[:-3]
if (
file_name.endswith(".py")
and (not file_name.startswith(("main.py", "index.py", "__init__.py")))
)
else url_parts[-2] if len(url_parts) > 1 else "function"
)
try:
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get(
url, headers={"Content-Type": "application/json"}
) as resp:
if resp.status != 200:
raise HTTPException(
status_code=resp.status, detail="Failed to fetch the tool"
)
data = await resp.text()
if not data:
raise HTTPException(
status_code=400, detail="No data received from the URL"
)
return {
"name": tool_name,
"content": data,
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error importing tool: {e}")
############################
# ExportTools
############################
@@ -137,15 +216,15 @@ async def create_new_tools(
if tools is None:
try:
form_data.content = replace_imports(form_data.content)
tools_module, frontmatter = load_tools_module_by_id(
tool_module, frontmatter = load_tool_module_by_id(
form_data.id, content=form_data.content
)
form_data.meta.manifest = frontmatter
TOOLS = request.app.state.TOOLS
TOOLS[form_data.id] = tools_module
TOOLS[form_data.id] = tool_module
specs = get_tools_specs(TOOLS[form_data.id])
specs = get_tool_specs(TOOLS[form_data.id])
tools = Tools.insert_new_tool(user.id, form_data, specs)
tool_cache_dir = CACHE_DIR / "tools" / form_data.id
@@ -226,15 +305,13 @@ async def update_tools_by_id(
try:
form_data.content = replace_imports(form_data.content)
tools_module, frontmatter = load_tools_module_by_id(
id, content=form_data.content
)
tool_module, frontmatter = load_tool_module_by_id(id, content=form_data.content)
form_data.meta.manifest = frontmatter
TOOLS = request.app.state.TOOLS
TOOLS[id] = tools_module
TOOLS[id] = tool_module
specs = get_tools_specs(TOOLS[id])
specs = get_tool_specs(TOOLS[id])
updated = {
**form_data.model_dump(exclude={"id"}),
@@ -332,7 +409,7 @@ async def get_tools_valves_spec_by_id(
if id in request.app.state.TOOLS:
tools_module = request.app.state.TOOLS[id]
else:
tools_module, _ = load_tools_module_by_id(id)
tools_module, _ = load_tool_module_by_id(id)
request.app.state.TOOLS[id] = tools_module
if hasattr(tools_module, "Valves"):
@@ -375,7 +452,7 @@ async def update_tools_valves_by_id(
if id in request.app.state.TOOLS:
tools_module = request.app.state.TOOLS[id]
else:
tools_module, _ = load_tools_module_by_id(id)
tools_module, _ = load_tool_module_by_id(id)
request.app.state.TOOLS[id] = tools_module
if not hasattr(tools_module, "Valves"):
@@ -431,7 +508,7 @@ async def get_tools_user_valves_spec_by_id(
if id in request.app.state.TOOLS:
tools_module = request.app.state.TOOLS[id]
else:
tools_module, _ = load_tools_module_by_id(id)
tools_module, _ = load_tool_module_by_id(id)
request.app.state.TOOLS[id] = tools_module
if hasattr(tools_module, "UserValves"):
@@ -455,7 +532,7 @@ async def update_tools_user_valves_by_id(
if id in request.app.state.TOOLS:
tools_module = request.app.state.TOOLS[id]
else:
tools_module, _ = load_tools_module_by_id(id)
tools_module, _ = load_tool_module_by_id(id)
request.app.state.TOOLS[id] = tools_module
if hasattr(tools_module, "UserValves"):
+133 -24
View File
@@ -6,6 +6,8 @@ from open_webui.models.groups import Groups
from open_webui.models.chats import Chats
from open_webui.models.users import (
UserModel,
UserListResponse,
UserInfoListResponse,
UserRoleUpdateForm,
Users,
UserSettings,
@@ -13,14 +15,18 @@ from open_webui.models.users import (
)
from open_webui.socket.main import get_active_status_by_user_id
from open_webui.socket.main import (
get_active_status_by_user_id,
get_active_user_ids,
get_user_active_status,
)
from open_webui.constants import ERROR_MESSAGES
from open_webui.env import SRC_LOG_LEVELS
from fastapi import APIRouter, Depends, HTTPException, Request, status
from pydantic import BaseModel
from open_webui.utils.auth import get_admin_user, get_password_hash, get_verified_user
from open_webui.utils.access_control import get_permissions
from open_webui.utils.access_control import get_permissions, has_permission
log = logging.getLogger(__name__)
@@ -28,18 +34,61 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
router = APIRouter()
############################
# GetActiveUsers
############################
@router.get("/active")
async def get_active_users(
user=Depends(get_verified_user),
):
"""
Get a list of active users.
"""
return {
"user_ids": get_active_user_ids(),
}
############################
# GetUsers
############################
@router.get("/", response_model=list[UserModel])
PAGE_ITEM_COUNT = 30
@router.get("/", response_model=UserListResponse)
async def get_users(
skip: Optional[int] = None,
limit: Optional[int] = None,
query: Optional[str] = None,
order_by: Optional[str] = None,
direction: Optional[str] = None,
page: Optional[int] = 1,
user=Depends(get_admin_user),
):
return Users.get_users(skip, limit)
limit = PAGE_ITEM_COUNT
page = max(1, page)
skip = (page - 1) * limit
filter = {}
if query:
filter["query"] = query
if order_by:
filter["order_by"] = order_by
if direction:
filter["direction"] = direction
return Users.get_users(filter=filter, skip=skip, limit=limit)
@router.get("/all", response_model=UserInfoListResponse)
async def get_all_users(
user=Depends(get_admin_user),
):
return Users.get_users()
############################
@@ -85,9 +134,16 @@ class SharingPermissions(BaseModel):
class ChatPermissions(BaseModel):
controls: bool = True
system_prompt: bool = True
file_upload: bool = True
delete: bool = True
edit: bool = True
share: bool = True
export: bool = True
stt: bool = True
tts: bool = True
call: bool = True
multiple_models: bool = True
temporary: bool = True
temporary_enforced: bool = False
@@ -97,6 +153,7 @@ class FeaturesPermissions(BaseModel):
web_search: bool = True
image_generation: bool = True
code_interpreter: bool = True
notes: bool = True
class UserPermissions(BaseModel):
@@ -132,22 +189,6 @@ async def update_default_user_permissions(
return request.app.state.config.USER_PERMISSIONS
############################
# UpdateUserRole
############################
@router.post("/update/role", response_model=Optional[UserModel])
async def update_user_role(form_data: UserRoleUpdateForm, user=Depends(get_admin_user)):
if user.id != form_data.id and form_data.id != Users.get_first_user().id:
return Users.update_user_role_by_id(form_data.id, form_data.role)
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACTION_PROHIBITED,
)
############################
# GetUserSettingsBySessionUser
############################
@@ -172,9 +213,22 @@ async def get_user_settings_by_session_user(user=Depends(get_verified_user)):
@router.post("/user/settings/update", response_model=UserSettings)
async def update_user_settings_by_session_user(
form_data: UserSettings, user=Depends(get_verified_user)
request: Request, form_data: UserSettings, user=Depends(get_verified_user)
):
user = Users.update_user_settings_by_id(user.id, form_data.model_dump())
updated_user_settings = form_data.model_dump()
if (
user.role != "admin"
and "toolServers" in updated_user_settings.get("ui").keys()
and not has_permission(
user.id,
"features.direct_tool_servers",
request.app.state.config.USER_PERMISSIONS,
)
):
# If the user is not an admin and does not have permission to use tool servers, remove the key
updated_user_settings["ui"].pop("toolServers", None)
user = Users.update_user_settings_by_id(user.id, updated_user_settings)
if user:
return user.settings
else:
@@ -273,6 +327,18 @@ async def get_user_by_id(user_id: str, user=Depends(get_verified_user)):
)
############################
# GetUserActiveStatusById
############################
@router.get("/{user_id}/active", response_model=dict)
async def get_user_active_status_by_id(user_id: str, user=Depends(get_verified_user)):
return {
"active": get_user_active_status(user_id),
}
############################
# UpdateUserById
############################
@@ -284,6 +350,32 @@ async def update_user_by_id(
form_data: UserUpdateForm,
session_user=Depends(get_admin_user),
):
# Prevent modification of the primary admin user by other admins
try:
first_user = Users.get_first_user()
if first_user:
if user_id == first_user.id:
if session_user.id != user_id:
# If the user trying to update is the primary admin, and they are not the primary admin themselves
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACTION_PROHIBITED,
)
if form_data.role != "admin":
# If the primary admin is trying to change their own role, prevent it
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACTION_PROHIBITED,
)
except Exception as e:
log.error(f"Error checking primary admin status: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Could not verify primary admin status.",
)
user = Users.get_user_by_id(user_id)
if user:
@@ -304,6 +396,7 @@ async def update_user_by_id(
updated_user = Users.update_user_by_id(
user_id,
{
"role": form_data.role,
"name": form_data.name,
"email": form_data.email.lower(),
"profile_image_url": form_data.profile_image_url,
@@ -331,6 +424,21 @@ async def update_user_by_id(
@router.delete("/{user_id}", response_model=bool)
async def delete_user_by_id(user_id: str, user=Depends(get_admin_user)):
# Prevent deletion of the primary admin user
try:
first_user = Users.get_first_user()
if first_user and user_id == first_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACTION_PROHIBITED,
)
except Exception as e:
log.error(f"Error checking primary admin status: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Could not verify primary admin status.",
)
if user.id != user_id:
result = Auths.delete_auth_by_id(user_id)
@@ -342,6 +450,7 @@ async def delete_user_by_id(user_id: str, user=Depends(get_admin_user)):
detail=ERROR_MESSAGES.DELETE_USER_ERROR,
)
# Prevent self-deletion
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACTION_PROHIBITED,
+1 -1
View File
@@ -33,7 +33,7 @@ class CodeForm(BaseModel):
@router.post("/code/format")
async def format_code(form_data: CodeForm, user=Depends(get_verified_user)):
async def format_code(form_data: CodeForm, user=Depends(get_admin_user)):
try:
formatted_code = black.format_str(form_data.code, mode=black.Mode())
return {"code": formatted_code}
+384 -82
View File
@@ -1,17 +1,21 @@
import asyncio
import random
import socketio
import logging
import sys
import time
from typing import Dict, Set
from redis import asyncio as aioredis
import pycrdt as Y
from open_webui.models.users import Users, UserNameResponse
from open_webui.models.channels import Channels
from open_webui.models.chats import Chats
from open_webui.models.notes import Notes, NoteUpdateForm
from open_webui.utils.redis import (
parse_redis_sentinel_url,
get_sentinels_from_env,
AsyncRedisSentinelManager,
get_sentinel_url_from_env,
)
from open_webui.env import (
@@ -23,7 +27,11 @@ from open_webui.env import (
WEBSOCKET_SENTINEL_HOSTS,
)
from open_webui.utils.auth import decode_token
from open_webui.socket.utils import RedisDict, RedisLock
from open_webui.socket.utils import RedisDict, RedisLock, YdocManager
from open_webui.tasks import create_task, stop_item_tasks
from open_webui.utils.redis import get_redis_connection
from open_webui.utils.access_control import has_access, get_users_with_access
from open_webui.env import (
GLOBAL_LOG_LEVEL,
@@ -36,17 +44,14 @@ log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["SOCKET"])
REDIS = None
if WEBSOCKET_MANAGER == "redis":
if WEBSOCKET_SENTINEL_HOSTS:
redis_config = parse_redis_sentinel_url(WEBSOCKET_REDIS_URL)
mgr = AsyncRedisSentinelManager(
WEBSOCKET_SENTINEL_HOSTS.split(","),
sentinel_port=int(WEBSOCKET_SENTINEL_PORT),
redis_port=redis_config["port"],
service=redis_config["service"],
db=redis_config["db"],
username=redis_config["username"],
password=redis_config["password"],
mgr = socketio.AsyncRedisManager(
get_sentinel_url_from_env(
WEBSOCKET_REDIS_URL, WEBSOCKET_SENTINEL_HOSTS, WEBSOCKET_SENTINEL_PORT
)
)
else:
mgr = socketio.AsyncRedisManager(WEBSOCKET_REDIS_URL)
@@ -75,6 +80,14 @@ TIMEOUT_DURATION = 3
if WEBSOCKET_MANAGER == "redis":
log.debug("Using Redis to manage websockets.")
REDIS = get_redis_connection(
redis_url=WEBSOCKET_REDIS_URL,
redis_sentinels=get_sentinels_from_env(
WEBSOCKET_SENTINEL_HOSTS, WEBSOCKET_SENTINEL_PORT
),
async_mode=True,
)
redis_sentinels = get_sentinels_from_env(
WEBSOCKET_SENTINEL_HOSTS, WEBSOCKET_SENTINEL_PORT
)
@@ -107,14 +120,37 @@ else:
SESSION_POOL = {}
USER_POOL = {}
USAGE_POOL = {}
aquire_func = release_func = renew_func = lambda: True
YDOC_MANAGER = YdocManager(
redis=REDIS,
redis_key_prefix="open-webui:ydoc:documents",
)
async def periodic_usage_pool_cleanup():
if not aquire_func():
log.debug("Usage pool cleanup lock already exists. Not running it.")
return
log.debug("Running periodic_usage_pool_cleanup")
max_retries = 2
retry_delay = random.uniform(
WEBSOCKET_REDIS_LOCK_TIMEOUT / 2, WEBSOCKET_REDIS_LOCK_TIMEOUT
)
for attempt in range(max_retries + 1):
if aquire_func():
break
else:
if attempt < max_retries:
log.debug(
f"Cleanup lock already exists. Retry {attempt + 1} after {retry_delay}s..."
)
await asyncio.sleep(retry_delay)
else:
log.warning(
"Failed to acquire cleanup lock after retries. Skipping cleanup."
)
return
log.debug("Running periodic_cleanup")
try:
while True:
if not renew_func():
@@ -141,11 +177,6 @@ async def periodic_usage_pool_cleanup():
USAGE_POOL[model_id] = connections
send_usage = True
if send_usage:
# Emit updated usage information after cleaning
await sio.emit("usage", {"models": get_models_in_use()})
await asyncio.sleep(TIMEOUT_DURATION)
finally:
release_func()
@@ -163,20 +194,59 @@ def get_models_in_use():
return models_in_use
def get_active_user_ids():
"""Get the list of active user IDs."""
return list(USER_POOL.keys())
def get_user_active_status(user_id):
"""Check if a user is currently active."""
return user_id in USER_POOL
def get_user_id_from_session_pool(sid):
user = SESSION_POOL.get(sid)
if user:
return user["id"]
return None
def get_session_ids_from_room(room):
"""Get all session IDs from a specific room."""
active_session_ids = sio.manager.get_participants(
namespace="/",
room=room,
)
return [session_id[0] for session_id in active_session_ids]
def get_user_ids_from_room(room):
active_session_ids = get_session_ids_from_room(room)
active_user_ids = list(
set([SESSION_POOL.get(session_id)["id"] for session_id in active_session_ids])
)
return active_user_ids
def get_active_status_by_user_id(user_id):
if user_id in USER_POOL:
return True
return False
@sio.on("usage")
async def usage(sid, data):
model_id = data["model"]
# Record the timestamp for the last update
current_time = int(time.time())
if sid in SESSION_POOL:
model_id = data["model"]
# Record the timestamp for the last update
current_time = int(time.time())
# Store the new usage data and task
USAGE_POOL[model_id] = {
**(USAGE_POOL[model_id] if model_id in USAGE_POOL else {}),
sid: {"updated_at": current_time},
}
# Broadcast the usage data to all clients
await sio.emit("usage", {"models": get_models_in_use()})
# Store the new usage data and task
USAGE_POOL[model_id] = {
**(USAGE_POOL[model_id] if model_id in USAGE_POOL else {}),
sid: {"updated_at": current_time},
}
@sio.event
@@ -195,10 +265,6 @@ async def connect(sid, environ, auth):
else:
USER_POOL[user.id] = [sid]
# print(f"user {user.name}({user.id}) connected with session ID {sid}")
await sio.emit("user-list", {"user_ids": list(USER_POOL.keys())})
await sio.emit("usage", {"models": get_models_in_use()})
@sio.on("user-join")
async def user_join(sid, data):
@@ -226,10 +292,6 @@ async def user_join(sid, data):
log.debug(f"{channels=}")
for channel in channels:
await sio.enter_room(sid, f"channel:{channel.id}")
# print(f"user {user.name}({user.id}) connected with session ID {sid}")
await sio.emit("user-list", {"user_ids": list(USER_POOL.keys())})
return {"id": user.id, "name": user.name}
@@ -254,6 +316,37 @@ async def join_channel(sid, data):
await sio.enter_room(sid, f"channel:{channel.id}")
@sio.on("join-note")
async def join_note(sid, data):
auth = data["auth"] if "auth" in data else None
if not auth or "token" not in auth:
return
token_data = decode_token(auth["token"])
if token_data is None or "id" not in token_data:
return
user = Users.get_user_by_id(token_data["id"])
if not user:
return
note = Notes.get_note_by_id(data["note_id"])
if not note:
log.error(f"Note {data['note_id']} not found for user {user.id}")
return
if (
user.role != "admin"
and user.id != note.user_id
and not has_access(user.id, type="read", access_control=note.access_control)
):
log.error(f"User {user.id} does not have access to note {data['note_id']}")
return
log.debug(f"Joining note {note.id} for user {user.id}")
await sio.enter_room(sid, f"note:{note.id}")
@sio.on("channel-events")
async def channel_events(sid, data):
room = f"channel:{data['channel_id']}"
@@ -282,9 +375,240 @@ async def channel_events(sid, data):
)
@sio.on("user-list")
async def user_list(sid):
await sio.emit("user-list", {"user_ids": list(USER_POOL.keys())})
@sio.on("ydoc:document:join")
async def ydoc_document_join(sid, data):
"""Handle user joining a document"""
user = SESSION_POOL.get(sid)
try:
document_id = data["document_id"]
if document_id.startswith("note:"):
note_id = document_id.split(":")[1]
note = Notes.get_note_by_id(note_id)
if not note:
log.error(f"Note {note_id} not found")
return
if (
user.get("role") != "admin"
and user.get("id") != note.user_id
and not has_access(
user.get("id"), type="read", access_control=note.access_control
)
):
log.error(
f"User {user.get('id')} does not have access to note {note_id}"
)
return
user_id = data.get("user_id", sid)
user_name = data.get("user_name", "Anonymous")
user_color = data.get("user_color", "#000000")
log.info(f"User {user_id} joining document {document_id}")
await YDOC_MANAGER.add_user(document_id=document_id, user_id=sid)
# Join Socket.IO room
await sio.enter_room(sid, f"doc_{document_id}")
active_session_ids = get_session_ids_from_room(f"doc_{document_id}")
# Get the Yjs document state
ydoc = Y.Doc()
updates = await YDOC_MANAGER.get_updates(document_id)
for update in updates:
ydoc.apply_update(bytes(update))
# Encode the entire document state as an update
state_update = ydoc.get_update()
await sio.emit(
"ydoc:document:state",
{
"document_id": document_id,
"state": list(state_update), # Convert bytes to list for JSON
"sessions": active_session_ids,
},
room=sid,
)
# Notify other users about the new user
await sio.emit(
"ydoc:user:joined",
{
"document_id": document_id,
"user_id": user_id,
"user_name": user_name,
"user_color": user_color,
},
room=f"doc_{document_id}",
skip_sid=sid,
)
log.info(f"User {user_id} successfully joined document {document_id}")
except Exception as e:
log.error(f"Error in yjs_document_join: {e}")
await sio.emit("error", {"message": "Failed to join document"}, room=sid)
async def document_save_handler(document_id, data, user):
if document_id.startswith("note:"):
note_id = document_id.split(":")[1]
note = Notes.get_note_by_id(note_id)
if not note:
log.error(f"Note {note_id} not found")
return
if (
user.get("role") != "admin"
and user.get("id") != note.user_id
and not has_access(
user.get("id"), type="read", access_control=note.access_control
)
):
log.error(f"User {user.get('id')} does not have access to note {note_id}")
return
Notes.update_note_by_id(note_id, NoteUpdateForm(data=data))
@sio.on("ydoc:document:state")
async def yjs_document_state(sid, data):
"""Send the current state of the Yjs document to the user"""
try:
document_id = data["document_id"]
room = f"doc_{document_id}"
active_session_ids = get_session_ids_from_room(room)
if sid not in active_session_ids:
log.warning(f"Session {sid} not in room {room}. Cannot send state.")
return
if not await YDOC_MANAGER.document_exists(document_id):
log.warning(f"Document {document_id} not found")
return
# Get the Yjs document state
ydoc = Y.Doc()
updates = await YDOC_MANAGER.get_updates(document_id)
for update in updates:
ydoc.apply_update(bytes(update))
# Encode the entire document state as an update
state_update = ydoc.get_update()
await sio.emit(
"ydoc:document:state",
{
"document_id": document_id,
"state": list(state_update), # Convert bytes to list for JSON
"sessions": active_session_ids,
},
room=sid,
)
except Exception as e:
log.error(f"Error in yjs_document_state: {e}")
@sio.on("ydoc:document:update")
async def yjs_document_update(sid, data):
"""Handle Yjs document updates"""
try:
document_id = data["document_id"]
try:
await stop_item_tasks(REDIS, document_id)
except:
pass
user_id = data.get("user_id", sid)
update = data["update"] # List of bytes from frontend
await YDOC_MANAGER.append_to_updates(
document_id=document_id,
update=update, # Convert list of bytes to bytes
)
# Broadcast update to all other users in the document
await sio.emit(
"ydoc:document:update",
{
"document_id": document_id,
"user_id": user_id,
"update": update,
"socket_id": sid, # Add socket_id to match frontend filtering
},
room=f"doc_{document_id}",
skip_sid=sid,
)
async def debounced_save():
await asyncio.sleep(0.5)
await document_save_handler(
document_id, data.get("data", {}), SESSION_POOL.get(sid)
)
if data.get("data"):
await create_task(REDIS, debounced_save(), document_id)
except Exception as e:
log.error(f"Error in yjs_document_update: {e}")
@sio.on("ydoc:document:leave")
async def yjs_document_leave(sid, data):
"""Handle user leaving a document"""
try:
document_id = data["document_id"]
user_id = data.get("user_id", sid)
log.info(f"User {user_id} leaving document {document_id}")
# Remove user from the document
await YDOC_MANAGER.remove_user(document_id=document_id, user_id=sid)
# Leave Socket.IO room
await sio.leave_room(sid, f"doc_{document_id}")
# Notify other users
await sio.emit(
"ydoc:user:left",
{"document_id": document_id, "user_id": user_id},
room=f"doc_{document_id}",
)
if (
YDOC_MANAGER.document_exists(document_id)
and len(await YDOC_MANAGER.get_users(document_id)) == 0
):
log.info(f"Cleaning up document {document_id} as no users are left")
await YDOC_MANAGER.clear_document(document_id)
except Exception as e:
log.error(f"Error in yjs_document_leave: {e}")
@sio.on("ydoc:awareness:update")
async def yjs_awareness_update(sid, data):
"""Handle awareness updates (cursors, selections, etc.)"""
try:
document_id = data["document_id"]
user_id = data.get("user_id", sid)
update = data["update"]
# Broadcast awareness update to all other users in the document
await sio.emit(
"ydoc:awareness:update",
{"document_id": document_id, "user_id": user_id, "update": update},
room=f"doc_{document_id}",
skip_sid=sid,
)
except Exception as e:
log.error(f"Error in yjs_awareness_update: {e}")
@sio.event
@@ -299,7 +623,7 @@ async def disconnect(sid):
if len(USER_POOL[user_id]) == 0:
del USER_POOL[user_id]
await sio.emit("user-list", {"user_ids": list(USER_POOL.keys())})
await YDOC_MANAGER.remove_user_from_all_documents(sid)
else:
pass
# print(f"Unknown session ID {sid} disconnected")
@@ -320,8 +644,8 @@ def get_event_emitter(request_info, update_db=True):
)
)
for session_id in session_ids:
await sio.emit(
emit_tasks = [
sio.emit(
"chat-events",
{
"chat_id": request_info.get("chat_id", None),
@@ -330,6 +654,10 @@ def get_event_emitter(request_info, update_db=True):
},
to=session_id,
)
for session_id in session_ids
]
await asyncio.gather(*emit_tasks)
if update_db:
if "type" in event_data and event_data["type"] == "status":
@@ -345,16 +673,17 @@ def get_event_emitter(request_info, update_db=True):
request_info["message_id"],
)
content = message.get("content", "")
content += event_data.get("data", {}).get("content", "")
if message:
content = message.get("content", "")
content += event_data.get("data", {}).get("content", "")
Chats.upsert_message_to_chat_by_id_and_message_id(
request_info["chat_id"],
request_info["message_id"],
{
"content": content,
},
)
Chats.upsert_message_to_chat_by_id_and_message_id(
request_info["chat_id"],
request_info["message_id"],
{
"content": content,
},
)
if "type" in event_data and event_data["type"] == "replace":
content = event_data.get("data", {}).get("content", "")
@@ -387,30 +716,3 @@ def get_event_call(request_info):
get_event_caller = get_event_call
def get_user_id_from_session_pool(sid):
user = SESSION_POOL.get(sid)
if user:
return user["id"]
return None
def get_user_ids_from_room(room):
active_session_ids = sio.manager.get_participants(
namespace="/",
room=room,
)
active_user_ids = list(
set(
[SESSION_POOL.get(session_id[0])["id"] for session_id in active_session_ids]
)
)
return active_user_ids
def get_active_status_by_user_id(user_id):
if user_id in USER_POOL:
return True
return False
+108
View File
@@ -1,6 +1,8 @@
import json
import uuid
from open_webui.utils.redis import get_redis_connection
from typing import Optional, List, Tuple
import pycrdt as Y
class RedisLock:
@@ -89,3 +91,109 @@ class RedisDict:
if key not in self:
self[key] = default
return self[key]
class YdocManager:
def __init__(
self,
redis=None,
redis_key_prefix: str = "open-webui:ydoc:documents",
):
self._updates = {}
self._users = {}
self._redis = redis
self._redis_key_prefix = redis_key_prefix
async def append_to_updates(self, document_id: str, update: bytes):
document_id = document_id.replace(":", "_")
if self._redis:
redis_key = f"{self._redis_key_prefix}:{document_id}:updates"
await self._redis.rpush(redis_key, json.dumps(list(update)))
else:
if document_id not in self._updates:
self._updates[document_id] = []
self._updates[document_id].append(update)
async def get_updates(self, document_id: str) -> List[bytes]:
document_id = document_id.replace(":", "_")
if self._redis:
redis_key = f"{self._redis_key_prefix}:{document_id}:updates"
updates = await self._redis.lrange(redis_key, 0, -1)
return [bytes(json.loads(update)) for update in updates]
else:
return self._updates.get(document_id, [])
async def document_exists(self, document_id: str) -> bool:
document_id = document_id.replace(":", "_")
if self._redis:
redis_key = f"{self._redis_key_prefix}:{document_id}:updates"
return await self._redis.exists(redis_key) > 0
else:
return document_id in self._updates
async def get_users(self, document_id: str) -> List[str]:
document_id = document_id.replace(":", "_")
if self._redis:
redis_key = f"{self._redis_key_prefix}:{document_id}:users"
users = await self._redis.smembers(redis_key)
return list(users)
else:
return self._users.get(document_id, [])
async def add_user(self, document_id: str, user_id: str):
document_id = document_id.replace(":", "_")
if self._redis:
redis_key = f"{self._redis_key_prefix}:{document_id}:users"
await self._redis.sadd(redis_key, user_id)
else:
if document_id not in self._users:
self._users[document_id] = set()
self._users[document_id].add(user_id)
async def remove_user(self, document_id: str, user_id: str):
document_id = document_id.replace(":", "_")
if self._redis:
redis_key = f"{self._redis_key_prefix}:{document_id}:users"
await self._redis.srem(redis_key, user_id)
else:
if document_id in self._users and user_id in self._users[document_id]:
self._users[document_id].remove(user_id)
async def remove_user_from_all_documents(self, user_id: str):
if self._redis:
keys = await self._redis.keys(f"{self._redis_key_prefix}:*")
for key in keys:
if key.endswith(":users"):
await self._redis.srem(key, user_id)
document_id = key.split(":")[-2]
if len(await self.get_users(document_id)) == 0:
await self.clear_document(document_id)
else:
for document_id in list(self._users.keys()):
if user_id in self._users[document_id]:
self._users[document_id].remove(user_id)
if not self._users[document_id]:
del self._users[document_id]
await self.clear_document(document_id)
async def clear_document(self, document_id: str):
document_id = document_id.replace(":", "_")
if self._redis:
redis_key = f"{self._redis_key_prefix}:{document_id}:updates"
await self._redis.delete(redis_key)
redis_users_key = f"{self._redis_key_prefix}:{document_id}:users"
await self._redis.delete(redis_users_key)
else:
if document_id in self._updates:
del self._updates[document_id]
if document_id in self._users:
del self._users[document_id]
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -269,11 +269,6 @@ tbody + tbody {
margin-bottom: 0;
}
/* Add a rule to reset margin-bottom for <p> not followed by <ul> */
.markdown-section p + ul {
margin-top: 0;
}
/* List item styles */
.markdown-section li {
padding: 2px;
+43 -11
View File
@@ -2,8 +2,9 @@ import os
import shutil
import json
import logging
import re
from abc import ABC, abstractmethod
from typing import BinaryIO, Tuple
from typing import BinaryIO, Tuple, Dict
import boto3
from botocore.config import Config
@@ -17,6 +18,7 @@ from open_webui.config import (
S3_SECRET_ACCESS_KEY,
S3_USE_ACCELERATE_ENDPOINT,
S3_ADDRESSING_STYLE,
S3_ENABLE_TAGGING,
GCS_BUCKET_NAME,
GOOGLE_APPLICATION_CREDENTIALS_JSON,
AZURE_STORAGE_ENDPOINT,
@@ -44,7 +46,9 @@ class StorageProvider(ABC):
pass
@abstractmethod
def upload_file(self, file: BinaryIO, filename: str) -> Tuple[bytes, str]:
def upload_file(
self, file: BinaryIO, filename: str, tags: Dict[str, str]
) -> Tuple[bytes, str]:
pass
@abstractmethod
@@ -58,7 +62,9 @@ class StorageProvider(ABC):
class LocalStorageProvider(StorageProvider):
@staticmethod
def upload_file(file: BinaryIO, filename: str) -> Tuple[bytes, str]:
def upload_file(
file: BinaryIO, filename: str, tags: Dict[str, str]
) -> Tuple[bytes, str]:
contents = file.read()
if not contents:
raise ValueError(ERROR_MESSAGES.EMPTY_CONTENT)
@@ -131,15 +137,37 @@ class S3StorageProvider(StorageProvider):
self.bucket_name = S3_BUCKET_NAME
self.key_prefix = S3_KEY_PREFIX if S3_KEY_PREFIX else ""
def upload_file(self, file: BinaryIO, filename: str) -> Tuple[bytes, str]:
@staticmethod
def sanitize_tag_value(s: str) -> str:
"""Only include S3 allowed characters."""
return re.sub(r"[^a-zA-Z0-9 äöüÄÖÜß\+\-=\._:/@]", "", s)
def upload_file(
self, file: BinaryIO, filename: str, tags: Dict[str, str]
) -> Tuple[bytes, str]:
"""Handles uploading of the file to S3 storage."""
_, file_path = LocalStorageProvider.upload_file(file, filename)
_, file_path = LocalStorageProvider.upload_file(file, filename, tags)
s3_key = os.path.join(self.key_prefix, filename)
try:
s3_key = os.path.join(self.key_prefix, filename)
self.s3_client.upload_file(file_path, self.bucket_name, s3_key)
if S3_ENABLE_TAGGING and tags:
sanitized_tags = {
self.sanitize_tag_value(k): self.sanitize_tag_value(v)
for k, v in tags.items()
}
tagging = {
"TagSet": [
{"Key": k, "Value": v} for k, v in sanitized_tags.items()
]
}
self.s3_client.put_object_tagging(
Bucket=self.bucket_name,
Key=s3_key,
Tagging=tagging,
)
return (
open(file_path, "rb").read(),
"s3://" + self.bucket_name + "/" + s3_key,
f"s3://{self.bucket_name}/{s3_key}",
)
except ClientError as e:
raise RuntimeError(f"Error uploading file to S3: {e}")
@@ -207,9 +235,11 @@ class GCSStorageProvider(StorageProvider):
self.gcs_client = storage.Client()
self.bucket = self.gcs_client.bucket(GCS_BUCKET_NAME)
def upload_file(self, file: BinaryIO, filename: str) -> Tuple[bytes, str]:
def upload_file(
self, file: BinaryIO, filename: str, tags: Dict[str, str]
) -> Tuple[bytes, str]:
"""Handles uploading of the file to GCS storage."""
contents, file_path = LocalStorageProvider.upload_file(file, filename)
contents, file_path = LocalStorageProvider.upload_file(file, filename, tags)
try:
blob = self.bucket.blob(filename)
blob.upload_from_filename(file_path)
@@ -277,9 +307,11 @@ class AzureStorageProvider(StorageProvider):
self.container_name
)
def upload_file(self, file: BinaryIO, filename: str) -> Tuple[bytes, str]:
def upload_file(
self, file: BinaryIO, filename: str, tags: Dict[str, str]
) -> Tuple[bytes, str]:
"""Handles uploading of the file to Azure Blob Storage."""
contents, file_path = LocalStorageProvider.upload_file(file, filename)
contents, file_path = LocalStorageProvider.upload_file(file, filename, tags)
try:
blob_client = self.container_client.get_blob_client(filename)
blob_client.upload_blob(contents, overwrite=True)
+137 -15
View File
@@ -2,19 +2,99 @@
import asyncio
from typing import Dict
from uuid import uuid4
import json
import logging
from redis.asyncio import Redis
from fastapi import Request
from typing import Dict, List, Optional
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MAIN"])
# A dictionary to keep track of active tasks
tasks: Dict[str, asyncio.Task] = {}
item_tasks = {}
def cleanup_task(task_id: str):
REDIS_TASKS_KEY = "open-webui:tasks"
REDIS_ITEM_TASKS_KEY = "open-webui:tasks:item"
REDIS_PUBSUB_CHANNEL = "open-webui:tasks:commands"
async def redis_task_command_listener(app):
redis: Redis = app.state.redis
pubsub = redis.pubsub()
await pubsub.subscribe(REDIS_PUBSUB_CHANNEL)
async for message in pubsub.listen():
if message["type"] != "message":
continue
try:
command = json.loads(message["data"])
if command.get("action") == "stop":
task_id = command.get("task_id")
local_task = tasks.get(task_id)
if local_task:
local_task.cancel()
except Exception as e:
log.exception(f"Error handling distributed task command: {e}")
### ------------------------------
### REDIS-ENABLED HANDLERS
### ------------------------------
async def redis_save_task(redis: Redis, task_id: str, item_id: Optional[str]):
pipe = redis.pipeline()
pipe.hset(REDIS_TASKS_KEY, task_id, item_id or "")
if item_id:
pipe.sadd(f"{REDIS_ITEM_TASKS_KEY}:{item_id}", task_id)
await pipe.execute()
async def redis_cleanup_task(redis: Redis, task_id: str, item_id: Optional[str]):
pipe = redis.pipeline()
pipe.hdel(REDIS_TASKS_KEY, task_id)
if item_id:
pipe.srem(f"{REDIS_ITEM_TASKS_KEY}:{item_id}", task_id)
if (await pipe.scard(f"{REDIS_ITEM_TASKS_KEY}:{item_id}").execute())[-1] == 0:
pipe.delete(f"{REDIS_ITEM_TASKS_KEY}:{item_id}") # Remove if empty set
await pipe.execute()
async def redis_list_tasks(redis: Redis) -> List[str]:
return list(await redis.hkeys(REDIS_TASKS_KEY))
async def redis_list_item_tasks(redis: Redis, item_id: str) -> List[str]:
return list(await redis.smembers(f"{REDIS_ITEM_TASKS_KEY}:{item_id}"))
async def redis_send_command(redis: Redis, command: dict):
await redis.publish(REDIS_PUBSUB_CHANNEL, json.dumps(command))
async def cleanup_task(redis, task_id: str, id=None):
"""
Remove a completed or canceled task from the global `tasks` dictionary.
"""
if redis:
await redis_cleanup_task(redis, task_id, id)
tasks.pop(task_id, None) # Remove the task if it exists
# If an ID is provided, remove the task from the item_tasks dictionary
if id and task_id in item_tasks.get(id, []):
item_tasks[id].remove(task_id)
if not item_tasks[id]: # If no tasks left for this ID, remove the entry
item_tasks.pop(id, None)
def create_task(coroutine):
async def create_task(redis, coroutine, id=None):
"""
Create a new asyncio task and add it to the global task dictionary.
"""
@@ -22,31 +102,58 @@ def create_task(coroutine):
task = asyncio.create_task(coroutine) # Create the task
# Add a done callback for cleanup
task.add_done_callback(lambda t: cleanup_task(task_id))
task.add_done_callback(
lambda t: asyncio.create_task(cleanup_task(redis, task_id, id))
)
tasks[task_id] = task
# If an ID is provided, associate the task with that ID
if item_tasks.get(id):
item_tasks[id].append(task_id)
else:
item_tasks[id] = [task_id]
if redis:
await redis_save_task(redis, task_id, id)
return task_id, task
def get_task(task_id: str):
"""
Retrieve a task by its task ID.
"""
return tasks.get(task_id)
def list_tasks():
async def list_tasks(redis):
"""
List all currently active task IDs.
"""
if redis:
return await redis_list_tasks(redis)
return list(tasks.keys())
async def stop_task(task_id: str):
async def list_task_ids_by_item_id(redis, id):
"""
List all tasks associated with a specific ID.
"""
if redis:
return await redis_list_item_tasks(redis, id)
return item_tasks.get(id, [])
async def stop_task(redis, task_id: str):
"""
Cancel a running task and remove it from the global task list.
"""
task = tasks.get(task_id)
if redis:
# PUBSUB: All instances check if they have this task, and stop if so.
await redis_send_command(
redis,
{
"action": "stop",
"task_id": task_id,
},
)
# Optionally check if task_id still in Redis a few moments later for feedback?
return {"status": True, "message": f"Stop signal sent for {task_id}"}
task = tasks.pop(task_id)
if not task:
raise ValueError(f"Task with ID {task_id} not found.")
@@ -55,7 +162,22 @@ async def stop_task(task_id: str):
await task # Wait for the task to handle the cancellation
except asyncio.CancelledError:
# Task successfully canceled
tasks.pop(task_id, None) # Remove it from the dictionary
return {"status": True, "message": f"Task {task_id} successfully stopped."}
return {"status": False, "message": f"Failed to stop task {task_id}."}
async def stop_item_tasks(redis: Redis, item_id: str):
"""
Stop all tasks associated with a specific item ID.
"""
task_ids = await list_task_ids_by_item_id(redis, item_id)
if not task_ids:
return {"status": True, "message": f"No tasks found for item {item_id}."}
for task_id in task_ids:
result = await stop_task(redis, task_id)
if not result["status"]:
return result # Return the first failure
return {"status": True, "message": f"All tasks for item {item_id} stopped."}
+793
View File
@@ -0,0 +1,793 @@
import pytest
from unittest.mock import Mock, patch, AsyncMock
import redis
from open_webui.utils.redis import (
SentinelRedisProxy,
parse_redis_service_url,
get_redis_connection,
get_sentinels_from_env,
MAX_RETRY_COUNT,
)
import inspect
class TestSentinelRedisProxy:
"""Test Redis Sentinel failover functionality"""
def test_parse_redis_service_url_valid(self):
"""Test parsing valid Redis service URL"""
url = "redis://user:pass@mymaster:6379/0"
result = parse_redis_service_url(url)
assert result["username"] == "user"
assert result["password"] == "pass"
assert result["service"] == "mymaster"
assert result["port"] == 6379
assert result["db"] == 0
def test_parse_redis_service_url_defaults(self):
"""Test parsing Redis service URL with defaults"""
url = "redis://mymaster"
result = parse_redis_service_url(url)
assert result["username"] is None
assert result["password"] is None
assert result["service"] == "mymaster"
assert result["port"] == 6379
assert result["db"] == 0
def test_parse_redis_service_url_invalid_scheme(self):
"""Test parsing invalid URL scheme"""
with pytest.raises(ValueError, match="Invalid Redis URL scheme"):
parse_redis_service_url("http://invalid")
def test_get_sentinels_from_env(self):
"""Test parsing sentinel hosts from environment"""
hosts = "sentinel1,sentinel2,sentinel3"
port = "26379"
result = get_sentinels_from_env(hosts, port)
expected = [("sentinel1", 26379), ("sentinel2", 26379), ("sentinel3", 26379)]
assert result == expected
def test_get_sentinels_from_env_empty(self):
"""Test empty sentinel hosts"""
result = get_sentinels_from_env(None, "26379")
assert result == []
@patch("redis.sentinel.Sentinel")
def test_sentinel_redis_proxy_sync_success(self, mock_sentinel_class):
"""Test successful sync operation with SentinelRedisProxy"""
mock_sentinel = Mock()
mock_master = Mock()
mock_master.get.return_value = "test_value"
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
# Test attribute access
get_method = proxy.__getattr__("get")
result = get_method("test_key")
assert result == "test_value"
mock_sentinel.master_for.assert_called_with("mymaster")
mock_master.get.assert_called_with("test_key")
@patch("redis.sentinel.Sentinel")
@pytest.mark.asyncio
async def test_sentinel_redis_proxy_async_success(self, mock_sentinel_class):
"""Test successful async operation with SentinelRedisProxy"""
mock_sentinel = Mock()
mock_master = Mock()
mock_master.get = AsyncMock(return_value="test_value")
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
# Test async attribute access
get_method = proxy.__getattr__("get")
result = await get_method("test_key")
assert result == "test_value"
mock_sentinel.master_for.assert_called_with("mymaster")
mock_master.get.assert_called_with("test_key")
@patch("redis.sentinel.Sentinel")
def test_sentinel_redis_proxy_failover_retry(self, mock_sentinel_class):
"""Test retry mechanism during failover"""
mock_sentinel = Mock()
mock_master = Mock()
# First call fails, second succeeds
mock_master.get.side_effect = [
redis.exceptions.ConnectionError("Master down"),
"test_value",
]
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
get_method = proxy.__getattr__("get")
result = get_method("test_key")
assert result == "test_value"
assert mock_master.get.call_count == 2
@patch("redis.sentinel.Sentinel")
def test_sentinel_redis_proxy_max_retries_exceeded(self, mock_sentinel_class):
"""Test failure after max retries exceeded"""
mock_sentinel = Mock()
mock_master = Mock()
# All calls fail
mock_master.get.side_effect = redis.exceptions.ConnectionError("Master down")
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
get_method = proxy.__getattr__("get")
with pytest.raises(redis.exceptions.ConnectionError):
get_method("test_key")
assert mock_master.get.call_count == MAX_RETRY_COUNT
@patch("redis.sentinel.Sentinel")
def test_sentinel_redis_proxy_readonly_error_retry(self, mock_sentinel_class):
"""Test retry on ReadOnlyError"""
mock_sentinel = Mock()
mock_master = Mock()
# First call gets ReadOnlyError (old master), second succeeds (new master)
mock_master.get.side_effect = [
redis.exceptions.ReadOnlyError("Read only"),
"test_value",
]
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
get_method = proxy.__getattr__("get")
result = get_method("test_key")
assert result == "test_value"
assert mock_master.get.call_count == 2
@patch("redis.sentinel.Sentinel")
def test_sentinel_redis_proxy_factory_methods(self, mock_sentinel_class):
"""Test factory methods are passed through directly"""
mock_sentinel = Mock()
mock_master = Mock()
mock_pipeline = Mock()
mock_master.pipeline.return_value = mock_pipeline
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
# Factory methods should be passed through without wrapping
pipeline_method = proxy.__getattr__("pipeline")
result = pipeline_method()
assert result == mock_pipeline
mock_master.pipeline.assert_called_once()
@patch("redis.sentinel.Sentinel")
@patch("redis.from_url")
def test_get_redis_connection_with_sentinel(
self, mock_from_url, mock_sentinel_class
):
"""Test getting Redis connection with Sentinel"""
mock_sentinel = Mock()
mock_sentinel_class.return_value = mock_sentinel
sentinels = [("sentinel1", 26379), ("sentinel2", 26379)]
redis_url = "redis://user:pass@mymaster:6379/0"
result = get_redis_connection(
redis_url=redis_url, redis_sentinels=sentinels, async_mode=False
)
assert isinstance(result, SentinelRedisProxy)
mock_sentinel_class.assert_called_once()
mock_from_url.assert_not_called()
@patch("redis.Redis.from_url")
def test_get_redis_connection_without_sentinel(self, mock_from_url):
"""Test getting Redis connection without Sentinel"""
mock_redis = Mock()
mock_from_url.return_value = mock_redis
redis_url = "redis://localhost:6379/0"
result = get_redis_connection(
redis_url=redis_url, redis_sentinels=None, async_mode=False
)
assert result == mock_redis
mock_from_url.assert_called_once_with(redis_url, decode_responses=True)
@patch("redis.asyncio.from_url")
def test_get_redis_connection_without_sentinel_async(self, mock_from_url):
"""Test getting async Redis connection without Sentinel"""
mock_redis = Mock()
mock_from_url.return_value = mock_redis
redis_url = "redis://localhost:6379/0"
result = get_redis_connection(
redis_url=redis_url, redis_sentinels=None, async_mode=True
)
assert result == mock_redis
mock_from_url.assert_called_once_with(redis_url, decode_responses=True)
class TestSentinelRedisProxyCommands:
"""Test Redis commands through SentinelRedisProxy"""
@patch("redis.sentinel.Sentinel")
def test_hash_commands_sync(self, mock_sentinel_class):
"""Test Redis hash commands in sync mode"""
mock_sentinel = Mock()
mock_master = Mock()
# Mock hash command responses
mock_master.hset.return_value = 1
mock_master.hget.return_value = "test_value"
mock_master.hgetall.return_value = {"key1": "value1", "key2": "value2"}
mock_master.hdel.return_value = 1
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
# Test hset
hset_method = proxy.__getattr__("hset")
result = hset_method("test_hash", "field1", "value1")
assert result == 1
mock_master.hset.assert_called_with("test_hash", "field1", "value1")
# Test hget
hget_method = proxy.__getattr__("hget")
result = hget_method("test_hash", "field1")
assert result == "test_value"
mock_master.hget.assert_called_with("test_hash", "field1")
# Test hgetall
hgetall_method = proxy.__getattr__("hgetall")
result = hgetall_method("test_hash")
assert result == {"key1": "value1", "key2": "value2"}
mock_master.hgetall.assert_called_with("test_hash")
# Test hdel
hdel_method = proxy.__getattr__("hdel")
result = hdel_method("test_hash", "field1")
assert result == 1
mock_master.hdel.assert_called_with("test_hash", "field1")
@patch("redis.sentinel.Sentinel")
@pytest.mark.asyncio
async def test_hash_commands_async(self, mock_sentinel_class):
"""Test Redis hash commands in async mode"""
mock_sentinel = Mock()
mock_master = Mock()
# Mock async hash command responses
mock_master.hset = AsyncMock(return_value=1)
mock_master.hget = AsyncMock(return_value="test_value")
mock_master.hgetall = AsyncMock(
return_value={"key1": "value1", "key2": "value2"}
)
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
# Test hset
hset_method = proxy.__getattr__("hset")
result = await hset_method("test_hash", "field1", "value1")
assert result == 1
mock_master.hset.assert_called_with("test_hash", "field1", "value1")
# Test hget
hget_method = proxy.__getattr__("hget")
result = await hget_method("test_hash", "field1")
assert result == "test_value"
mock_master.hget.assert_called_with("test_hash", "field1")
# Test hgetall
hgetall_method = proxy.__getattr__("hgetall")
result = await hgetall_method("test_hash")
assert result == {"key1": "value1", "key2": "value2"}
mock_master.hgetall.assert_called_with("test_hash")
@patch("redis.sentinel.Sentinel")
def test_string_commands_sync(self, mock_sentinel_class):
"""Test Redis string commands in sync mode"""
mock_sentinel = Mock()
mock_master = Mock()
# Mock string command responses
mock_master.set.return_value = True
mock_master.get.return_value = "test_value"
mock_master.delete.return_value = 1
mock_master.exists.return_value = True
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
# Test set
set_method = proxy.__getattr__("set")
result = set_method("test_key", "test_value")
assert result is True
mock_master.set.assert_called_with("test_key", "test_value")
# Test get
get_method = proxy.__getattr__("get")
result = get_method("test_key")
assert result == "test_value"
mock_master.get.assert_called_with("test_key")
# Test delete
delete_method = proxy.__getattr__("delete")
result = delete_method("test_key")
assert result == 1
mock_master.delete.assert_called_with("test_key")
# Test exists
exists_method = proxy.__getattr__("exists")
result = exists_method("test_key")
assert result is True
mock_master.exists.assert_called_with("test_key")
@patch("redis.sentinel.Sentinel")
def test_list_commands_sync(self, mock_sentinel_class):
"""Test Redis list commands in sync mode"""
mock_sentinel = Mock()
mock_master = Mock()
# Mock list command responses
mock_master.lpush.return_value = 1
mock_master.rpop.return_value = "test_value"
mock_master.llen.return_value = 5
mock_master.lrange.return_value = ["item1", "item2", "item3"]
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
# Test lpush
lpush_method = proxy.__getattr__("lpush")
result = lpush_method("test_list", "item1")
assert result == 1
mock_master.lpush.assert_called_with("test_list", "item1")
# Test rpop
rpop_method = proxy.__getattr__("rpop")
result = rpop_method("test_list")
assert result == "test_value"
mock_master.rpop.assert_called_with("test_list")
# Test llen
llen_method = proxy.__getattr__("llen")
result = llen_method("test_list")
assert result == 5
mock_master.llen.assert_called_with("test_list")
# Test lrange
lrange_method = proxy.__getattr__("lrange")
result = lrange_method("test_list", 0, -1)
assert result == ["item1", "item2", "item3"]
mock_master.lrange.assert_called_with("test_list", 0, -1)
@patch("redis.sentinel.Sentinel")
def test_pubsub_commands_sync(self, mock_sentinel_class):
"""Test Redis pubsub commands in sync mode"""
mock_sentinel = Mock()
mock_master = Mock()
mock_pubsub = Mock()
# Mock pubsub responses
mock_master.pubsub.return_value = mock_pubsub
mock_master.publish.return_value = 1
mock_pubsub.subscribe.return_value = None
mock_pubsub.get_message.return_value = {"type": "message", "data": "test_data"}
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
# Test pubsub (factory method - should pass through)
pubsub_method = proxy.__getattr__("pubsub")
result = pubsub_method()
assert result == mock_pubsub
mock_master.pubsub.assert_called_once()
# Test publish
publish_method = proxy.__getattr__("publish")
result = publish_method("test_channel", "test_message")
assert result == 1
mock_master.publish.assert_called_with("test_channel", "test_message")
@patch("redis.sentinel.Sentinel")
def test_pipeline_commands_sync(self, mock_sentinel_class):
"""Test Redis pipeline commands in sync mode"""
mock_sentinel = Mock()
mock_master = Mock()
mock_pipeline = Mock()
# Mock pipeline responses
mock_master.pipeline.return_value = mock_pipeline
mock_pipeline.set.return_value = mock_pipeline
mock_pipeline.get.return_value = mock_pipeline
mock_pipeline.execute.return_value = [True, "test_value"]
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
# Test pipeline (factory method - should pass through)
pipeline_method = proxy.__getattr__("pipeline")
result = pipeline_method()
assert result == mock_pipeline
mock_master.pipeline.assert_called_once()
@patch("redis.sentinel.Sentinel")
def test_commands_with_failover_retry(self, mock_sentinel_class):
"""Test Redis commands with failover retry mechanism"""
mock_sentinel = Mock()
mock_master = Mock()
# First call fails with connection error, second succeeds
mock_master.hget.side_effect = [
redis.exceptions.ConnectionError("Connection failed"),
"recovered_value",
]
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
# Test hget with retry
hget_method = proxy.__getattr__("hget")
result = hget_method("test_hash", "field1")
assert result == "recovered_value"
assert mock_master.hget.call_count == 2
# Verify both calls were made with same parameters
expected_calls = [(("test_hash", "field1"),), (("test_hash", "field1"),)]
actual_calls = [call.args for call in mock_master.hget.call_args_list]
assert actual_calls == expected_calls
@patch("redis.sentinel.Sentinel")
def test_commands_with_readonly_error_retry(self, mock_sentinel_class):
"""Test Redis commands with ReadOnlyError retry mechanism"""
mock_sentinel = Mock()
mock_master = Mock()
# First call fails with ReadOnlyError, second succeeds
mock_master.hset.side_effect = [
redis.exceptions.ReadOnlyError(
"READONLY You can't write against a read only replica"
),
1,
]
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
# Test hset with retry
hset_method = proxy.__getattr__("hset")
result = hset_method("test_hash", "field1", "value1")
assert result == 1
assert mock_master.hset.call_count == 2
# Verify both calls were made with same parameters
expected_calls = [
(("test_hash", "field1", "value1"),),
(("test_hash", "field1", "value1"),),
]
actual_calls = [call.args for call in mock_master.hset.call_args_list]
assert actual_calls == expected_calls
@patch("redis.sentinel.Sentinel")
@pytest.mark.asyncio
async def test_async_commands_with_failover_retry(self, mock_sentinel_class):
"""Test async Redis commands with failover retry mechanism"""
mock_sentinel = Mock()
mock_master = Mock()
# First call fails with connection error, second succeeds
mock_master.hget = AsyncMock(
side_effect=[
redis.exceptions.ConnectionError("Connection failed"),
"recovered_value",
]
)
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
# Test async hget with retry
hget_method = proxy.__getattr__("hget")
result = await hget_method("test_hash", "field1")
assert result == "recovered_value"
assert mock_master.hget.call_count == 2
# Verify both calls were made with same parameters
expected_calls = [(("test_hash", "field1"),), (("test_hash", "field1"),)]
actual_calls = [call.args for call in mock_master.hget.call_args_list]
assert actual_calls == expected_calls
class TestSentinelRedisProxyFactoryMethods:
"""Test Redis factory methods in async mode - these are special cases that remain sync"""
@patch("redis.sentinel.Sentinel")
@pytest.mark.asyncio
async def test_pubsub_factory_method_async(self, mock_sentinel_class):
"""Test pubsub factory method in async mode - should pass through without wrapping"""
mock_sentinel = Mock()
mock_master = Mock()
mock_pubsub = Mock()
# Mock pubsub factory method
mock_master.pubsub.return_value = mock_pubsub
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
# Test pubsub factory method - should NOT be wrapped as async
pubsub_method = proxy.__getattr__("pubsub")
result = pubsub_method()
assert result == mock_pubsub
mock_master.pubsub.assert_called_once()
# Verify it's not wrapped as async (no await needed)
assert not inspect.iscoroutine(result)
@patch("redis.sentinel.Sentinel")
@pytest.mark.asyncio
async def test_pipeline_factory_method_async(self, mock_sentinel_class):
"""Test pipeline factory method in async mode - should pass through without wrapping"""
mock_sentinel = Mock()
mock_master = Mock()
mock_pipeline = Mock()
# Mock pipeline factory method
mock_master.pipeline.return_value = mock_pipeline
mock_pipeline.set.return_value = mock_pipeline
mock_pipeline.get.return_value = mock_pipeline
mock_pipeline.execute.return_value = [True, "test_value"]
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
# Test pipeline factory method - should NOT be wrapped as async
pipeline_method = proxy.__getattr__("pipeline")
result = pipeline_method()
assert result == mock_pipeline
mock_master.pipeline.assert_called_once()
# Verify it's not wrapped as async (no await needed)
assert not inspect.iscoroutine(result)
# Test pipeline usage (these should also be sync)
pipeline_result = result.set("key", "value").get("key").execute()
assert pipeline_result == [True, "test_value"]
@patch("redis.sentinel.Sentinel")
@pytest.mark.asyncio
async def test_factory_methods_vs_regular_commands_async(self, mock_sentinel_class):
"""Test that factory methods behave differently from regular commands in async mode"""
mock_sentinel = Mock()
mock_master = Mock()
# Mock both factory method and regular command
mock_pubsub = Mock()
mock_master.pubsub.return_value = mock_pubsub
mock_master.get = AsyncMock(return_value="test_value")
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
# Test factory method - should NOT be wrapped
pubsub_method = proxy.__getattr__("pubsub")
pubsub_result = pubsub_method()
# Test regular command - should be wrapped as async
get_method = proxy.__getattr__("get")
get_result = get_method("test_key")
# Factory method returns directly
assert pubsub_result == mock_pubsub
assert not inspect.iscoroutine(pubsub_result)
# Regular command returns coroutine
assert inspect.iscoroutine(get_result)
# Regular command needs await
actual_value = await get_result
assert actual_value == "test_value"
@patch("redis.sentinel.Sentinel")
@pytest.mark.asyncio
async def test_factory_methods_with_failover_async(self, mock_sentinel_class):
"""Test factory methods with failover in async mode"""
mock_sentinel = Mock()
mock_master = Mock()
# First call fails, second succeeds
mock_pubsub = Mock()
mock_master.pubsub.side_effect = [
redis.exceptions.ConnectionError("Connection failed"),
mock_pubsub,
]
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
# Test pubsub factory method with failover
pubsub_method = proxy.__getattr__("pubsub")
result = pubsub_method()
assert result == mock_pubsub
assert mock_master.pubsub.call_count == 2 # Retry happened
# Verify it's still not wrapped as async after retry
assert not inspect.iscoroutine(result)
@patch("redis.sentinel.Sentinel")
@pytest.mark.asyncio
async def test_monitor_factory_method_async(self, mock_sentinel_class):
"""Test monitor factory method in async mode - should pass through without wrapping"""
mock_sentinel = Mock()
mock_master = Mock()
mock_monitor = Mock()
# Mock monitor factory method
mock_master.monitor.return_value = mock_monitor
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
# Test monitor factory method - should NOT be wrapped as async
monitor_method = proxy.__getattr__("monitor")
result = monitor_method()
assert result == mock_monitor
mock_master.monitor.assert_called_once()
# Verify it's not wrapped as async (no await needed)
assert not inspect.iscoroutine(result)
@patch("redis.sentinel.Sentinel")
@pytest.mark.asyncio
async def test_client_factory_method_async(self, mock_sentinel_class):
"""Test client factory method in async mode - should pass through without wrapping"""
mock_sentinel = Mock()
mock_master = Mock()
mock_client = Mock()
# Mock client factory method
mock_master.client.return_value = mock_client
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
# Test client factory method - should NOT be wrapped as async
client_method = proxy.__getattr__("client")
result = client_method()
assert result == mock_client
mock_master.client.assert_called_once()
# Verify it's not wrapped as async (no await needed)
assert not inspect.iscoroutine(result)
@patch("redis.sentinel.Sentinel")
@pytest.mark.asyncio
async def test_transaction_factory_method_async(self, mock_sentinel_class):
"""Test transaction factory method in async mode - should pass through without wrapping"""
mock_sentinel = Mock()
mock_master = Mock()
mock_transaction = Mock()
# Mock transaction factory method
mock_master.transaction.return_value = mock_transaction
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
# Test transaction factory method - should NOT be wrapped as async
transaction_method = proxy.__getattr__("transaction")
result = transaction_method()
assert result == mock_transaction
mock_master.transaction.assert_called_once()
# Verify it's not wrapped as async (no await needed)
assert not inspect.iscoroutine(result)
@patch("redis.sentinel.Sentinel")
@pytest.mark.asyncio
async def test_all_factory_methods_async(self, mock_sentinel_class):
"""Test all factory methods in async mode - comprehensive test"""
mock_sentinel = Mock()
mock_master = Mock()
# Mock all factory methods
mock_objects = {
"pipeline": Mock(),
"pubsub": Mock(),
"monitor": Mock(),
"client": Mock(),
"transaction": Mock(),
}
for method_name, mock_obj in mock_objects.items():
setattr(mock_master, method_name, Mock(return_value=mock_obj))
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
# Test all factory methods
for method_name, expected_obj in mock_objects.items():
method = proxy.__getattr__(method_name)
result = method()
assert result == expected_obj
assert not inspect.iscoroutine(result)
getattr(mock_master, method_name).assert_called_once()
# Reset mock for next iteration
getattr(mock_master, method_name).reset_mock()
@patch("redis.sentinel.Sentinel")
@pytest.mark.asyncio
async def test_mixed_factory_and_regular_commands_async(self, mock_sentinel_class):
"""Test using both factory methods and regular commands in async mode"""
mock_sentinel = Mock()
mock_master = Mock()
# Mock pipeline factory and regular commands
mock_pipeline = Mock()
mock_master.pipeline.return_value = mock_pipeline
mock_pipeline.set.return_value = mock_pipeline
mock_pipeline.get.return_value = mock_pipeline
mock_pipeline.execute.return_value = [True, "pipeline_value"]
mock_master.get = AsyncMock(return_value="regular_value")
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
# Use factory method (sync)
pipeline = proxy.__getattr__("pipeline")()
pipeline_result = pipeline.set("key1", "value1").get("key1").execute()
# Use regular command (async)
get_method = proxy.__getattr__("get")
regular_result = await get_method("key2")
# Verify both work correctly
assert pipeline_result == [True, "pipeline_value"]
assert regular_result == "regular_value"
# Verify calls
mock_master.pipeline.assert_called_once()
mock_master.get.assert_called_with("key2")
+1 -1
View File
@@ -60,7 +60,7 @@ def get_permissions(
# Combine permissions from all user groups
for group in user_groups:
group_permissions = group.permissions
group_permissions = group.permissions or {}
permissions = combine_permissions(permissions, group_permissions)
# Ensure all fields from default_permissions are present and filled in
+44 -10
View File
@@ -37,7 +37,7 @@ if TYPE_CHECKING:
class AuditLogEntry:
# `Metadata` audit level properties
id: str
user: dict[str, Any]
user: Optional[dict[str, Any]]
audit_level: str
verb: str
request_uri: str
@@ -190,21 +190,40 @@ class AuditLoggingMiddleware:
finally:
await self._log_audit_entry(request, context)
async def _get_authenticated_user(self, request: Request) -> UserModel:
async def _get_authenticated_user(self, request: Request) -> Optional[UserModel]:
auth_header = request.headers.get("Authorization")
assert auth_header
user = get_current_user(request, None, get_http_authorization_cred(auth_header))
return user
try:
user = get_current_user(
request, None, get_http_authorization_cred(auth_header)
)
return user
except Exception as e:
logger.debug(f"Failed to get authenticated user: {str(e)}")
return None
def _should_skip_auditing(self, request: Request) -> bool:
if (
request.method not in {"POST", "PUT", "PATCH", "DELETE"}
or AUDIT_LOG_LEVEL == "NONE"
or not request.headers.get("authorization")
):
return True
ALWAYS_LOG_ENDPOINTS = {
"/api/v1/auths/signin",
"/api/v1/auths/signout",
"/api/v1/auths/signup",
}
path = request.url.path.lower()
for endpoint in ALWAYS_LOG_ENDPOINTS:
if path.startswith(endpoint):
return False # Do NOT skip logging for auth endpoints
# Skip logging if the request is not authenticated
if not request.headers.get("authorization"):
return True
# match either /api/<resource>/...(for the endpoint /api/chat case) or /api/v1/<resource>/...
pattern = re.compile(
r"^/api(?:/v1)?/(" + "|".join(self.excluded_paths) + r")\b"
@@ -231,17 +250,32 @@ class AuditLoggingMiddleware:
try:
user = await self._get_authenticated_user(request)
user = (
user.model_dump(include={"id", "name", "email", "role"}) if user else {}
)
request_body = context.request_body.decode("utf-8", errors="replace")
response_body = context.response_body.decode("utf-8", errors="replace")
# Redact sensitive information
if "password" in request_body:
request_body = re.sub(
r'"password":\s*"(.*?)"',
'"password": "********"',
request_body,
)
entry = AuditLogEntry(
id=str(uuid.uuid4()),
user=user.model_dump(include={"id", "name", "email", "role"}),
user=user,
audit_level=self.audit_level.value,
verb=request.method,
request_uri=str(request.url),
response_status_code=context.metadata.get("response_status_code", None),
source_ip=request.client.host if request.client else None,
user_agent=request.headers.get("user-agent"),
request_object=context.request_body.decode("utf-8", errors="replace"),
response_object=context.response_body.decode("utf-8", errors="replace"),
request_object=request_body,
response_object=response_body,
)
self.audit_logger.write(entry)

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