Compare commits

...
460 Commits
Author SHA1 Message Date
Tim Baek e85c7f7931 Merge pull request #18402 from open-webui/dev
0.6.35
2025-11-06 13:40:46 -05:00
Timothy Jaeryang Baek d5fe0f6067 refac 2025-11-06 13:31:55 -05:00
Timothy Jaeryang Baek 9c0bd0c0ed refac 2025-11-06 13:24:34 -05:00
Timothy Jaeryang Baek 651f855289 refac 2025-11-06 13:20:03 -05:00
Timothy Jaeryang Baek dc3d704800 refac: shortcuts 2025-11-06 13:08:48 -05:00
Davixk 8da4e5bb19 fix(chats): fix chat search crash (#18576)
* fix(chats): handle null bytes in PostgreSQL search

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

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

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

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

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG with recent feature additions

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

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

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

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

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

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

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

* Update Selector.svelte

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

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

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

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

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

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

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

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

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

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

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

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG for version 0.6.34

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

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

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

* feat: improve ollama model management experience

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

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

* fix

* i18n: improve Chinese translation

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

* German translation of new strings in i18n

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

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

* fix onedrive

* fix: discovery url

* fix: default permissions not being loaded

* fix: ai hallucination

* fix: non rich text input copy

* refac: rm print statements

* refac: disable direct models from model editors

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

* Update pull_request_template.md

* Update generated image translation in DE-de

* added missing danish translations

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

* style(onedrive): Formatting fix

* feat: Implement toggling for vertical and horizontal flow layouts

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* refac: ollama embed form data

* fix: non rich text handling

* fix: oauth client registration

* refac

* chore: dep bump

* chore: fastapi bump

* chore/refac: bump bcrypt and remove passlib

* Improving Korean Translation

* refac

* Improving Korean Translation

* feat: PWA share_target implementation

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

* refac: message input mobile detection behaviour

* feat: model_ids per folder

* Update translation.json (pt-BR)

inclusion of new translations of items that have been added

* refac

* refac

* refac

* refac

* refac/fix: temp chat

* refac

* refac: stop task

* refac/fix: azure audio escape

* refac: external tool validation

* refac/enh: start.sh additional args support

* refac

* refac: styling

* refac/fix: direct connection floating action buttons

* refac/fix: system prompt duplication

* refac/enh: openai tts additional params support

* refac

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

* i18n: improve Chinese translation

* refac

* refac: model selector

* UPD: i18n es-ES Translation v0.6.33

UPD: i18n es-ES Translation v0.6.33

Updated new strings.

* refac

* improved query pref by querying only relevant columns

* refac/enh: docling params

* refac

* refac: openai additional headers support

* refac

* FEAT: Add Vega Char Visualizer Renderer

### FEAT: Add Vega Char Visualizer Renderer

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

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

The treatment is similar as for mermaid diagrams.

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

* refac

* chore

* refac

* FEAT: Add Vega-Lite Char Visualizer Renderer

### FEAT: Add Vega Char Visualizer Renderer

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

* refac

* refac/fix: switch

* enh/refac: url input handling

* refac

* refac: styling

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

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

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

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

* removed redundant knowledge API call

* Fix Code Format

* refac: model workspace view

* refac

* refac: knowledge

* refac: prompts

* refac: tools

* refac

* feat: attach folder

* refac: make tencentcloud-sdk-python optional

* refac/fix: oauth

* enh: ENABLE_OAUTH_EMAIL_FALLBACK

* refac/fix: folders

* Update requirements.txt

* Update pyproject.toml

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

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

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

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

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

* Removed unused toast import & Code Format

* refac

* refac: external tool server view

* refac

* refac: overview

* refac: styling

* refac

* Update bug_report.yaml

* refac

* refac

* refac

* refac

* refac: oauth client fallback

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

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

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

* refac: fallback to reasoning content

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

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

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

This improves clarity and consistency in the UI.

* refac/fix: ENABLE_STAR_SESSIONS_MIDDLEWARE

* refac/fix: redirect

* refac

* refac

* refac

* refac: web search error handling

* refac: source parsing

* refac: functions

* refac

* refac/enh: note pdf export

* refac/fix: mcp oauth2.1

* chore: format

* chore: Changelog (#17995)

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* refac

* chore: dep bump

---------

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

* fix: search action i18n

---------

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

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

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

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

* fix

* i18n: improve Chinese translation

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

* German translation of new strings in i18n

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

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

* fix onedrive

* fix: discovery url

* fix: default permissions not being loaded

* fix: ai hallucination

* fix: non rich text input copy

* refac: rm print statements

* refac: disable direct models from model editors

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

* Update pull_request_template.md

* Update generated image translation in DE-de

* added missing danish translations

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

* style(onedrive): Formatting fix

* feat: Implement toggling for vertical and horizontal flow layouts

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

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

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

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

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



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

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

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



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

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

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



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

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

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



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

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

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



* refac: ollama embed form data

* fix: non rich text handling

* fix: oauth client registration

* refac

* chore: dep bump

* chore: fastapi bump

* chore/refac: bump bcrypt and remove passlib

* Improving Korean Translation

* refac

* Improving Korean Translation

* feat: PWA share_target implementation



* refac: message input mobile detection behaviour

* feat: model_ids per folder

* Update translation.json (pt-BR)

inclusion of new translations of items that have been added

* refac

* refac

* refac

* refac

* refac/fix: temp chat

* refac

* refac: stop task

* refac/fix: azure audio escape

* refac: external tool validation

* refac/enh: start.sh additional args support

* refac

* refac: styling

* refac/fix: direct connection floating action buttons

* refac/fix: system prompt duplication

* refac/enh: openai tts additional params support

* refac

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

* i18n: improve Chinese translation

* refac

* refac: model selector

* UPD: i18n es-ES Translation v0.6.33

UPD: i18n es-ES Translation v0.6.33

Updated new strings.

* refac

* improved query pref by querying only relevant columns

* refac/enh: docling params

* refac

* refac: openai additional headers support

* refac

* FEAT: Add Vega Char Visualizer Renderer

### FEAT: Add Vega Char Visualizer Renderer

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

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

The treatment is similar as for mermaid diagrams.

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

* refac

* chore

* refac

* FEAT: Add Vega-Lite Char Visualizer Renderer

### FEAT: Add Vega Char Visualizer Renderer

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

* refac

* refac/fix: switch

* enh/refac: url input handling

* refac

* refac: styling

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

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

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

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

* removed redundant knowledge API call

* Fix Code Format

* refac: model workspace view

* refac

* refac: knowledge

* refac: prompts

* refac: tools

* refac

* feat: attach folder

* refac: make tencentcloud-sdk-python optional

* refac/fix: oauth

* enh: ENABLE_OAUTH_EMAIL_FALLBACK

* refac/fix: folders

* Update requirements.txt

* Update pyproject.toml

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

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

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

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

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

* Removed unused toast import & Code Format

* refac

* refac: external tool server view

* refac

* refac: overview

* refac: styling

* refac

* Update bug_report.yaml

* refac

* refac

* refac

* refac

* refac: oauth client fallback

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

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

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

* refac: fallback to reasoning content

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

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

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

This improves clarity and consistency in the UI.

* refac/fix: ENABLE_STAR_SESSIONS_MIDDLEWARE

* refac/fix: redirect

* refac

* refac

* refac

* refac: web search error handling

* refac: source parsing

* refac: functions

* refac

* refac/enh: note pdf export

* refac/fix: mcp oauth2.1

* chore: format

* chore: Changelog (#17995)

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* refac

* chore: dep bump

---------

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

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

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

* fix

* i18n: improve Chinese translation

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

* German translation of new strings in i18n

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

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

* fix onedrive

* fix: discovery url

* fix: default permissions not being loaded

* fix: ai hallucination

* fix: non rich text input copy

* refac: rm print statements

* refac: disable direct models from model editors

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

* Update pull_request_template.md

* Update generated image translation in DE-de

* added missing danish translations

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

* style(onedrive): Formatting fix

* feat: Implement toggling for vertical and horizontal flow layouts

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* refac: ollama embed form data

* fix: non rich text handling

* fix: oauth client registration

* refac

* chore: dep bump

* chore: fastapi bump

* chore/refac: bump bcrypt and remove passlib

* Improving Korean Translation

* refac

* Improving Korean Translation

* feat: PWA share_target implementation

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

* refac: message input mobile detection behaviour

* feat: model_ids per folder

* Update translation.json (pt-BR)

inclusion of new translations of items that have been added

* refac

* refac

* refac

* refac

* refac/fix: temp chat

* refac

* refac: stop task

* refac/fix: azure audio escape

* refac: external tool validation

* refac/enh: start.sh additional args support

* refac

* refac: styling

* refac/fix: direct connection floating action buttons

* refac/fix: system prompt duplication

* refac/enh: openai tts additional params support

* refac

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

* i18n: improve Chinese translation

* refac

* refac: model selector

* UPD: i18n es-ES Translation v0.6.33

UPD: i18n es-ES Translation v0.6.33

Updated new strings.

* refac

* improved query pref by querying only relevant columns

* refac/enh: docling params

* refac

* refac: openai additional headers support

* refac

* FEAT: Add Vega Char Visualizer Renderer

### FEAT: Add Vega Char Visualizer Renderer

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

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

The treatment is similar as for mermaid diagrams.

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

* refac

* chore

* refac

* FEAT: Add Vega-Lite Char Visualizer Renderer

### FEAT: Add Vega Char Visualizer Renderer

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

* refac

* refac/fix: switch

* enh/refac: url input handling

* refac

* refac: styling

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

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

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

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

* removed redundant knowledge API call

* Fix Code Format

* refac: model workspace view

* refac

* refac: knowledge

* refac: prompts

* refac: tools

* refac

* feat: attach folder

* refac: make tencentcloud-sdk-python optional

* refac/fix: oauth

* enh: ENABLE_OAUTH_EMAIL_FALLBACK

* refac/fix: folders

* Update requirements.txt

* Update pyproject.toml

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

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

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

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

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

* Removed unused toast import & Code Format

* refac

* refac: external tool server view

* refac

* refac: overview

* refac: styling

* refac

* Update bug_report.yaml

* refac

* refac

* refac

* refac

* refac: oauth client fallback

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

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

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

* refac: fallback to reasoning content

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

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

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

This improves clarity and consistency in the UI.

* refac/fix: ENABLE_STAR_SESSIONS_MIDDLEWARE

* refac/fix: redirect

* refac

* refac

* refac

* refac: web search error handling

* refac: source parsing

* refac: functions

* refac

* refac/enh: note pdf export

* refac/fix: mcp oauth2.1

* chore: format

* chore: Changelog (#17995)

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* refac

* chore: dep bump

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: silentoplayz <jacwoo21@outlook.com>
Co-authored-by: Shirasawa <764798966@qq.com>
Co-authored-by: Jan Kessler <jakessle@uni-mainz.de>
Co-authored-by: Jacob Leksan <jacob.leksan@expedient.com>
Co-authored-by: Classic298 <27028174+Classic298@users.noreply.github.com>
Co-authored-by: sinejespersen <sinejespersen@protonmail.com>
Co-authored-by: Selene Blok <selene.blok@rws.nl>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Cyp <cypher9715@naver.com>
Co-authored-by: gjveld <19951982+gjveld@users.noreply.github.com>
Co-authored-by: joaoback <156559121+joaoback@users.noreply.github.com>
Co-authored-by: _00_ <131402327+rgaricano@users.noreply.github.com>
Co-authored-by: expruc <eygabi01@gmail.com>
Co-authored-by: YetheSamartaka <55753928+YetheSamartaka@users.noreply.github.com>
Co-authored-by: Akutangulo <akutangulo@gmail.com>
2025-10-07 16:20:27 -05:00
Timothy Jaeryang Baek c1e86ad0a6 chore: dep bump 2025-10-07 16:19:47 -05:00
Timothy Jaeryang Baek dbbdad3ebd refac 2025-10-07 16:13:20 -05:00
Classic298 73f38dce88 chore: Changelog (#17995)
* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md
2025-10-07 16:10:52 -05:00
Timothy Jaeryang Baek d11fba2dfd chore: format 2025-10-07 15:23:46 -05:00
Timothy Jaeryang Baek 911a114ad4 refac/fix: mcp oauth2.1 2025-10-07 14:56:10 -05:00
Timothy Jaeryang Baek 216fb5c3db refac/enh: note pdf export 2025-10-07 14:34:47 -05:00
Timothy Jaeryang Baek 18d74f2b10 refac 2025-10-07 14:23:44 -05:00
Timothy Jaeryang Baek f5e1a42f51 refac: functions 2025-10-07 14:20:07 -05:00
Tim Jaeryang Baek 299e0e2674 Merge pull request #18114 from Akutangulo/main
i18n: Fix typo in Spanish translation ("Eriquetas" → "Etiquetas")
2025-10-07 13:53:05 -05:00
Timothy Jaeryang Baek 3c47e49cf0 refac: source parsing 2025-10-07 12:46:18 -05:00
Timothy Jaeryang Baek 53e98620bf refac: web search error handling 2025-10-07 12:15:24 -05:00
Timothy Jaeryang Baek 7d205b1711 refac 2025-10-07 12:08:21 -05:00
Timothy Jaeryang Baek e000494e48 refac 2025-10-07 11:53:30 -05:00
Timothy Jaeryang Baek 2d71193bc0 refac 2025-10-07 10:59:53 -05:00
Tim Jaeryang Baek 696876d393 Merge pull request #18108 from YetheSamartaka/main
fix: "Cannot handle batch sizes > 1 if no padding token is defined" for some reranking models
2025-10-07 10:57:34 -05:00
Timothy Jaeryang Baek db1780678e refac/fix: redirect 2025-10-07 10:53:58 -05:00
Timothy Jaeryang Baek 861953fd2d refac/fix: ENABLE_STAR_SESSIONS_MIDDLEWARE 2025-10-07 10:52:34 -05:00
Akutangulo 7448cf6c65 fix(i18n): corrected typo in Spanish translation for "Reasoning Tags"
Typo fixed in Spanish translation file at line 1240 of `open-webui/src/lib/i18n/locales/es-ES/translation.json`:

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

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

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

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

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

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

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

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

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

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

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

The treatment is similar as for mermaid diagrams.

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

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

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

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

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

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

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

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

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

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

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

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

*   **`Flow.svelte` Refactoring:**
    *   Updates logic for calculating level offsets and node positions to consistently respect the current flow orientation.
    *   Adds a control panel using `<Controls>` and `<SwitchButton>` components.
    *   Provides user interface elements to easily switch the flow layout between horizontal and vertical orientations.
2025-09-30 16:09:55 -04:00
Tim Jaeryang Baek 39b6f4bbf5 Merge pull request #17912 from Classic298/patch-1
chore: proposed changes to pull request template
2025-09-30 10:35:51 -05:00
Tim Jaeryang Baek c850439f92 Merge pull request #17916 from Classic298/patch-2
i18n: Update generated image translation in DE-de
2025-09-30 10:35:28 -05:00
Tim Jaeryang Baek 44b71de41e Merge pull request #17923 from itk-dev/feature/danish-translations-added
added missing danish translations
2025-09-30 10:35:10 -05:00
Selene Blok 5c059e604b style(onedrive): Formatting fix 2025-09-30 16:49:53 +02:00
Selene Blok 07cc807bc9 feat(onedrive): Enable search and "My Organization" pivot 2025-09-30 16:43:38 +02:00
sinejespersen e96fb67367 added missing danish translations 2025-09-30 12:49:12 +02:00
Classic298 7259905114 Update generated image translation in DE-de 2025-09-30 10:51:39 +02:00
Classic298 0d0d28641d Update pull_request_template.md 2025-09-30 08:36:31 +02:00
Timothy Jaeryang Baek 01a5b97415 refac/fix: do not process xlsx files with azure doc intelligence 2025-09-29 23:05:24 -05:00
Timothy Jaeryang Baek 8c662c65a9 refac: disable direct models from model editors 2025-09-29 22:59:28 -05:00
Timothy Jaeryang Baek 88a6fe3c7a refac: rm print statements 2025-09-29 22:59:10 -05:00
Timothy Jaeryang Baek fcc3d9ed2b fix: non rich text input copy 2025-09-29 22:45:38 -05:00
Timothy Jaeryang Baek 58efa18f96 fix: ai hallucination 2025-09-29 21:37:38 -05:00
Timothy Jaeryang Baek 887772db22 fix: default permissions not being loaded 2025-09-29 21:36:07 -05:00
Timothy Jaeryang Baek 73f32a88e3 fix: discovery url 2025-09-29 21:30:19 -05:00
Tim Jaeryang Baek ddb109a456 Merge pull request #17888 from Ithanil/nolog_websearch
fix: log web search queries only with level 'debug' instead of 'info'
2025-09-29 21:27:27 -05:00
Tim Jaeryang Baek ccf53afad4 Merge pull request #17886 from Ithanil/de_i18n
i18n: German translation of new strings
2025-09-29 21:26:24 -05:00
Tim Jaeryang Baek 170ae9f3be Merge pull request #17882 from ShirasawaSama/patch-3
fix: handle non‑UTF8 chars in third‑party responses without error
2025-09-29 21:25:55 -05:00
Tim Jaeryang Baek 7d513ff1fe Merge pull request #17881 from ShirasawaSama/patch-20
i18n: improve Chinese (zhCN & zh-TW) translation
2025-09-29 21:25:26 -05:00
Tim Jaeryang Baek b387880194 Merge pull request #17897 from jmleksan/fix/tool-task-only-include-text
Fix: Tool task only include text
2025-09-29 21:23:24 -05:00
Tim Jaeryang Baek b15db795e1 Merge pull request #17902 from Classic298/patch-1
fix: onedrive
2025-09-29 21:22:56 -05:00
Classic298 2a3f57bc61 fix onedrive 2025-09-29 22:12:49 +02:00
Jacob Leksan 0a928d6e9d Tool calls now only include text and dont inlcude other content like image b64 2025-09-29 12:50:01 -04:00
Tim Jaeryang Baek 4d7fddaf7e Merge pull request #17892 from open-webui/dev
Dev
2025-09-29 10:07:13 -05:00
Timothy Jaeryang Baek 5438eb0057 doc: changelog 2025-09-29 10:04:21 -05:00
Tim Jaeryang Baek 88f7852928 Merge pull request #17890 from Classic298/patch-1
DEV branch changelog 0.6.32
2025-09-29 10:03:53 -05:00
Timothy Jaeryang Baek a5a098943c refac 2025-09-29 10:03:03 -05:00
Classic298 f267e479ba DEV branch changelog 0.6.32 2025-09-29 15:55:58 +02:00
Jan Kessler c9c0dd367f log web search queries only with level 'debug' instead of 'info' 2025-09-29 11:59:04 +02:00
Jan Kessler a395af31b1 German translation of new strings in i18n 2025-09-29 11:43:17 +02:00
Shirasawa 3389f0eece fix: handle non‑UTF8 chars in third‑party responses without error 2025-09-29 08:16:50 +00:00
Shirasawa 3b1197a1bb i18n: improve Chinese translation 2025-09-29 07:47:11 +00:00
silentoplayz 8df74dde80 fix 2025-08-23 15:19:35 -04:00
silentoplayz c6d80496ab feat: improve ollama model management experience
This commit introduces several improvements to the Ollama model management modal:

- Adds a cancel button to the model pulling operation, using the existing 'x' button pattern.
- Adds a cancel button to the "Update All" models operation, allowing the user to cancel the update for the currently processing model.
- Cleans up toast notifications when updating all models. A single toast is now shown at the beginning and a summary toast at the end, preventing notification spam.
- Refactors the `ManageOllama.svelte` component to support these new cancellation features.
- Adds tooltips to all buttons in the modal to improve clarity.
- Disables buttons when their corresponding input fields are empty to prevent accidental clicks.
2025-08-23 15:08:07 -04:00
241 changed files with 16867 additions and 10833 deletions
+7 -1
View File
@@ -11,7 +11,9 @@ body:
## Important Notes
- **Before submitting a bug report**: Please check the [Issues](https://github.com/open-webui/open-webui/issues) and [Discussions](https://github.com/open-webui/open-webui/discussions) sections to see if a similar issue has already been reported. If unsure, start a discussion first, as this helps us efficiently focus on improving the project. Duplicates may be closed without notice. **Please search for existing issues and discussions.**
- **Before submitting a bug report**: Please check the [Issues](https://github.com/open-webui/open-webui/issues) and [Discussions](https://github.com/open-webui/open-webui/discussions) sections to see if a similar issue has already been reported. If unsure, start a discussion first, as this helps us efficiently focus on improving the project. Duplicates may be closed without notice. **Please search for existing issues AND discussions. No matter open or closed.**
- Check for opened, **but also for (recently) CLOSED issues** as the issue you are trying to report **might already have been fixed on the dev branch!**
- **Respectful collaboration**: Open WebUI is a volunteer-driven project with a single maintainer and contributors who also have full-time jobs. Please be constructive and respectful in your communication.
@@ -19,6 +21,8 @@ body:
- **Bug Reproducibility**: If a bug cannot be reproduced using a `:main` or `:dev` Docker setup or with `pip install` on Python 3.11, community assistance may be required. In such cases, we will move it to the "[Issues](https://github.com/open-webui/open-webui/discussions/categories/issues)" Discussions section. Your help is appreciated!
- **Scope**: If you want to report a SECURITY VULNERABILITY, then do so through our [GitHub security page](https://github.com/open-webui/open-webui/security).
- type: checkboxes
id: issue-check
attributes:
@@ -29,6 +33,8 @@ body:
required: true
- label: I have searched for any existing and/or related discussions.
required: true
- label: I have also searched in the CLOSED issues AND CLOSED discussions and found no related items (your issue might already be addressed on the development branch!).
required: true
- label: I am using the latest version of Open WebUI.
required: true
+23 -5
View File
@@ -8,10 +8,21 @@ body:
value: |
## Important Notes
### Before submitting
Please check the [Issues](https://github.com/open-webui/open-webui/issues) or [Discussions](https://github.com/open-webui/open-webui/discussions) to see if a similar request has been posted.
Please check the **open AND closed** [Issues](https://github.com/open-webui/open-webui/issues) AND [Discussions](https://github.com/open-webui/open-webui/discussions) to see if a similar request has been posted.
It's likely we're already tracking it! If you’re unsure, start a discussion post first.
This will help us efficiently focus on improving the project.
#### Scope
If your feature request is likely to take more than a quick coding session to implement, test and verify, then open it in the **Ideas** section of the [Discussions](https://github.com/open-webui/open-webui/discussions) instead.
**We will close and force move your feature request to the Ideas section, if we believe your feature request is not trivial/quick to implement.**
This is to ensure the issues tab is used only for issues, quickly addressable feature requests and tracking tickets by the maintainers.
Other feature requests belong in the **Ideas** section of the [Discussions](https://github.com/open-webui/open-webui/discussions).
If your feature request might impact others in the community, definitely open a discussion instead and evaluate whether and how to implement it.
This will help us efficiently focus on improving the project.
### Collaborate respectfully
We value a **constructive attitude**, so please be mindful of your communication. If negativity is part of your approach, our capacity to engage may be limited. We're here to help if you're **open to learning** and **communicating positively**.
@@ -22,7 +33,6 @@ body:
We appreciate your time and ask that you **respect ours**.
### Contributing
If you encounter an issue, we highly encourage you to submit a pull request or fork the project. We actively work to prevent contributor burnout to maintain the quality and continuity of Open WebUI.
@@ -35,14 +45,22 @@ body:
label: Check Existing Issues
description: Please confirm that you've checked for existing similar requests
options:
- label: I have searched the existing issues and discussions.
- label: I have searched for all existing **open AND closed** issues and discussions for similar requests. I have found none that is comparable to my request.
required: true
- type: checkboxes
id: feature-scope
attributes:
label: Verify Feature Scope
description: Please confirm the feature's scope is within the described scope
options:
- label: I have read through and understood the scope definition for feature requests in the Issues section. I believe my feature request meets the definition and belongs in the Issues section instead of the Discussions.
required: true
- type: textarea
id: problem-description
attributes:
label: Problem Description
description: Is your feature request related to a problem? Please provide a clear and concise description of what the problem is.
placeholder: "Ex. I'm always frustrated when..."
placeholder: "Ex. I'm always frustrated when... / Not related to a problem"
validations:
required: true
- type: textarea
+12 -6
View File
@@ -1,17 +1,20 @@
# Pull Request Checklist
### Note to first-time contributors: Please open a discussion post in [Discussions](https://github.com/open-webui/open-webui/discussions) and describe your changes before submitting a pull request.
### Note to first-time contributors: Please open a discussion post in [Discussions](https://github.com/open-webui/open-webui/discussions) to discuss your idea/fix with the community before creating a pull request, and describe your changes before submitting a pull request.
This is to ensure large feature PRs are discussed with the community first, before starting work on it. If the community does not want this feature or it is not relevant for Open WebUI as a project, it can be identified in the discussion before working on the feature and submitting the PR.
**Before submitting, make sure you've checked the following:**
- [ ] **Target branch:** Please verify that the pull request targets the `dev` branch.
- [ ] **Description:** Provide a concise description of the changes made in this pull request.
- [ ] **Target branch:** Verify that the pull request targets the `dev` branch. **Not targeting the `dev` branch will lead to immediate closure of the PR.**
- [ ] **Description:** Provide a concise description of the changes made in this pull request down below.
- [ ] **Changelog:** Ensure a changelog entry following the format of [Keep a Changelog](https://keepachangelog.com/) is added at the bottom of the PR description.
- [ ] **Documentation:** Have you updated relevant documentation [Open WebUI Docs](https://github.com/open-webui/docs), or other documentation sources?
- [ ] **Documentation:** If necessary, update relevant documentation [Open WebUI Docs](https://github.com/open-webui/docs) like environment variables, the tutorials, or other documentation sources.
- [ ] **Dependencies:** Are there any new dependencies? Have you updated the dependency versions in the documentation?
- [ ] **Testing:** Have you written and run sufficient tests to validate the changes?
- [ ] **Testing:** Perform manual tests to **verify the implemented fix/feature works as intended AND does not break any other functionality**. Take this as an opportunity to **make screenshots of the feature/fix and include it in the PR description**.
- [ ] **Agentic AI Code:** Confirm this Pull Request is **not written by any AI Agent** or has at least **gone through additional human review AND manual testing**. If any AI Agent is the co-author of this PR, it may lead to immediate closure of the PR.
- [ ] **Code review:** Have you performed a self-review of your code, addressing any coding standard issues and ensuring adherence to the project's coding standards?
- [ ] **Prefix:** To clearly categorize this pull request, prefix the pull request title using one of the following:
- [ ] **Title Prefix:** To clearly categorize this pull request, prefix the pull request title using one of the following:
- **BREAKING CHANGE**: Significant changes that may affect compatibility
- **build**: Changes that affect the build system or external dependencies
- **ci**: Changes to our continuous integration processes or workflows
@@ -74,3 +77,6 @@
### Contributor License Agreement
By submitting this pull request, I confirm that I have read and fully agree to the [Contributor License Agreement (CLA)](https://github.com/open-webui/open-webui/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT), and I am providing my contributions under its terms.
> [!NOTE]
> Deleting the CLA section will lead to immediate closure of your PR and it will not be merged in.
+212 -12
View File
@@ -5,25 +5,225 @@ 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.35] - 2025-11-06
### Added
- 🖼️ Image generation system received a comprehensive overhaul with major new capabilities including full image editing support allowing users to modify existing images using text prompts with OpenAI, Gemini, or ComfyUI engines, adding Gemini 2.5 Flash Image (Nano Banana) support, Qwen Image Edit integration, resolution of base64-encoded image display issues, streamlined AUTOMATIC1111 configuration by consolidating parameters into a flexible JSON parameters field, and enhanced UI with a code editor modal for ComfyUI workflow management. [#17434](https://github.com/open-webui/open-webui/pull/17434), [#16976](https://github.com/open-webui/open-webui/issues/16976), [Commit](https://github.com/open-webui/open-webui/commit/8e5690aab4f632a57027e2acf880b8f89a8717c0), [Commit](https://github.com/open-webui/open-webui/commit/72f8539fd2e679fec0762945f22f4b8a6920afa0), [Commit](https://github.com/open-webui/open-webui/commit/8d34fcb586eeee1fac6da2f991518b8a68b00b72), [Commit](https://github.com/open-webui/open-webui/commit/72900cd686de1fa6be84b5a8a2fc857cff7b91b8)
- 🔒 CORS origin validation was added to WebSocket connections as a defense-in-depth security measure against cross-site WebSocket hijacking attacks. [#18411](https://github.com/open-webui/open-webui/pull/18411), [#18410](https://github.com/open-webui/open-webui/issues/18410)
- 🔄 Automatic page refresh now occurs when a version update is detected via WebSocket connection, ensuring users always run the latest version without cache issues. [Commit](https://github.com/open-webui/open-webui/commit/989f192c92d2fe55daa31336e7971e21798b96ae)
- 🐍 Experimental initial preparations for Python 3.13 compatibility by updating dependencies with security enhancements and cryptographic improvements. [#18430](https://github.com/open-webui/open-webui/pull/18430), [#18424](https://github.com/open-webui/open-webui/pull/18424)
- ⚡ Image compression now preserves the original image format instead of converting to PNG, significantly reducing file sizes and improving chat loading performance. [#18506](https://github.com/open-webui/open-webui/pull/18506)
- 🎤 Mistral Voxtral model support was added for text-to-speech, including voxtral-small and voxtral-mini models with both transcription and chat completion API support. [#18934](https://github.com/open-webui/open-webui/pull/18934)
- 🔊 Text-to-speech now uses a global audio queue system to prevent overlapping playback, ensuring only one TTS instance plays at a time with proper stop/start controls and automatic cleanup when switching between messages. [#16152](https://github.com/open-webui/open-webui/pull/16152), [#18744](https://github.com/open-webui/open-webui/pull/18744), [#16150](https://github.com/open-webui/open-webui/issues/16150)
- 🔊 ELEVENLABS_API_BASE_URL environment variable now allows configuration of custom ElevenLabs API endpoints, enabling support for EU residency API requirements. [#18402](https://github.com/open-webui/open-webui/issues/18402)
- 🔐 OAUTH_ROLES_SEPARATOR environment variable now allows custom role separators for OAuth roles that contain commas, useful for roles specified in LDAP syntax. [#18572](https://github.com/open-webui/open-webui/pull/18572)
- 📄 External document loaders can now optionally forward user information headers when ENABLE_FORWARD_USER_INFO_HEADERS is enabled, enabling cost tracking, audit logs, and usage analytics for external services. [#18731](https://github.com/open-webui/open-webui/pull/18731)
- 📄 MISTRAL_OCR_API_BASE_URL environment variable now allows configuration of custom Mistral OCR API endpoints for flexible deployment options. [Commit](https://github.com/open-webui/open-webui/commit/415b93c7c35c2e2db4425e6da1b88b3750f496b0)
- ⌨️ Keyboard shortcut hints are now displayed on sidebar buttons with a refactored shortcuts modal that accurately reflects all available hotkeys across different keyboard layouts. [#18473](https://github.com/open-webui/open-webui/pull/18473)
- 🛠️ Tooltips now display tool descriptions when hovering over tool names on the model edit page, improving usability and providing immediate context. [#18707](https://github.com/open-webui/open-webui/pull/18707)
- 📝 "Create a new note" from the search modal now immediately creates a new private note and opens it in the editor instead of navigating to the generic notes page. [#18255](https://github.com/open-webui/open-webui/pull/18255)
- 🖨️ Code block output now preserves whitespace formatting with monospace font to accurately reflect terminal behavior. [#18352](https://github.com/open-webui/open-webui/pull/18352)
- ✏️ Edit button is now available in the three-dot menu of models in the workspace section for quick access to model editing, with the menu reorganized for better user experience and Edit, Clone, Copy Link, and Share options logically grouped. [#18574](https://github.com/open-webui/open-webui/pull/18574)
- 📌 Sidebar models section is now collapsible, allowing users to expand and collapse the pinned models list for better sidebar organization. [Commit](https://github.com/open-webui/open-webui/commit/82c08a3b5d189f81c96b6548cc872198771015b0)
- 🌙 Dark mode styles for select elements were added using Tailwind CSS classes, improving consistency across the interface. [#18636](https://github.com/open-webui/open-webui/pull/18636)
- 🔄 Various improvements were implemented across the frontend and backend to enhance performance, stability, and security.
- 🌐 Translations for Portuguese (Brazil), Greek, German, Traditional Chinese, Simplified Chinese, Spanish, Georgian, Danish, and Estonian were enhanced and expanded.
### Fixed
- 🔒 Server-Sent Event (SSE) code injection vulnerability in Direct Connections is resolved by blocking event emission from untrusted external model servers; event emitters from direct connected model servers are no longer supported, preventing arbitrary JavaScript execution in user browsers. [Commit](https://github.com/open-webui/open-webui/commit/8af6a4cf21b756a66cd58378a01c60f74c39b7ca)
- 🛡️ DOM XSS vulnerability in "Insert Prompt as Rich Text" is resolved by sanitizing HTML content with DOMPurify before rendering. [Commit](https://github.com/open-webui/open-webui/commit/eb9c4c0e358c274aea35f21c2856c0a20051e5f1)
- ⚙️ MCP server cancellation scope corruption is prevented by reversing disconnection order to follow LIFO and properly handling exceptions, resolving 100% CPU usage when resuming chats with expired tokens or using multiple streamable MCP servers. [#18537](https://github.com/open-webui/open-webui/pull/18537)
- 🔧 UI freeze when querying models with knowledge bases containing inconsistent distance metrics is resolved by properly initializing the distances array in citations. [#18585](https://github.com/open-webui/open-webui/pull/18585)
- 🤖 Duplicate model IDs from multiple OpenAI endpoints are now automatically deduplicated server-side, preventing frontend crashes for users with unified gateway proxies that aggregate multiple providers. [Commit](https://github.com/open-webui/open-webui/commit/fdf7ca11d4f3cc8fe63e81c98dc0d1e48e52ba36)
- 🔐 Login failures with passwords longer than 72 bytes are resolved by safely truncating oversized passwords for bcrypt compatibility. [#18157](https://github.com/open-webui/open-webui/issues/18157)
- 🔐 OAuth 2.1 MCP tool connections now automatically re-register clients when stored client IDs become stale, preventing unauthorized_client errors after editing tool endpoints and providing detailed error messages for callback failures. [#18415](https://github.com/open-webui/open-webui/pull/18415), [#18309](https://github.com/open-webui/open-webui/issues/18309)
- 🔓 OAuth 2.1 discovery, metadata fetching, and dynamic client registration now correctly use HTTP proxy environment variables when trust_env is enabled. [Commit](https://github.com/open-webui/open-webui/commit/bafeb76c411483bd6b135f0edbcdce048120f264)
- 🔌 MCP server connection failures now display clear error messages in the chat interface instead of silently failing. [#18892](https://github.com/open-webui/open-webui/pull/18892), [#18889](https://github.com/open-webui/open-webui/issues/18889)
- 💬 Chat titles are now properly generated even when title auto-generation is disabled in interface settings, fixing an issue where chats would remain labeled as "New chat". [#18761](https://github.com/open-webui/open-webui/pull/18761), [#18717](https://github.com/open-webui/open-webui/issues/18717), [#6478](https://github.com/open-webui/open-webui/issues/6478)
- 🔍 Chat query errors are prevented by properly validating and handling the "order_by" parameter to ensure requested columns exist. [#18400](https://github.com/open-webui/open-webui/pull/18400), [#18452](https://github.com/open-webui/open-webui/pull/18452)
- 🔧 Root-level max_tokens parameter is no longer dropped when proxying to Ollama, properly converting to num_predict to limit output token length as intended. [#18618](https://github.com/open-webui/open-webui/issues/18618)
- 🔑 Self-hosted Marker instances can now be used without requiring an API key, while keeping it optional for datalab Marker service users. [#18617](https://github.com/open-webui/open-webui/issues/18617)
- 🔧 OpenAPI specification endpoint conflict between "/api/v1/models" and "/api/v1/models/" is resolved by changing the models router endpoint to "/list", preventing duplicate operationId errors when generating TypeScript API clients. [#18758](https://github.com/open-webui/open-webui/issues/18758)
- 🏷️ Model tags are now de-duplicated case-insensitively in both the model selector and workspace models page, preventing duplicate entries with different capitalization from appearing in filter dropdowns. [#18716](https://github.com/open-webui/open-webui/pull/18716), [#18711](https://github.com/open-webui/open-webui/issues/18711)
- 📄 Docling RAG parameter configuration is now correctly saved in the admin UI by fixing the typo in the "DOCLING_PARAMS" parameter name. [#18390](https://github.com/open-webui/open-webui/pull/18390)
- 📃 Tika document processing now automatically detects content types instead of relying on potentially incorrect browser-provided mime-types, improving file handling accuracy for formats like RTF. [#18765](https://github.com/open-webui/open-webui/pull/18765), [#18683](https://github.com/open-webui/open-webui/issues/18683)
- 🖼️ Image and video uploads to knowledge bases now display proper error messages instead of showing an infinite spinner when the content extraction engine does not support these file types. [#18514](https://github.com/open-webui/open-webui/issues/18514)
- 📝 Notes PDF export now properly detects and applies dark mode styling consistently across both the notes list and individual note pages, with a shared utility function to eliminate code duplication. [#18526](https://github.com/open-webui/open-webui/issues/18526)
- 💭 Details tags for reasoning content are now correctly identified and rendered even when the same tag is present in user messages. [#18840](https://github.com/open-webui/open-webui/pull/18840), [#18294](https://github.com/open-webui/open-webui/issues/18294)
- 📊 Mermaid and Vega rendering errors now display inline with the code instead of showing repetitive toast notifications, improving user experience when models generate invalid diagram syntax. [Commit](https://github.com/open-webui/open-webui/commit/fdc0f04a8b7dd0bc9f9dc0e7e30854f7a0eea3e9)
- 📈 Mermaid diagram rendering errors no longer cause UI unavailability or display error messages below the input box. [#18493](https://github.com/open-webui/open-webui/pull/18493), [#18340](https://github.com/open-webui/open-webui/issues/18340)
- 🔗 Web search SSL verification is now asynchronous, preventing the website from hanging during web search operations. [#18714](https://github.com/open-webui/open-webui/pull/18714), [#18699](https://github.com/open-webui/open-webui/issues/18699)
- 🌍 Web search results now correctly use HTTP proxy environment variables when WEB_SEARCH_TRUST_ENV is enabled. [#18667](https://github.com/open-webui/open-webui/pull/18667), [#7008](https://github.com/open-webui/open-webui/discussions/7008)
- 🔍 Google Programmable Search Engine now properly includes referer headers, enabling API keys with HTTP referrer restrictions configured in Google Cloud Console. [#18871](https://github.com/open-webui/open-webui/pull/18871), [#18870](https://github.com/open-webui/open-webui/issues/18870)
- ⚡ YouTube video transcript fetching now works correctly when using a proxy connection. [#18419](https://github.com/open-webui/open-webui/pull/18419)
- 🎙️ Speech-to-text transcription no longer deletes or replaces existing text in the prompt input field, properly preserving any previously entered content. [#18540](https://github.com/open-webui/open-webui/issues/18540)
- 🎙️ The "Instant Auto-Send After Voice Transcription" setting now functions correctly and automatically sends transcribed text when enabled. [#18466](https://github.com/open-webui/open-webui/issues/18466)
- ⚙️ Chat settings now load properly when reopening a tab or starting a new session by initializing defaults when sessionStorage is empty. [#18438](https://github.com/open-webui/open-webui/pull/18438)
- 🔎 Folder tag search in the sidebar now correctly handles folder names with multiple spaces by replacing all spaces with underscores. [Commit](https://github.com/open-webui/open-webui/commit/a8fe979af68e47e4e4bb3eb76e48d93d60cd2a45)
- 🛠️ Functions page now updates immediately after deleting a function, removing the need for a manual page reload. [#18912](https://github.com/open-webui/open-webui/pull/18912), [#18908](https://github.com/open-webui/open-webui/issues/18908)
- 🛠️ Native tool calling now properly supports sequential tool calls with shared context, allowing tools to access images and data from previous tool executions in the same conversation. [#18664](https://github.com/open-webui/open-webui/pull/18664)
- 🎯 Globally enabled actions in the model editor now correctly apply as global instead of being treated as disabled. [#18577](https://github.com/open-webui/open-webui/pull/18577)
- 📋 Clipboard images pasted via the "{{CLIPBOARD}}" prompt variable are now correctly converted to base64 format before being sent to the backend, resolving base64 encoding errors. [#18432](https://github.com/open-webui/open-webui/pull/18432), [#18425](https://github.com/open-webui/open-webui/issues/18425)
- 📋 File list is now cleared when switching to models that do not support file uploads, preventing files from being sent to incompatible models. [#18496](https://github.com/open-webui/open-webui/pull/18496)
- 📂 Move menu no longer displays when folders are empty. [#18484](https://github.com/open-webui/open-webui/pull/18484)
- 📁 Folder and channel creation now validates that names are not empty, preventing creation of folders or channels with no name and showing an error toast if attempted. [#18564](https://github.com/open-webui/open-webui/pull/18564)
- 🖊️ Rich text input no longer removes text between equals signs when pasting code with comparison operators. [#18551](https://github.com/open-webui/open-webui/issues/18551)
- ⌨️ Keyboard shortcuts now display the correct keys for international and non-QWERTY keyboard layouts by detecting the user's layout using the Keyboard API. [#18533](https://github.com/open-webui/open-webui/pull/18533)
- 🌐 "Attach Webpage" button now displays with correct disabled styling when a model does not support file uploads. [#18483](https://github.com/open-webui/open-webui/pull/18483)
- 🎚️ Divider no longer displays in the integrations menu when no integrations are enabled. [#18487](https://github.com/open-webui/open-webui/pull/18487)
- 📱 Chat controls button is now properly hidden on mobile for users without admin or explicit chat control permissions. [#18641](https://github.com/open-webui/open-webui/pull/18641)
- 📍 User menu, download submenu, and move submenu are now repositioned to prevent overlap with the Chat Controls sidebar when it is open. [Commit](https://github.com/open-webui/open-webui/commit/414ab51cb6df1ab0d6c85ac6c1f2c5c9a5f8e2aa)
- 🎯 Artifacts button no longer appears in the chat menu when there are no artifacts to display. [Commit](https://github.com/open-webui/open-webui/commit/ed6449d35f84f68dc75ee5c6b3f4748a3fda0096)
- 🎨 Artifacts view now automatically displays when opening an existing conversation containing artifacts, improving user experience. [#18215](https://github.com/open-webui/open-webui/pull/18215)
- 🖌️ Formatting toolbar is no longer hidden under images or code blocks in chat and now displays correctly above all message content.
- 🎨 Layout shift near system instructions is prevented by properly rendering the chat component when system prompts are empty. [#18594](https://github.com/open-webui/open-webui/pull/18594)
- 📐 Modal layout shift caused by scrollbar appearance is prevented by adding a stable scrollbar gutter. [#18591](https://github.com/open-webui/open-webui/pull/18591)
- ✨ Spacing between icon and label in the user menu dropdown items is now consistent. [#18595](https://github.com/open-webui/open-webui/pull/18595)
- 💬 Duplicate prompt suggestions no longer cause the webpage to freeze or throw JavaScript errors by implementing proper key management with composite keys. [#18841](https://github.com/open-webui/open-webui/pull/18841), [#18566](https://github.com/open-webui/open-webui/issues/18566)
- 🔍 Chat preview loading in the search modal now works correctly for all search results by fixing an index boundary check that previously caused out-of-bounds errors. [#18911](https://github.com/open-webui/open-webui/pull/18911)
- ♿ Screen reader support was enhanced by wrapping messages in semantic elements with descriptive aria-labels, adding "Assistant is typing" and "Response complete" announcements for improved accessibility. [#18735](https://github.com/open-webui/open-webui/pull/18735)
- 🔒 Incorrect await call in the OAuth 2.1 flow is removed, eliminating a logged exception during authentication. [#18236](https://github.com/open-webui/open-webui/pull/18236)
- 🛡️ Duplicate crossorigin attribute in the manifest file was removed. [#18413](https://github.com/open-webui/open-webui/pull/18413)
### Changed
- 🔄 Firecrawl integration was refactored to use the official Firecrawl SDK instead of direct HTTP requests and langchain_community FireCrawlLoader, improving reliability and performance with batch scraping support and enhanced error handling. [#18635](https://github.com/open-webui/open-webui/pull/18635)
- 📄 MinerU content extraction engine now only supports PDF files following the upstream removal of LibreOffice document conversion in version 2.0.0; users needing to process office documents should convert them to PDF format first. [#18448](https://github.com/open-webui/open-webui/issues/18448)
## [0.6.34] - 2025-10-16
### Added
- 📄 MinerU is now supported as a document parser backend, with support for both local and managed API deployments. [#18306](https://github.com/open-webui/open-webui/pull/18306)
- 🔒 JWT token expiration default is now set to 4 weeks instead of never expiring, with security warnings displayed in backend logs and admin UI when set to unlimited. [#18261](https://github.com/open-webui/open-webui/pull/18261), [#18262](https://github.com/open-webui/open-webui/pull/18262)
- ⚡ Page loading performance is improved by preventing unnecessary API requests when sidebar folders are not expanded. [#18179](https://github.com/open-webui/open-webui/pull/18179), [#17476](https://github.com/open-webui/open-webui/issues/17476)
- 📁 File hash values are now included in the knowledge endpoint response, enabling efficient file synchronization through hash comparison. [#18284](https://github.com/open-webui/open-webui/pull/18284), [#18283](https://github.com/open-webui/open-webui/issues/18283)
- 🎨 Chat dialog scrollbar visibility is improved by increasing its width, making it easier to use for navigation. [#18369](https://github.com/open-webui/open-webui/pull/18369), [#11782](https://github.com/open-webui/open-webui/issues/11782)
- 🔄 Various improvements were implemented across the frontend and backend to enhance performance, stability, and security.
- 🌐 Translations for Catalan, Chinese, Czech, Finnish, German, Kabyle, Korean, Portuguese (Brazil), Spanish, Thai, and Turkish were enhanced and expanded.
### Fixed
- 📚 Focused retrieval mode now works correctly, preventing the system from forcing full context mode and loading all documents in a knowledge base regardless of settings. [#18133](https://github.com/open-webui/open-webui/issues/18133)
- 🔧 Filter inlet functions now correctly execute on tool call continuations, ensuring parameter persistence throughout tool interactions. [#18222](https://github.com/open-webui/open-webui/issues/18222)
- 🛠️ External tool servers now properly support DELETE requests with body data. [#18289](https://github.com/open-webui/open-webui/pull/18289), [#18287](https://github.com/open-webui/open-webui/issues/18287)
- 🗄️ Oracle23ai vector database client now correctly handles variable initialization, resolving UnboundLocalError when retrieving items from collections. [#18356](https://github.com/open-webui/open-webui/issues/18356)
- 🔧 Model auto-pull functionality now works correctly even when user settings remain unmodified. [#18324](https://github.com/open-webui/open-webui/pull/18324)
- 🎨 Duplicate HTML content in artifacts is now prevented by improving code block detection logic. [#18195](https://github.com/open-webui/open-webui/pull/18195), [#6154](https://github.com/open-webui/open-webui/issues/6154)
- 💬 Pinned chats now appear in the Reference Chats list and can be referenced in conversations. [#18288](https://github.com/open-webui/open-webui/issues/18288)
- 📝 Misleading knowledge base warning text in documents settings is clarified to correctly instruct users about reindexing vectors. [#18263](https://github.com/open-webui/open-webui/pull/18263)
- 🔔 Toast notifications can now be dismissed even when a modal is open. [#18260](https://github.com/open-webui/open-webui/pull/18260)
- 🔘 The "Chats" button in the sidebar now correctly toggles chat list visibility without navigating away from the current page. [#18232](https://github.com/open-webui/open-webui/pull/18232)
- 🎯 The Integrations menu no longer closes prematurely when clicking outside the Valves modal. [#18310](https://github.com/open-webui/open-webui/pull/18310)
- 🛠️ Tool ID display issues where "undefined" was incorrectly shown in the interface are now resolved. [#18178](https://github.com/open-webui/open-webui/pull/18178)
- 🛠️ Model management issues caused by excessively long model IDs are now prevented through validation that limits model IDs to 256 characters. [#18125](https://github.com/open-webui/open-webui/issues/18125)
## [0.6.33] - 2025-10-08
### Added
- 🎨 Workspace interface received a comprehensive redesign across Models, Knowledge, Prompts, and Tools sections, featuring reorganized controls, view filters for created vs shared items, tag selectors, improved visual hierarchy, and streamlined import/export functionality. [Commit](https://github.com/open-webui/open-webui/commit/2c59a288603d8c5f004f223ee00fef37cc763a8e), [Commit](https://github.com/open-webui/open-webui/commit/6050c86ab6ef6b8c96dd3f99c62a6867011b67a4), [Commit](https://github.com/open-webui/open-webui/commit/96ecb47bc71c072aa34ef2be10781b042bef4e8c), [Commit](https://github.com/open-webui/open-webui/commit/2250d102b28075a9611696e911536547abb8b38a), [Commit](https://github.com/open-webui/open-webui/commit/23c8f6d507bfee75ab0015a3e2972d5c26f7e9bf), [Commit](https://github.com/open-webui/open-webui/commit/a743b16728c6ae24b8befbc2d7f24eb9e20c4ad5)
- 🛠️ Functions admin interface received a comprehensive redesign with creator attribution display, ownership filters for created vs shared items, improved organization, and refined styling. [Commit](https://github.com/open-webui/open-webui/commit/f5e1a42f51acc0b9d5b63a33c1ca2e42470239c1)
- ⚡ Page initialization performance is significantly improved through parallel data loading and optimized folder API calls, reducing initial page load time. [#17559](https://github.com/open-webui/open-webui/pull/17559), [#17889](https://github.com/open-webui/open-webui/pull/17889)
- ⚡ Chat overview component is now dynamically loaded on demand, reducing initial page bundle size by approximately 470KB and improving first-screen loading speed. [#17595](https://github.com/open-webui/open-webui/pull/17595)
- 📁 Folders can now be attached to chats using the "#" command, automatically expanding to include all files within the folder for streamlined knowledge base integration. [Commit](https://github.com/open-webui/open-webui/commit/d2cb78179d66dc85188172a08622d4c97a2ea1ee)
- 📱 Progressive Web App now supports Android share target functionality, allowing users to share web pages, YouTube videos, and text directly to Open WebUI from the system share menu. [#17633](https://github.com/open-webui/open-webui/pull/17633), [#17125](https://github.com/open-webui/open-webui/issues/17125)
- 🗄️ Redis session storage is now available as an experimental option for OAuth authentication flows via the ENABLE_STAR_SESSIONS_MIDDLEWARE environment variable, providing shared session state across multi-replica deployments to address CSRF errors, though currently only basic Redis setups are supported. [#17223](https://github.com/open-webui/open-webui/pull/17223), [#15373](https://github.com/open-webui/open-webui/issues/15373), [Docs:Commit](https://github.com/open-webui/docs/commit/14052347f165d1b597615370373d7289ce44c7f9)
- 📊 Vega and Vega-Lite chart visualization renderers are now supported in code blocks, enabling inline rendering of data visualizations with automatic compilation of Vega-Lite specifications. [#18033](https://github.com/open-webui/open-webui/pull/18033), [#18040](https://github.com/open-webui/open-webui/pull/18040), [#18022](https://github.com/open-webui/open-webui/issues/18022)
- 🔗 OpenAI connections now support custom HTTP headers, enabling users to configure authentication and routing headers for specific deployment requirements. [#18021](https://github.com/open-webui/open-webui/pull/18021), [#9732](https://github.com/open-webui/open-webui/discussions/9732)
- 🔐 OpenID Connect authentication now supports OIDC providers without email scope via the ENABLE_OAUTH_WITHOUT_EMAIL environment variable, enabling compatibility with identity providers that don't expose email addresses. [#18047](https://github.com/open-webui/open-webui/pull/18047), [#18045](https://github.com/open-webui/open-webui/issues/18045)
- 🤖 Ollama model management modal now features individual model update cancellation, comprehensive tooltips for all buttons, and streamlined notification behavior to reduce toast spam. [#16863](https://github.com/open-webui/open-webui/pull/16863)
- ☁️ OneDrive file picker now includes search functionality and "My Organization" pivot for business accounts, enabling easier file discovery across organizational content. [#17930](https://github.com/open-webui/open-webui/pull/17930), [#17929](https://github.com/open-webui/open-webui/issues/17929)
- 📊 Chat overview flow diagram now supports toggling between vertical and horizontal layout orientations for improved visualization flexibility. [#17941](https://github.com/open-webui/open-webui/pull/17941)
- 🔊 OpenAI Text-to-Speech engine now supports additional parameters, allowing users to customize TTS behavior with provider-specific options via JSON configuration. [#17985](https://github.com/open-webui/open-webui/issues/17985), [#17188](https://github.com/open-webui/open-webui/pull/17188)
- 🛠️ Tool server list now displays server name, URL, and type (OpenAPI or MCP) for easier identification and management. [#18062](https://github.com/open-webui/open-webui/issues/18062)
- 📁 Folders now remember the last selected model, automatically applying it when starting new chats within that folder. [#17836](https://github.com/open-webui/open-webui/issues/17836)
- 🔢 Ollama embedding endpoint now supports the optional dimensions parameter for controlling embedding output size, compatible with Ollama v0.11.11 and later. [#17942](https://github.com/open-webui/open-webui/pull/17942)
- ⚡ Workspace knowledge page load time is improved by removing redundant API calls, enhancing overall responsiveness. [#18057](https://github.com/open-webui/open-webui/pull/18057)
- ⚡ File metadata query performance is enhanced by selecting only relevant columns instead of retrieving entire records, reducing database overhead. [#18013](https://github.com/open-webui/open-webui/pull/18013)
- 📄 Note PDF exports now include titles and properly render in dark mode with appropriate background colors. [Commit](https://github.com/open-webui/open-webui/commit/216fb5c3db1a223ffe6e72d97aa9551fe0e2d028)
- 📄 Docling document extraction now supports additional parameters for VLM pipeline configuration, enabling customized vision model settings. [#17363](https://github.com/open-webui/open-webui/pull/17363)
- ⚙️ Server startup script now supports passing arbitrary arguments to uvicorn, enabling custom server configuration options. [#17919](https://github.com/open-webui/open-webui/pull/17919), [#17918](https://github.com/open-webui/open-webui/issues/17918)
- 🔄 Various improvements were implemented across the frontend and backend to enhance performance, stability, and security.
- 🌐 Translations for German, Danish, Spanish, Korean, Portuguese (Brazil), Simplified Chinese, and Traditional Chinese were enhanced and expanded.
### Fixed
- 💬 System prompts are no longer duplicated in chat requests, eliminating confusion and excessive token usage caused by repeated instructions being sent to models. [#17198](https://github.com/open-webui/open-webui/issues/17198), [#16855](https://github.com/open-webui/open-webui/issues/16855)
- 🔐 MCP OAuth 2.1 authentication now complies with the standard by implementing PKCE with S256 code challenge method and explicitly passing client credentials during token authorization, resolving "code_challenge: Field required" and "client_id: Field required" errors when connecting to OAuth-secured MCP servers. [Commit](https://github.com/open-webui/open-webui/commit/911a114ad459f5deebd97543c13c2b90196efb54), [#18010](https://github.com/open-webui/open-webui/issues/18010), [#18087](https://github.com/open-webui/open-webui/pull/18087)
- 🔐 OAuth signup flow now handles password hashing correctly by migrating from passlib to native bcrypt, preventing failures when passwords exceed 72 bytes. [#17917](https://github.com/open-webui/open-webui/issues/17917)
- 🔐 OAuth token refresh errors are resolved by properly registering and storing OAuth clients, fixing "Constructor parameter should be str" exceptions for Google, Microsoft, and OIDC providers. [#17829](https://github.com/open-webui/open-webui/issues/17829)
- 🔐 OAuth server metadata URL is now correctly accessed via the proper attribute, fixing automatic token refresh and logout functionality for Microsoft OAuth provider when OPENID_PROVIDER_URL is not set. [#18065](https://github.com/open-webui/open-webui/pull/18065)
- 🔐 OAuth credential decryption failures now allow the application to start gracefully with clear error messages instead of crashing, preventing complete service outages when WEBUI_SECRET_KEY mismatches occur during database migrations or environment changes. [#18094](https://github.com/open-webui/open-webui/pull/18094), [#18092](https://github.com/open-webui/open-webui/issues/18092)
- 🔐 OAuth 2.1 server discovery now correctly attempts all configured discovery URLs in sequence instead of only trying the first URL. [#17906](https://github.com/open-webui/open-webui/pull/17906), [#17904](https://github.com/open-webui/open-webui/issues/17904), [#18026](https://github.com/open-webui/open-webui/pull/18026)
- 🔐 Login redirect now correctly honors the redirect query parameter after authentication, ensuring users are returned to their intended destination with query parameters intact instead of defaulting to the homepage. [#18071](https://github.com/open-webui/open-webui/issues/18071)
- ☁️ OneDrive Business integration authentication regression is resolved, ensuring the popup now properly triggers when connecting to OneDrive accounts. [#17902](https://github.com/open-webui/open-webui/pull/17902), [#17825](https://github.com/open-webui/open-webui/discussions/17825), [#17816](https://github.com/open-webui/open-webui/issues/17816)
- 👥 Default group settings now persist correctly after page navigation, ensuring configuration changes are properly saved and retained. [#17899](https://github.com/open-webui/open-webui/issues/17899), [#18003](https://github.com/open-webui/open-webui/issues/18003)
- 📁 Folder data integrity is now verified on retrieval, automatically fixing orphaned folders with invalid parent references and ensuring proper cascading deletion of nested folder structures. [Commit](https://github.com/open-webui/open-webui/commit/5448618dd5ea181b9635b77040cef60926a902ff)
- 🗄️ Redis Sentinel and Redis Cluster configurations with the experimental ENABLE_STAR_SESSIONS_MIDDLEWARE feature are now properly isolated by making the feature opt-in only, preventing ReadOnlyError failures when connecting to read replicas in multi-node Redis deployments. [#18073](https://github.com/open-webui/open-webui/issues/18073)
- 📊 Mermaid and Vega diagram rendering now displays error toast notifications when syntax errors are detected, helping users identify and fix diagram issues instead of silently failing. [#18068](https://github.com/open-webui/open-webui/pull/18068)
- 🤖 Reasoning models that return reasoning_content instead of content no longer cause NoneType errors during chat title generation, follow-up suggestions, and tag generation. [#18080](https://github.com/open-webui/open-webui/pull/18080)
- 📚 Citation rendering now correctly handles multiple source references in a single bracket, parsing formats like [1,2] and [1, 2] into separate clickable citation links. [#18120](https://github.com/open-webui/open-webui/pull/18120)
- 🔍 Web search now handles individual source failures gracefully, continuing to process remaining sources instead of failing entirely when a single URL is unreachable or returns an error. [Commit](https://github.com/open-webui/open-webui/commit/e000494e488090c5f66989a2b3f89d3eaeb7946b), [Commit](https://github.com/open-webui/open-webui/commit/53e98620bff38ab9280aee5165af0a704bdd99b9)
- 🔍 Hybrid search with reranking now handles empty result sets gracefully instead of crashing with ValueError when all results are filtered out due to relevance thresholds. [#18096](https://github.com/open-webui/open-webui/issues/18096)
- 🔍 Reranking models without defined padding tokens now work correctly by automatically falling back to eos_token_id as pad_token_id, fixing "Cannot handle batch sizes > 1" errors for models like Qwen3-Reranker. [#18108](https://github.com/open-webui/open-webui/pull/18108), [#16027](https://github.com/open-webui/open-webui/discussions/16027)
- 🔍 Model selector search now correctly returns results for non-admin users by dynamically updating the search index when the model list changes, fixing a race condition that caused empty search results. [#17996](https://github.com/open-webui/open-webui/pull/17996), [#17960](https://github.com/open-webui/open-webui/pull/17960)
- ⚡ Task model function calling performance is improved by excluding base64 image data from payloads, significantly reducing token count and memory usage when images are present in conversations. [#17897](https://github.com/open-webui/open-webui/pull/17897)
- 🤖 Text selection "Ask" action now correctly recognizes and uses local models configured via direct connections instead of only showing external provider models. [#17896](https://github.com/open-webui/open-webui/issues/17896)
- 🛑 Task cancellation API now returns accurate response status, correctly reporting successful cancellations instead of incorrectly indicating failures. [#17920](https://github.com/open-webui/open-webui/issues/17920)
- 💬 Follow-up query suggestions are now generated and displayed in temporary chats, matching the behavior of saved chats. [#14987](https://github.com/open-webui/open-webui/issues/14987)
- 🔊 Azure Text-to-Speech now properly escapes special characters like ampersands in SSML, preventing HTTP 400 errors and ensuring audio generation succeeds for all text content. [#17962](https://github.com/open-webui/open-webui/issues/17962)
- 🛠️ OpenAPI tool server calls with optional parameters now execute successfully even when no arguments are provided, removing the incorrect requirement for a request body. [#18036](https://github.com/open-webui/open-webui/issues/18036)
- 🛠️ MCP mode tool server connections no longer incorrectly validate the OpenAPI path field, allowing seamless switching between OpenAPI and MCP connection types. [#17989](https://github.com/open-webui/open-webui/pull/17989), [#17988](https://github.com/open-webui/open-webui/issues/17988)
- 🛠️ Third-party tool responses containing non-UTF8 or invalid byte sequences are now handled gracefully without causing request failures. [#17882](https://github.com/open-webui/open-webui/pull/17882)
- 🎨 Workspace filter dropdown now correctly renders model tags as strings instead of displaying individual characters, fixing broken filtering interface when models have multiple tags. [#18034](https://github.com/open-webui/open-webui/issues/18034)
- ⌨️ Ctrl+Enter keyboard shortcut now correctly sends messages in mobile and narrow browser views on Chrome instead of inserting newlines. [#17975](https://github.com/open-webui/open-webui/issues/17975)
- ⌨️ Tab characters are now preserved when pasting code or formatted text into the chat input box in plain text mode. [#17958](https://github.com/open-webui/open-webui/issues/17958)
- 📋 Text selection copying from the chat input box now correctly copies only the selected text instead of the entire textbox content. [#17911](https://github.com/open-webui/open-webui/issues/17911)
- 🔍 Web search query logging now uses debug level instead of info level, preventing user search queries from appearing in production logs. [#17888](https://github.com/open-webui/open-webui/pull/17888)
- 📝 Debug print statements in middleware were removed to prevent excessive log pollution and respect configured logging levels. [#17943](https://github.com/open-webui/open-webui/issues/17943)
### Changed
- 🗄️ Milvus vector database dependency is updated from pymilvus 2.5.0 to 2.6.2, ensuring compatibility with newer Milvus versions but requiring users on older Milvus instances to either upgrade their database or manually downgrade the pymilvus package. [#18066](https://github.com/open-webui/open-webui/pull/18066)
## [0.6.32] - 2025-09-29
### Added
- 🗝️ Permission toggle for public sharing of notes was added, allowing note owners to quickly enable or disable public access from the note settings interface.
- ⚠️ A warning is now displayed in the user edit modal if conflicting group permissions are detected, helping administrators resolve access control ambiguities before saving changes.
- ⚡ JSON model import moved to backend processing for significant performance improvements when importing large model files. [#17871](https://github.com/open-webui/open-webui/pull/17871)
- ⚠️ Visual warnings for group permissions that display when a permission is disabled in a group but remains enabled in the default user role, clarifying inheritance behavior for administrators. [#17848](https://github.com/open-webui/open-webui/pull/17848)
- 🗄️ Milvus multi-tenancy mode using shared collections with resource ID filtering for improved scalability, mirroring the existing Qdrant implementation and configurable via ENABLE_MILVUS_MULTITENANCY_MODE environment variable. [#17837](https://github.com/open-webui/open-webui/pull/17837)
- 🛠️ Enhanced tool result processing with improved error handling, better MCP tool result handling, and performance improvements for embedded UI components. [Commit](https://github.com/open-webui/open-webui/commit/4f06f29348b2c9d71c87d1bbe5b748a368f5101f)
- 👥 New user groups now automatically inherit default group permissions, streamlining the admin setup process by eliminating manual permission configuration. [#17843](https://github.com/open-webui/open-webui/pull/17843)
- 🗂️ Bulk unarchive functionality for all chats, providing a single backend endpoint to efficiently restore all archived chats at once. [#17857](https://github.com/open-webui/open-webui/pull/17857)
- 🏷️ Browser tab title toggle setting allows users to control whether chat titles appear in the browser tab or display only "Open WebUI". [#17851](https://github.com/open-webui/open-webui/pull/17851)
- 💬 Reply-to-message functionality in channels, allowing users to reply directly to specific messages with visual threading and context display. [Commit](https://github.com/open-webui/open-webui/commit/1a18928c94903ad1f1f0391b8ade042c3e60205b)
- 🔧 Tool server import and export functionality, allowing direct upload of openapi.json and openapi.yaml files as an alternative to URL-based configuration. [#14446](https://github.com/open-webui/open-webui/issues/14446)
- 🔧 User valve configuration for Functions is now available in the integration menu, providing consistent management alongside Tools. [#17784](https://github.com/open-webui/open-webui/issues/17784)
- 🔐 Admin permission toggle for controlling public sharing of notes, configurable via USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING environment variable. [#17801](https://github.com/open-webui/open-webui/pull/17801), [Docs:#715](https://github.com/open-webui/docs/pull/715)
- 🗄️ DISKANN index type support for Milvus vector database with configurable maximum degree and search list size parameters. [#17770](https://github.com/open-webui/open-webui/pull/17770), [Docs:Commit](https://github.com/open-webui/docs/commit/cec50ab4d4b659558ca1ccd4b5e6fc024f05fb83)
- 🔄 Various improvements were implemented across the frontend and backend to enhance performance, stability, and security.
- 🌐 Translations for Chinese (Simplified & Traditional) and Bosnian (Latin) were enhanced and expanded.
### Fixed
- 🧰 Fixed regression where External Tool servers (OpenAPI) were nonfunctional after the 0.6.31 update; external tools integration is now restored and reliable.
- 🚑 Resolved a critical bug causing Streamable HTTP OAuth 2.1 (MCP server) integrations to throw a 500 error on first invocation due to missing 'SessionMiddleware'. OAuth 2.1 registration now succeeds and works on subsequent requests as expected.
- 🐛 The "Set as default" option is now reliably clickable in model and filter selection menus, fixing cases where the interface appeared unresponsive.
- 🛠️ Embed UI now works seamlessly with both default and native function calling flows, ensuring the tool embedding experience is consistent regardless of invocation method.
- 🧹 Addressed various minor UI bugs and inconsistencies for a cleaner user experience.
### Changed
- 🧬 MCP tool result handling code was refactored for improved parsing and robustness of tool outputs.
- 🧩 The user edit modal was overhauled for clarity and usability, improving the organization of group, permission, and public sharing controls.
- 🛠️ MCP tool calls are now correctly routed to the appropriate server when multiple streamable-http MCP servers are enabled, preventing "Tool not found" errors. [#17817](https://github.com/open-webui/open-webui/issues/17817)
- 🛠️ External tool servers (OpenAPI/MCP) now properly process and return tool results to the model, restoring functionality that was broken in v0.6.31. [#17764](https://github.com/open-webui/open-webui/issues/17764)
- 🔧 User valve detection now correctly identifies valves in imported tool code, ensuring gear icons appear in the integrations menu for all tools with user valves. [#17765](https://github.com/open-webui/open-webui/issues/17765)
- 🔐 MCP OAuth discovery now correctly handles multi-tenant configurations by including subpaths in metadata URL discovery. [#17768](https://github.com/open-webui/open-webui/issues/17768)
- 🗄️ Milvus query operations now correctly use -1 instead of None for unlimited queries, preventing TypeError exceptions. [#17769](https://github.com/open-webui/open-webui/pull/17769), [#17088](https://github.com/open-webui/open-webui/issues/17088)
- 📁 File upload error messages are now displayed when files are modified during upload, preventing user confusion on Android and Windows devices. [#17777](https://github.com/open-webui/open-webui/pull/17777)
- 🎨 MessageInput Integrations button hover effect now displays correctly with proper visual feedback. [#17767](https://github.com/open-webui/open-webui/pull/17767)
- 🎯 "Set as default" label positioning is fixed to ensure it remains clickable in all scenarios, including multi-model configurations. [#17779](https://github.com/open-webui/open-webui/pull/17779)
- 🎛️ Floating buttons now correctly retrieve message context by using the proper messageId parameter in createMessagesList calls. [#17823](https://github.com/open-webui/open-webui/pull/17823)
- 📌 Pinned chats are now properly cleared from the sidebar after archiving all chats, ensuring UI consistency without requiring a page refresh. [#17832](https://github.com/open-webui/open-webui/pull/17832)
- 🗑️ Delete confirmation modals now properly truncate long names for Notes, Prompts, Tools, and Functions to prevent modal overflow. [#17812](https://github.com/open-webui/open-webui/pull/17812)
- 🌐 Internationalization function calls now use proper Svelte store subscription syntax, preventing "i18n.t is not a function" errors on the model creation page. [#17819](https://github.com/open-webui/open-webui/pull/17819)
- 🎨 Playground chat interface button layout is corrected to prevent vertical text rendering for Assistant/User role buttons. [#17819](https://github.com/open-webui/open-webui/pull/17819)
- 🏷️ UI text truncation is improved across multiple components including usernames in admin panels, arena model names, model tags, and filter tags to prevent layout overflow issues. [#17805](https://github.com/open-webui/open-webui/pull/17805), [#17803](https://github.com/open-webui/open-webui/pull/17803), [#17791](https://github.com/open-webui/open-webui/pull/17791), [#17796](https://github.com/open-webui/open-webui/pull/17796)
## [0.6.31] - 2025-09-25
+1 -38
View File
@@ -17,7 +17,7 @@ Passionate about open-source AI? [Join our team →](https://careers.openwebui.c
![Open WebUI Demo](./demo.gif)
> [!TIP]
> **Looking for an [Enterprise Plan](https://docs.openwebui.com/enterprise)?** – **[Speak with Our Sales Team Today!](mailto:sales@openwebui.com)**
> **Looking for an [Enterprise Plan](https://docs.openwebui.com/enterprise)?** – **[Speak with Our Sales Team Today!](https://docs.openwebui.com/enterprise)**
>
> Get **enhanced capabilities**, including **custom theming and branding**, **Service Level Agreement (SLA) support**, **Long-Term Support (LTS) versions**, and **more!**
@@ -65,43 +65,6 @@ For more information, be sure to check out our [Open WebUI Documentation](https:
Want to learn more about Open WebUI's features? Check out our [Open WebUI documentation](https://docs.openwebui.com/features) for a comprehensive overview!
## Sponsors 🙌
#### Emerald
<table>
<!-- <tr>
<td>
<a href="https://n8n.io/" target="_blank">
<img src="https://docs.openwebui.com/sponsors/logos/n8n.png" alt="n8n" style="width: 8rem; height: 8rem; border-radius: .75rem;" />
</a>
</td>
<td>
<a href="https://n8n.io/">n8n</a> • Does your interface have a backend yet?<br>Try <a href="https://n8n.io/">n8n</a>
</td>
</tr> -->
<tr>
<td>
<a href="https://tailscale.com/blog/self-host-a-local-ai-stack/?utm_source=OpenWebUI&utm_medium=paid-ad-placement&utm_campaign=OpenWebUI-Docs" target="_blank">
<img src="https://docs.openwebui.com/sponsors/logos/tailscale.png" alt="Tailscale" style="width: 8rem; height: 8rem; border-radius: .75rem;" />
</a>
</td>
<td>
<a href="https://tailscale.com/blog/self-host-a-local-ai-stack/?utm_source=OpenWebUI&utm_medium=paid-ad-placement&utm_campaign=OpenWebUI-Docs">Tailscale</a> • Connect self-hosted AI to any device with Tailscale
</td>
</tr>
<tr>
<td>
<a href="https://warp.dev/open-webui" target="_blank">
<img src="https://docs.openwebui.com/sponsors/logos/warp.png" alt="Warp" style="width: 8rem; height: 8rem; border-radius: .75rem;" />
</a>
</td>
<td>
<a href="https://warp.dev/open-webui">Warp</a> • The intelligent terminal for developers
</td>
</tr>
</table>
---
We are incredibly grateful for the generous support of our sponsors. Their contributions help us to maintain and improve our project, ensuring we can continue to deliver quality work to our community. Thank you!
+211 -53
View File
@@ -307,9 +307,15 @@ API_KEY_ALLOWED_ENDPOINTS = PersistentConfig(
JWT_EXPIRES_IN = PersistentConfig(
"JWT_EXPIRES_IN", "auth.jwt_expiry", os.environ.get("JWT_EXPIRES_IN", "-1")
"JWT_EXPIRES_IN", "auth.jwt_expiry", os.environ.get("JWT_EXPIRES_IN", "4w")
)
if JWT_EXPIRES_IN.value == "-1":
log.warning(
"⚠️ SECURITY WARNING: JWT_EXPIRES_IN is set to '-1'\n"
" See: https://docs.openwebui.com/getting-started/env-configuration\n"
)
####################################
# OAuth config
####################################
@@ -570,19 +576,26 @@ OAUTH_ROLES_CLAIM = PersistentConfig(
os.environ.get("OAUTH_ROLES_CLAIM", "roles"),
)
SEP = os.environ.get("OAUTH_ROLES_SEPARATOR", ",")
OAUTH_ALLOWED_ROLES = PersistentConfig(
"OAUTH_ALLOWED_ROLES",
"oauth.allowed_roles",
[
role.strip()
for role in os.environ.get("OAUTH_ALLOWED_ROLES", "user,admin").split(",")
for role in os.environ.get("OAUTH_ALLOWED_ROLES", f"user{SEP}admin").split(SEP)
if role
],
)
OAUTH_ADMIN_ROLES = PersistentConfig(
"OAUTH_ADMIN_ROLES",
"oauth.admin_roles",
[role.strip() for role in os.environ.get("OAUTH_ADMIN_ROLES", "admin").split(",")],
[
role.strip()
for role in os.environ.get("OAUTH_ADMIN_ROLES", "admin").split(SEP)
if role
],
)
OAUTH_ALLOWED_DOMAINS = PersistentConfig(
@@ -605,8 +618,8 @@ def load_oauth_providers():
OAUTH_PROVIDERS.clear()
if GOOGLE_CLIENT_ID.value and GOOGLE_CLIENT_SECRET.value:
def google_oauth_register(client: OAuth):
client.register(
def google_oauth_register(oauth: OAuth):
client = oauth.register(
name="google",
client_id=GOOGLE_CLIENT_ID.value,
client_secret=GOOGLE_CLIENT_SECRET.value,
@@ -621,6 +634,7 @@ def load_oauth_providers():
},
redirect_uri=GOOGLE_REDIRECT_URI.value,
)
return client
OAUTH_PROVIDERS["google"] = {
"redirect_uri": GOOGLE_REDIRECT_URI.value,
@@ -633,8 +647,8 @@ def load_oauth_providers():
and MICROSOFT_CLIENT_TENANT_ID.value
):
def microsoft_oauth_register(client: OAuth):
client.register(
def microsoft_oauth_register(oauth: OAuth):
client = oauth.register(
name="microsoft",
client_id=MICROSOFT_CLIENT_ID.value,
client_secret=MICROSOFT_CLIENT_SECRET.value,
@@ -649,6 +663,7 @@ def load_oauth_providers():
},
redirect_uri=MICROSOFT_REDIRECT_URI.value,
)
return client
OAUTH_PROVIDERS["microsoft"] = {
"redirect_uri": MICROSOFT_REDIRECT_URI.value,
@@ -658,8 +673,8 @@ def load_oauth_providers():
if GITHUB_CLIENT_ID.value and GITHUB_CLIENT_SECRET.value:
def github_oauth_register(client: OAuth):
client.register(
def github_oauth_register(oauth: OAuth):
client = oauth.register(
name="github",
client_id=GITHUB_CLIENT_ID.value,
client_secret=GITHUB_CLIENT_SECRET.value,
@@ -677,6 +692,7 @@ def load_oauth_providers():
},
redirect_uri=GITHUB_CLIENT_REDIRECT_URI.value,
)
return client
OAUTH_PROVIDERS["github"] = {
"redirect_uri": GITHUB_CLIENT_REDIRECT_URI.value,
@@ -690,7 +706,7 @@ def load_oauth_providers():
and OPENID_PROVIDER_URL.value
):
def oidc_oauth_register(client: OAuth):
def oidc_oauth_register(oauth: OAuth):
client_kwargs = {
"scope": OAUTH_SCOPES.value,
**(
@@ -716,7 +732,7 @@ def load_oauth_providers():
% ("S256", OAUTH_CODE_CHALLENGE_METHOD.value)
)
client.register(
client = oauth.register(
name="oidc",
client_id=OAUTH_CLIENT_ID.value,
client_secret=OAUTH_CLIENT_SECRET.value,
@@ -724,6 +740,7 @@ def load_oauth_providers():
client_kwargs=client_kwargs,
redirect_uri=OPENID_REDIRECT_URI.value,
)
return client
OAUTH_PROVIDERS["oidc"] = {
"name": OAUTH_PROVIDER_NAME.value,
@@ -733,8 +750,8 @@ def load_oauth_providers():
if FEISHU_CLIENT_ID.value and FEISHU_CLIENT_SECRET.value:
def feishu_oauth_register(client: OAuth):
client.register(
def feishu_oauth_register(oauth: OAuth):
client = oauth.register(
name="feishu",
client_id=FEISHU_CLIENT_ID.value,
client_secret=FEISHU_CLIENT_SECRET.value,
@@ -752,6 +769,7 @@ def load_oauth_providers():
},
redirect_uri=FEISHU_REDIRECT_URI.value,
)
return client
OAUTH_PROVIDERS["feishu"] = {
"register": feishu_oauth_register,
@@ -2018,7 +2036,7 @@ MILVUS_DISKANN_SEARCH_LIST_SIZE = int(
os.environ.get("MILVUS_DISKANN_SEARCH_LIST_SIZE", "100")
)
ENABLE_MILVUS_MULTITENANCY_MODE = (
os.environ.get("ENABLE_MILVUS_MULTITENANCY_MODE", "true").lower() == "true"
os.environ.get("ENABLE_MILVUS_MULTITENANCY_MODE", "false").lower() == "true"
)
# Hyphens not allowed, need to use underscores in collection names
MILVUS_COLLECTION_PREFIX = os.environ.get("MILVUS_COLLECTION_PREFIX", "open_webui")
@@ -2286,6 +2304,36 @@ DATALAB_MARKER_OUTPUT_FORMAT = PersistentConfig(
os.environ.get("DATALAB_MARKER_OUTPUT_FORMAT", "markdown"),
)
MINERU_API_MODE = PersistentConfig(
"MINERU_API_MODE",
"rag.mineru_api_mode",
os.environ.get("MINERU_API_MODE", "local"), # "local" or "cloud"
)
MINERU_API_URL = PersistentConfig(
"MINERU_API_URL",
"rag.mineru_api_url",
os.environ.get("MINERU_API_URL", "http://localhost:8000"),
)
MINERU_API_KEY = PersistentConfig(
"MINERU_API_KEY",
"rag.mineru_api_key",
os.environ.get("MINERU_API_KEY", ""),
)
mineru_params = os.getenv("MINERU_PARAMS", "")
try:
mineru_params = json.loads(mineru_params)
except json.JSONDecodeError:
mineru_params = {}
MINERU_PARAMS = PersistentConfig(
"MINERU_PARAMS",
"rag.mineru_params",
mineru_params,
)
EXTERNAL_DOCUMENT_LOADER_URL = PersistentConfig(
"EXTERNAL_DOCUMENT_LOADER_URL",
"rag.external_document_loader_url",
@@ -2310,6 +2358,18 @@ DOCLING_SERVER_URL = PersistentConfig(
os.getenv("DOCLING_SERVER_URL", "http://docling:5001"),
)
docling_params = os.getenv("DOCLING_PARAMS", "")
try:
docling_params = json.loads(docling_params)
except json.JSONDecodeError:
docling_params = {}
DOCLING_PARAMS = PersistentConfig(
"DOCLING_PARAMS",
"rag.docling_params",
docling_params,
)
DOCLING_DO_OCR = PersistentConfig(
"DOCLING_DO_OCR",
"rag.docling_do_ocr",
@@ -2404,6 +2464,12 @@ DOCUMENT_INTELLIGENCE_KEY = PersistentConfig(
os.getenv("DOCUMENT_INTELLIGENCE_KEY", ""),
)
MISTRAL_OCR_API_BASE_URL = PersistentConfig(
"MISTRAL_OCR_API_BASE_URL",
"rag.MISTRAL_OCR_API_BASE_URL",
os.getenv("MISTRAL_OCR_API_BASE_URL", "https://api.mistral.ai/v1"),
)
MISTRAL_OCR_API_KEY = PersistentConfig(
"MISTRAL_OCR_API_KEY",
"rag.mistral_ocr_api_key",
@@ -3014,16 +3080,30 @@ EXTERNAL_WEB_LOADER_API_KEY = PersistentConfig(
# Images
####################################
ENABLE_IMAGE_GENERATION = PersistentConfig(
"ENABLE_IMAGE_GENERATION",
"image_generation.enable",
os.environ.get("ENABLE_IMAGE_GENERATION", "").lower() == "true",
)
IMAGE_GENERATION_ENGINE = PersistentConfig(
"IMAGE_GENERATION_ENGINE",
"image_generation.engine",
os.getenv("IMAGE_GENERATION_ENGINE", "openai"),
)
ENABLE_IMAGE_GENERATION = PersistentConfig(
"ENABLE_IMAGE_GENERATION",
"image_generation.enable",
os.environ.get("ENABLE_IMAGE_GENERATION", "").lower() == "true",
IMAGE_GENERATION_MODEL = PersistentConfig(
"IMAGE_GENERATION_MODEL",
"image_generation.model",
os.getenv("IMAGE_GENERATION_MODEL", ""),
)
IMAGE_SIZE = PersistentConfig(
"IMAGE_SIZE", "image_generation.size", os.getenv("IMAGE_SIZE", "512x512")
)
IMAGE_STEPS = PersistentConfig(
"IMAGE_STEPS", "image_generation.steps", int(os.getenv("IMAGE_STEPS", 50))
)
ENABLE_IMAGE_PROMPT_GENERATION = PersistentConfig(
@@ -3043,35 +3123,17 @@ AUTOMATIC1111_API_AUTH = PersistentConfig(
os.getenv("AUTOMATIC1111_API_AUTH", ""),
)
AUTOMATIC1111_CFG_SCALE = PersistentConfig(
"AUTOMATIC1111_CFG_SCALE",
"image_generation.automatic1111.cfg_scale",
(
float(os.environ.get("AUTOMATIC1111_CFG_SCALE"))
if os.environ.get("AUTOMATIC1111_CFG_SCALE")
else None
),
)
automatic1111_params = os.getenv("AUTOMATIC1111_PARAMS", "")
try:
automatic1111_params = json.loads(automatic1111_params)
except json.JSONDecodeError:
automatic1111_params = {}
AUTOMATIC1111_SAMPLER = PersistentConfig(
"AUTOMATIC1111_SAMPLER",
"image_generation.automatic1111.sampler",
(
os.environ.get("AUTOMATIC1111_SAMPLER")
if os.environ.get("AUTOMATIC1111_SAMPLER")
else None
),
)
AUTOMATIC1111_SCHEDULER = PersistentConfig(
"AUTOMATIC1111_SCHEDULER",
"image_generation.automatic1111.scheduler",
(
os.environ.get("AUTOMATIC1111_SCHEDULER")
if os.environ.get("AUTOMATIC1111_SCHEDULER")
else None
),
AUTOMATIC1111_PARAMS = PersistentConfig(
"AUTOMATIC1111_PARAMS",
"image_generation.automatic1111.api_auth",
automatic1111_params,
)
COMFYUI_BASE_URL = PersistentConfig(
@@ -3237,18 +3299,79 @@ IMAGES_GEMINI_API_KEY = PersistentConfig(
os.getenv("IMAGES_GEMINI_API_KEY", GEMINI_API_KEY),
)
IMAGE_SIZE = PersistentConfig(
"IMAGE_SIZE", "image_generation.size", os.getenv("IMAGE_SIZE", "512x512")
IMAGES_GEMINI_ENDPOINT_METHOD = PersistentConfig(
"IMAGES_GEMINI_ENDPOINT_METHOD",
"image_generation.gemini.endpoint_method",
os.getenv("IMAGES_GEMINI_ENDPOINT_METHOD", ""),
)
IMAGE_STEPS = PersistentConfig(
"IMAGE_STEPS", "image_generation.steps", int(os.getenv("IMAGE_STEPS", 50))
IMAGE_EDIT_ENGINE = PersistentConfig(
"IMAGE_EDIT_ENGINE",
"images.edit.engine",
os.getenv("IMAGE_EDIT_ENGINE", "openai"),
)
IMAGE_GENERATION_MODEL = PersistentConfig(
"IMAGE_GENERATION_MODEL",
"image_generation.model",
os.getenv("IMAGE_GENERATION_MODEL", ""),
IMAGE_EDIT_MODEL = PersistentConfig(
"IMAGE_EDIT_MODEL",
"images.edit.model",
os.getenv("IMAGE_EDIT_MODEL", ""),
)
IMAGE_EDIT_SIZE = PersistentConfig(
"IMAGE_EDIT_SIZE", "images.edit.size", os.getenv("IMAGE_EDIT_SIZE", "")
)
IMAGES_EDIT_OPENAI_API_BASE_URL = PersistentConfig(
"IMAGES_EDIT_OPENAI_API_BASE_URL",
"images.edit.openai.api_base_url",
os.getenv("IMAGES_EDIT_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
)
IMAGES_EDIT_OPENAI_API_VERSION = PersistentConfig(
"IMAGES_EDIT_OPENAI_API_VERSION",
"images.edit.openai.api_version",
os.getenv("IMAGES_EDIT_OPENAI_API_VERSION", ""),
)
IMAGES_EDIT_OPENAI_API_KEY = PersistentConfig(
"IMAGES_EDIT_OPENAI_API_KEY",
"images.edit.openai.api_key",
os.getenv("IMAGES_EDIT_OPENAI_API_KEY", OPENAI_API_KEY),
)
IMAGES_EDIT_GEMINI_API_BASE_URL = PersistentConfig(
"IMAGES_EDIT_GEMINI_API_BASE_URL",
"images.edit.gemini.api_base_url",
os.getenv("IMAGES_EDIT_GEMINI_API_BASE_URL", GEMINI_API_BASE_URL),
)
IMAGES_EDIT_GEMINI_API_KEY = PersistentConfig(
"IMAGES_EDIT_GEMINI_API_KEY",
"images.edit.gemini.api_key",
os.getenv("IMAGES_EDIT_GEMINI_API_KEY", GEMINI_API_KEY),
)
IMAGES_EDIT_COMFYUI_BASE_URL = PersistentConfig(
"IMAGES_EDIT_COMFYUI_BASE_URL",
"images.edit.comfyui.base_url",
os.getenv("IMAGES_EDIT_COMFYUI_BASE_URL", ""),
)
IMAGES_EDIT_COMFYUI_API_KEY = PersistentConfig(
"IMAGES_EDIT_COMFYUI_API_KEY",
"images.edit.comfyui.api_key",
os.getenv("IMAGES_EDIT_COMFYUI_API_KEY", ""),
)
IMAGES_EDIT_COMFYUI_WORKFLOW = PersistentConfig(
"IMAGES_EDIT_COMFYUI_WORKFLOW",
"images.edit.comfyui.workflow",
os.getenv("IMAGES_EDIT_COMFYUI_WORKFLOW", ""),
)
IMAGES_EDIT_COMFYUI_WORKFLOW_NODES = PersistentConfig(
"IMAGES_EDIT_COMFYUI_WORKFLOW_NODES",
"images.edit.comfyui.nodes",
[],
)
####################################
@@ -3283,6 +3406,10 @@ DEEPGRAM_API_KEY = PersistentConfig(
os.getenv("DEEPGRAM_API_KEY", ""),
)
# ElevenLabs configuration
ELEVENLABS_API_BASE_URL = os.getenv(
"ELEVENLABS_API_BASE_URL", "https://api.elevenlabs.io"
)
AUDIO_STT_OPENAI_API_BASE_URL = PersistentConfig(
"AUDIO_STT_OPENAI_API_BASE_URL",
@@ -3350,6 +3477,24 @@ AUDIO_STT_AZURE_MAX_SPEAKERS = PersistentConfig(
os.getenv("AUDIO_STT_AZURE_MAX_SPEAKERS", ""),
)
AUDIO_STT_MISTRAL_API_KEY = PersistentConfig(
"AUDIO_STT_MISTRAL_API_KEY",
"audio.stt.mistral.api_key",
os.getenv("AUDIO_STT_MISTRAL_API_KEY", ""),
)
AUDIO_STT_MISTRAL_API_BASE_URL = PersistentConfig(
"AUDIO_STT_MISTRAL_API_BASE_URL",
"audio.stt.mistral.api_base_url",
os.getenv("AUDIO_STT_MISTRAL_API_BASE_URL", "https://api.mistral.ai/v1"),
)
AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS = PersistentConfig(
"AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS",
"audio.stt.mistral.use_chat_completions",
os.getenv("AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS", "false").lower() == "true",
)
AUDIO_TTS_OPENAI_API_BASE_URL = PersistentConfig(
"AUDIO_TTS_OPENAI_API_BASE_URL",
"audio.tts.openai.api_base_url",
@@ -3361,6 +3506,19 @@ AUDIO_TTS_OPENAI_API_KEY = PersistentConfig(
os.getenv("AUDIO_TTS_OPENAI_API_KEY", OPENAI_API_KEY),
)
audio_tts_openai_params = os.getenv("AUDIO_TTS_OPENAI_PARAMS", "")
try:
audio_tts_openai_params = json.loads(audio_tts_openai_params)
except json.JSONDecodeError:
audio_tts_openai_params = {}
AUDIO_TTS_OPENAI_PARAMS = PersistentConfig(
"AUDIO_TTS_OPENAI_PARAMS",
"audio.tts.openai.params",
audio_tts_openai_params,
)
AUDIO_TTS_API_KEY = PersistentConfig(
"AUDIO_TTS_API_KEY",
"audio.tts.api_key",
+1
View File
@@ -38,6 +38,7 @@ class ERROR_MESSAGES(str, Enum):
ID_TAKEN = "Uh-oh! This id is already registered. Please choose another id string."
MODEL_ID_TAKEN = "Uh-oh! This model id is already registered. Please choose another model id string."
NAME_TAG_TAKEN = "Uh-oh! This name tag is already registered. Please choose another name tag string."
MODEL_ID_TOO_LONG = "The model id is too long. Please make sure your model id is less than 256 characters long."
INVALID_TOKEN = (
"Your session has expired or the token is invalid. Please sign in again."
+8 -2
View File
@@ -212,6 +212,11 @@ ENABLE_FORWARD_USER_INFO_HEADERS = (
os.environ.get("ENABLE_FORWARD_USER_INFO_HEADERS", "False").lower() == "true"
)
# Experimental feature, may be removed in future
ENABLE_STAR_SESSIONS_MIDDLEWARE = (
os.environ.get("ENABLE_STAR_SESSIONS_MIDDLEWARE", "False").lower() == "true"
)
####################################
# WEBUI_BUILD_HASH
####################################
@@ -468,7 +473,9 @@ ENABLE_COMPRESSION_MIDDLEWARE = (
####################################
# OAUTH Configuration
####################################
ENABLE_OAUTH_EMAIL_FALLBACK = (
os.environ.get("ENABLE_OAUTH_EMAIL_FALLBACK", "False").lower() == "true"
)
ENABLE_OAUTH_ID_TOKEN_COOKIE = (
os.environ.get("ENABLE_OAUTH_ID_TOKEN_COOKIE", "True").lower() == "true"
@@ -482,7 +489,6 @@ OAUTH_SESSION_TOKEN_ENCRYPTION_KEY = os.environ.get(
"OAUTH_SESSION_TOKEN_ENCRYPTION_KEY", WEBUI_SECRET_KEY
)
####################################
# SCIM Configuration
####################################
+227 -41
View File
@@ -8,6 +8,7 @@ import shutil
import sys
import time
import random
import re
from uuid import uuid4
@@ -145,9 +146,7 @@ from open_webui.config import (
# Image
AUTOMATIC1111_API_AUTH,
AUTOMATIC1111_BASE_URL,
AUTOMATIC1111_CFG_SCALE,
AUTOMATIC1111_SAMPLER,
AUTOMATIC1111_SCHEDULER,
AUTOMATIC1111_PARAMS,
COMFYUI_BASE_URL,
COMFYUI_API_KEY,
COMFYUI_WORKFLOW,
@@ -163,6 +162,19 @@ from open_webui.config import (
IMAGES_OPENAI_API_KEY,
IMAGES_GEMINI_API_BASE_URL,
IMAGES_GEMINI_API_KEY,
IMAGES_GEMINI_ENDPOINT_METHOD,
IMAGE_EDIT_ENGINE,
IMAGE_EDIT_MODEL,
IMAGE_EDIT_SIZE,
IMAGES_EDIT_OPENAI_API_BASE_URL,
IMAGES_EDIT_OPENAI_API_KEY,
IMAGES_EDIT_OPENAI_API_VERSION,
IMAGES_EDIT_GEMINI_API_BASE_URL,
IMAGES_EDIT_GEMINI_API_KEY,
IMAGES_EDIT_COMFYUI_BASE_URL,
IMAGES_EDIT_COMFYUI_API_KEY,
IMAGES_EDIT_COMFYUI_WORKFLOW,
IMAGES_EDIT_COMFYUI_WORKFLOW_NODES,
# Audio
AUDIO_STT_ENGINE,
AUDIO_STT_MODEL,
@@ -174,13 +186,17 @@ from open_webui.config import (
AUDIO_STT_AZURE_LOCALES,
AUDIO_STT_AZURE_BASE_URL,
AUDIO_STT_AZURE_MAX_SPEAKERS,
AUDIO_TTS_API_KEY,
AUDIO_STT_MISTRAL_API_KEY,
AUDIO_STT_MISTRAL_API_BASE_URL,
AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS,
AUDIO_TTS_ENGINE,
AUDIO_TTS_MODEL,
AUDIO_TTS_VOICE,
AUDIO_TTS_OPENAI_API_BASE_URL,
AUDIO_TTS_OPENAI_API_KEY,
AUDIO_TTS_OPENAI_PARAMS,
AUDIO_TTS_API_KEY,
AUDIO_TTS_SPLIT_ON,
AUDIO_TTS_VOICE,
AUDIO_TTS_AZURE_SPEECH_REGION,
AUDIO_TTS_AZURE_SPEECH_BASE_URL,
AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT,
@@ -241,11 +257,16 @@ from open_webui.config import (
DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION,
DATALAB_MARKER_FORMAT_LINES,
DATALAB_MARKER_OUTPUT_FORMAT,
MINERU_API_MODE,
MINERU_API_URL,
MINERU_API_KEY,
MINERU_PARAMS,
DATALAB_MARKER_USE_LLM,
EXTERNAL_DOCUMENT_LOADER_URL,
EXTERNAL_DOCUMENT_LOADER_API_KEY,
TIKA_SERVER_URL,
DOCLING_SERVER_URL,
DOCLING_PARAMS,
DOCLING_DO_OCR,
DOCLING_FORCE_OCR,
DOCLING_OCR_ENGINE,
@@ -259,6 +280,7 @@ from open_webui.config import (
DOCLING_PICTURE_DESCRIPTION_API,
DOCUMENT_INTELLIGENCE_ENDPOINT,
DOCUMENT_INTELLIGENCE_KEY,
MISTRAL_OCR_API_BASE_URL,
MISTRAL_OCR_API_KEY,
RAG_TEXT_SPLITTER,
TIKTOKEN_ENCODING_NAME,
@@ -447,6 +469,7 @@ from open_webui.env import (
ENABLE_OTEL,
EXTERNAL_PWA_MANIFEST_URL,
AIOHTTP_CLIENT_SESSION_SSL,
ENABLE_STAR_SESSIONS_MIDDLEWARE,
)
@@ -474,9 +497,11 @@ from open_webui.utils.auth import (
)
from open_webui.utils.plugin import install_tool_and_function_dependencies
from open_webui.utils.oauth import (
get_oauth_client_info_with_dynamic_client_registration,
encrypt_data,
decrypt_data,
OAuthManager,
OAuthClientManager,
decrypt_data,
OAuthClientInformationFull,
)
from open_webui.utils.security_headers import SecurityHeadersMiddleware
@@ -834,6 +859,7 @@ app.state.config.EXTERNAL_DOCUMENT_LOADER_URL = EXTERNAL_DOCUMENT_LOADER_URL
app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY = EXTERNAL_DOCUMENT_LOADER_API_KEY
app.state.config.TIKA_SERVER_URL = TIKA_SERVER_URL
app.state.config.DOCLING_SERVER_URL = DOCLING_SERVER_URL
app.state.config.DOCLING_PARAMS = DOCLING_PARAMS
app.state.config.DOCLING_DO_OCR = DOCLING_DO_OCR
app.state.config.DOCLING_FORCE_OCR = DOCLING_FORCE_OCR
app.state.config.DOCLING_OCR_ENGINE = DOCLING_OCR_ENGINE
@@ -847,7 +873,12 @@ app.state.config.DOCLING_PICTURE_DESCRIPTION_LOCAL = DOCLING_PICTURE_DESCRIPTION
app.state.config.DOCLING_PICTURE_DESCRIPTION_API = DOCLING_PICTURE_DESCRIPTION_API
app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT = DOCUMENT_INTELLIGENCE_ENDPOINT
app.state.config.DOCUMENT_INTELLIGENCE_KEY = DOCUMENT_INTELLIGENCE_KEY
app.state.config.MISTRAL_OCR_API_BASE_URL = MISTRAL_OCR_API_BASE_URL
app.state.config.MISTRAL_OCR_API_KEY = MISTRAL_OCR_API_KEY
app.state.config.MINERU_API_MODE = MINERU_API_MODE
app.state.config.MINERU_API_URL = MINERU_API_URL
app.state.config.MINERU_API_KEY = MINERU_API_KEY
app.state.config.MINERU_PARAMS = MINERU_PARAMS
app.state.config.TEXT_SPLITTER = RAG_TEXT_SPLITTER
app.state.config.TIKTOKEN_ENCODING_NAME = TIKTOKEN_ENCODING_NAME
@@ -1049,27 +1080,40 @@ app.state.config.IMAGE_GENERATION_ENGINE = IMAGE_GENERATION_ENGINE
app.state.config.ENABLE_IMAGE_GENERATION = ENABLE_IMAGE_GENERATION
app.state.config.ENABLE_IMAGE_PROMPT_GENERATION = ENABLE_IMAGE_PROMPT_GENERATION
app.state.config.IMAGE_GENERATION_MODEL = IMAGE_GENERATION_MODEL
app.state.config.IMAGE_SIZE = IMAGE_SIZE
app.state.config.IMAGE_STEPS = IMAGE_STEPS
app.state.config.IMAGES_OPENAI_API_BASE_URL = IMAGES_OPENAI_API_BASE_URL
app.state.config.IMAGES_OPENAI_API_VERSION = IMAGES_OPENAI_API_VERSION
app.state.config.IMAGES_OPENAI_API_KEY = IMAGES_OPENAI_API_KEY
app.state.config.IMAGES_GEMINI_API_BASE_URL = IMAGES_GEMINI_API_BASE_URL
app.state.config.IMAGES_GEMINI_API_KEY = IMAGES_GEMINI_API_KEY
app.state.config.IMAGE_GENERATION_MODEL = IMAGE_GENERATION_MODEL
app.state.config.IMAGES_GEMINI_ENDPOINT_METHOD = IMAGES_GEMINI_ENDPOINT_METHOD
app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
app.state.config.AUTOMATIC1111_API_AUTH = AUTOMATIC1111_API_AUTH
app.state.config.AUTOMATIC1111_CFG_SCALE = AUTOMATIC1111_CFG_SCALE
app.state.config.AUTOMATIC1111_SAMPLER = AUTOMATIC1111_SAMPLER
app.state.config.AUTOMATIC1111_SCHEDULER = AUTOMATIC1111_SCHEDULER
app.state.config.AUTOMATIC1111_PARAMS = AUTOMATIC1111_PARAMS
app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
app.state.config.COMFYUI_API_KEY = COMFYUI_API_KEY
app.state.config.COMFYUI_WORKFLOW = COMFYUI_WORKFLOW
app.state.config.COMFYUI_WORKFLOW_NODES = COMFYUI_WORKFLOW_NODES
app.state.config.IMAGE_SIZE = IMAGE_SIZE
app.state.config.IMAGE_STEPS = IMAGE_STEPS
app.state.config.IMAGE_EDIT_ENGINE = IMAGE_EDIT_ENGINE
app.state.config.IMAGE_EDIT_MODEL = IMAGE_EDIT_MODEL
app.state.config.IMAGE_EDIT_SIZE = IMAGE_EDIT_SIZE
app.state.config.IMAGES_EDIT_OPENAI_API_BASE_URL = IMAGES_EDIT_OPENAI_API_BASE_URL
app.state.config.IMAGES_EDIT_OPENAI_API_KEY = IMAGES_EDIT_OPENAI_API_KEY
app.state.config.IMAGES_EDIT_OPENAI_API_VERSION = IMAGES_EDIT_OPENAI_API_VERSION
app.state.config.IMAGES_EDIT_GEMINI_API_BASE_URL = IMAGES_EDIT_GEMINI_API_BASE_URL
app.state.config.IMAGES_EDIT_GEMINI_API_KEY = IMAGES_EDIT_GEMINI_API_KEY
app.state.config.IMAGES_EDIT_COMFYUI_BASE_URL = IMAGES_EDIT_COMFYUI_BASE_URL
app.state.config.IMAGES_EDIT_COMFYUI_API_KEY = IMAGES_EDIT_COMFYUI_API_KEY
app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW = IMAGES_EDIT_COMFYUI_WORKFLOW
app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES = IMAGES_EDIT_COMFYUI_WORKFLOW_NODES
########################################
@@ -1095,11 +1139,21 @@ app.state.config.AUDIO_STT_AZURE_LOCALES = AUDIO_STT_AZURE_LOCALES
app.state.config.AUDIO_STT_AZURE_BASE_URL = AUDIO_STT_AZURE_BASE_URL
app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS = AUDIO_STT_AZURE_MAX_SPEAKERS
app.state.config.TTS_OPENAI_API_BASE_URL = AUDIO_TTS_OPENAI_API_BASE_URL
app.state.config.TTS_OPENAI_API_KEY = AUDIO_TTS_OPENAI_API_KEY
app.state.config.AUDIO_STT_MISTRAL_API_KEY = AUDIO_STT_MISTRAL_API_KEY
app.state.config.AUDIO_STT_MISTRAL_API_BASE_URL = AUDIO_STT_MISTRAL_API_BASE_URL
app.state.config.AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS = (
AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS
)
app.state.config.TTS_ENGINE = AUDIO_TTS_ENGINE
app.state.config.TTS_MODEL = AUDIO_TTS_MODEL
app.state.config.TTS_VOICE = AUDIO_TTS_VOICE
app.state.config.TTS_OPENAI_API_BASE_URL = AUDIO_TTS_OPENAI_API_BASE_URL
app.state.config.TTS_OPENAI_API_KEY = AUDIO_TTS_OPENAI_API_KEY
app.state.config.TTS_OPENAI_PARAMS = AUDIO_TTS_OPENAI_PARAMS
app.state.config.TTS_API_KEY = AUDIO_TTS_API_KEY
app.state.config.TTS_SPLIT_ON = AUDIO_TTS_SPLIT_ON
@@ -1170,12 +1224,32 @@ class RedirectMiddleware(BaseHTTPMiddleware):
path = request.url.path
query_params = dict(parse_qs(urlparse(str(request.url)).query))
redirect_params = {}
# Check for the specific watch path and the presence of 'v' parameter
if path.endswith("/watch") and "v" in query_params:
# Extract the first 'v' parameter
video_id = query_params["v"][0]
encoded_video_id = urlencode({"youtube": video_id})
redirect_url = f"/?{encoded_video_id}"
youtube_video_id = query_params["v"][0]
redirect_params["youtube"] = youtube_video_id
if "shared" in query_params and len(query_params["shared"]) > 0:
# PWA share_target support
text = query_params["shared"][0]
if text:
urls = re.match(r"https://\S+", text)
if urls:
from open_webui.retrieval.loaders.youtube import _parse_video_id
if youtube_video_id := _parse_video_id(urls[0]):
redirect_params["youtube"] = youtube_video_id
else:
redirect_params["load-url"] = urls[0]
else:
redirect_params["q"] = text
if redirect_params:
redirect_url = f"/?{urlencode(redirect_params)}"
return RedirectResponse(url=redirect_url)
# Proceed with the normal flow of other requests
@@ -1474,7 +1548,7 @@ async def chat_completion(
}
if metadata.get("chat_id") and (user and user.role != "admin"):
if metadata["chat_id"] != "local":
if not metadata["chat_id"].startswith("local:"):
chat = Chats.get_chat_by_id_and_user_id(metadata["chat_id"], user.id)
if chat is None:
raise HTTPException(
@@ -1501,13 +1575,14 @@ async def chat_completion(
response = await chat_completion_handler(request, form_data, user)
if metadata.get("chat_id") and metadata.get("message_id"):
try:
Chats.upsert_message_to_chat_by_id_and_message_id(
metadata["chat_id"],
metadata["message_id"],
{
"model": model_id,
},
)
if not metadata["chat_id"].startswith("local:"):
Chats.upsert_message_to_chat_by_id_and_message_id(
metadata["chat_id"],
metadata["message_id"],
{
"model": model_id,
},
)
except:
pass
@@ -1518,23 +1593,28 @@ async def chat_completion(
log.info("Chat processing was cancelled")
try:
event_emitter = get_event_emitter(metadata)
await event_emitter(
{"type": "chat:tasks:cancel"},
await asyncio.shield(
event_emitter(
{"type": "chat:tasks:cancel"},
)
)
except Exception as e:
pass
finally:
raise # re-raise to ensure proper task cancellation handling
except Exception as e:
log.debug(f"Error processing chat payload: {e}")
if metadata.get("chat_id") and metadata.get("message_id"):
# Update the chat message with the error
try:
Chats.upsert_message_to_chat_by_id_and_message_id(
metadata["chat_id"],
metadata["message_id"],
{
"error": {"content": str(e)},
},
)
if not metadata["chat_id"].startswith("local:"):
Chats.upsert_message_to_chat_by_id_and_message_id(
metadata["chat_id"],
metadata["message_id"],
{
"error": {"content": str(e)},
},
)
event_emitter = get_event_emitter(metadata)
await event_emitter(
@@ -1552,7 +1632,7 @@ async def chat_completion(
finally:
try:
if mcp_clients := metadata.get("mcp_clients"):
for client in mcp_clients.values():
for client in reversed(mcp_clients.values()):
await client.disconnect()
except Exception as e:
log.debug(f"Error cleaning up: {e}")
@@ -1898,18 +1978,26 @@ if len(app.state.config.TOOL_SERVER_CONNECTIONS) > 0:
if tool_server_connection.get("type", "openapi") == "mcp":
server_id = tool_server_connection.get("info", {}).get("id")
auth_type = tool_server_connection.get("auth_type", "none")
if server_id and auth_type == "oauth_2.1":
oauth_client_info = tool_server_connection.get("info", {}).get(
"oauth_client_info", ""
)
oauth_client_info = decrypt_data(oauth_client_info)
app.state.oauth_client_manager.add_client(
f"mcp:{server_id}", OAuthClientInformationFull(**oauth_client_info)
)
try:
oauth_client_info = decrypt_data(oauth_client_info)
app.state.oauth_client_manager.add_client(
f"mcp:{server_id}",
OAuthClientInformationFull(**oauth_client_info),
)
except Exception as e:
log.error(
f"Error adding OAuth client for MCP tool server {server_id}: {e}"
)
pass
try:
if REDIS_URL:
if ENABLE_STAR_SESSIONS_MIDDLEWARE:
redis_session_store = RedisStore(
url=REDIS_URL,
prefix=(f"{REDIS_KEY_PREFIX}:session:" if REDIS_KEY_PREFIX else "session:"),
@@ -1936,6 +2024,64 @@ except Exception as e:
)
async def register_client(self, request, client_id: str) -> bool:
server_type, server_id = client_id.split(":", 1)
connection = None
connection_idx = None
for idx, conn in enumerate(request.app.state.config.TOOL_SERVER_CONNECTIONS or []):
if conn.get("type", "openapi") == server_type:
info = conn.get("info", {})
if info.get("id") == server_id:
connection = conn
connection_idx = idx
break
if connection is None or connection_idx is None:
log.warning(
f"Unable to locate MCP tool server configuration for client {client_id} during re-registration"
)
return False
server_url = connection.get("url")
oauth_server_key = (connection.get("config") or {}).get("oauth_server_key")
try:
oauth_client_info = (
await get_oauth_client_info_with_dynamic_client_registration(
request,
client_id,
server_url,
oauth_server_key,
)
)
except Exception as e:
log.error(f"Dynamic client re-registration failed for {client_id}: {e}")
return False
try:
request.app.state.config.TOOL_SERVER_CONNECTIONS[connection_idx] = {
**connection,
"info": {
**connection.get("info", {}),
"oauth_client_info": encrypt_data(
oauth_client_info.model_dump(mode="json")
),
},
}
except Exception as e:
log.error(
f"Failed to persist updated OAuth client info for tool server {client_id}: {e}"
)
return False
oauth_client_manager.remove_client(client_id)
oauth_client_manager.add_client(client_id, oauth_client_info)
log.info(f"Re-registered OAuth client {client_id} for tool server")
return True
@app.get("/oauth/clients/{client_id}/authorize")
async def oauth_client_authorize(
client_id: str,
@@ -1943,6 +2089,41 @@ async def oauth_client_authorize(
response: Response,
user=Depends(get_verified_user),
):
# ensure_valid_client_registration
client = oauth_client_manager.get_client(client_id)
client_info = oauth_client_manager.get_client_info(client_id)
if client is None or client_info is None:
raise HTTPException(status.HTTP_404_NOT_FOUND)
if not await oauth_client_manager._preflight_authorization_url(client, client_info):
log.info(
"Detected invalid OAuth client %s; attempting re-registration",
client_id,
)
registered = await register_client(request, client_id)
if not registered:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to re-register OAuth client",
)
client = oauth_client_manager.get_client(client_id)
client_info = oauth_client_manager.get_client_info(client_id)
if client is None or client_info is None:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="OAuth client unavailable after re-registration",
)
if not await oauth_client_manager._preflight_authorization_url(
client, client_info
):
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="OAuth client registration is still invalid after re-registration",
)
return await oauth_client_manager.handle_authorize(request, client_id=client_id)
@@ -2004,6 +2185,11 @@ async def get_manifest_json():
"purpose": "maskable",
},
],
"share_target": {
"action": "/",
"method": "GET",
"params": {"text": "shared"},
},
}
+14 -4
View File
@@ -440,7 +440,10 @@ class ChatTable:
order_by = filter.get("order_by")
direction = filter.get("direction")
if order_by and direction and getattr(Chat, order_by):
if order_by and direction:
if not getattr(Chat, order_by, None):
raise ValueError("Invalid order_by field")
if direction.lower() == "asc":
query = query.order_by(getattr(Chat, order_by).asc())
elif direction.lower() == "desc":
@@ -502,6 +505,7 @@ class ChatTable:
user_id: str,
include_archived: bool = False,
include_folders: bool = False,
include_pinned: bool = False,
skip: Optional[int] = None,
limit: Optional[int] = None,
) -> list[ChatTitleIdResponse]:
@@ -511,7 +515,8 @@ class ChatTable:
if not include_folders:
query = query.filter_by(folder_id=None)
query = query.filter(or_(Chat.pinned == False, Chat.pinned == None))
if not include_pinned:
query = query.filter(or_(Chat.pinned == False, Chat.pinned == None))
if not include_archived:
query = query.filter_by(archived=False)
@@ -760,15 +765,20 @@ class ChatTable:
)
elif dialect_name == "postgresql":
# PostgreSQL relies on proper JSON query for search
# PostgreSQL doesn't allow null bytes in text. We filter those out by checking
# the JSON representation for \u0000 before attempting text extraction
postgres_content_sql = (
"EXISTS ("
" SELECT 1 "
" FROM json_array_elements(Chat.chat->'messages') AS message "
" WHERE LOWER(message->>'content') LIKE '%' || :content_key || '%'"
" WHERE message->'content' IS NOT NULL "
" AND (message->'content')::text NOT LIKE '%\\u0000%' "
" AND LOWER(message->>'content') LIKE '%' || :content_key || '%'"
")"
)
postgres_content_clause = text(postgres_content_sql)
# Also filter out chats with null bytes in title
query = query.filter(text("Chat.title::text NOT LIKE '%\\x00%'"))
query = query.filter(
or_(
Chat.title.ilike(bindparam("title_key")),
+6 -1
View File
@@ -82,6 +82,7 @@ class FileModelResponse(BaseModel):
class FileMetadataResponse(BaseModel):
id: str
hash: Optional[str] = None
meta: dict
created_at: int # timestamp in epoch
updated_at: int # timestamp in epoch
@@ -147,6 +148,7 @@ class FilesTable:
file = db.get(File, id)
return FileMetadataResponse(
id=file.id,
hash=file.hash,
meta=file.meta,
created_at=file.created_at,
updated_at=file.updated_at,
@@ -182,11 +184,14 @@ class FilesTable:
return [
FileMetadataResponse(
id=file.id,
hash=file.hash,
meta=file.meta,
created_at=file.created_at,
updated_at=file.updated_at,
)
for file in db.query(File)
for file in db.query(
File.id, File.hash, File.meta, File.created_at, File.updated_at
)
.filter(File.id.in_(ids))
.order_by(File.updated_at.desc())
.all()
+27 -1
View File
@@ -3,7 +3,7 @@ import time
from typing import Optional
from open_webui.internal.db import Base, JSONField, get_db
from open_webui.models.users import Users
from open_webui.models.users import Users, UserModel
from open_webui.env import SRC_LOG_LEVELS
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Boolean, Column, String, Text, Index
@@ -76,6 +76,10 @@ class FunctionWithValvesModel(BaseModel):
####################
class FunctionUserResponse(FunctionModel):
user: Optional[UserModel] = None
class FunctionResponse(BaseModel):
id: str
user_id: str
@@ -203,6 +207,28 @@ class FunctionsTable:
FunctionModel.model_validate(function) for function in functions
]
def get_function_list(self) -> list[FunctionUserResponse]:
with get_db() as db:
functions = db.query(Function).order_by(Function.updated_at.desc()).all()
user_ids = list(set(func.user_id for func in functions))
users = Users.get_users_by_user_ids(user_ids) if user_ids else []
users_dict = {user.id: user for user in users}
return [
FunctionUserResponse.model_validate(
{
**FunctionModel.model_validate(func).model_dump(),
"user": (
users_dict.get(func.user_id).model_dump()
if func.user_id in users_dict
else None
),
}
)
for func in functions
]
def get_functions_by_type(
self, type: str, active_only=False
) -> list[FunctionModel]:
@@ -262,5 +262,16 @@ class OAuthSessionTable:
log.error(f"Error deleting OAuth sessions by user ID: {e}")
return False
def delete_sessions_by_provider(self, provider: str) -> bool:
"""Delete all OAuth sessions for a provider"""
try:
with get_db() as db:
db.query(OAuthSession).filter_by(provider=provider).delete()
db.commit()
return True
except Exception as e:
log.error(f"Error deleting OAuth sessions by provider {provider}: {e}")
return False
OAuthSessions = OAuthSessionTable()
@@ -5,6 +5,7 @@ from urllib.parse import quote
from langchain_core.document_loaders import BaseLoader
from langchain_core.documents import Document
from open_webui.utils.headers import include_user_info_headers
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
@@ -18,6 +19,7 @@ class ExternalDocumentLoader(BaseLoader):
url: str,
api_key: str,
mime_type=None,
user=None,
**kwargs,
) -> None:
self.url = url
@@ -26,6 +28,8 @@ class ExternalDocumentLoader(BaseLoader):
self.file_path = file_path
self.mime_type = mime_type
self.user = user
def load(self) -> List[Document]:
with open(self.file_path, "rb") as f:
data = f.read()
@@ -42,6 +46,9 @@ class ExternalDocumentLoader(BaseLoader):
except:
pass
if self.user is not None:
headers = include_user_info_headers(headers, self.user)
url = self.url
if url.endswith("/"):
url = url[:-1]
+17 -14
View File
@@ -27,6 +27,7 @@ from open_webui.retrieval.loaders.external_document import ExternalDocumentLoade
from open_webui.retrieval.loaders.mistral import MistralLoader
from open_webui.retrieval.loaders.datalab_marker import DatalabMarkerLoader
from open_webui.retrieval.loaders.mineru import MinerULoader
from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL
@@ -227,6 +228,7 @@ class DoclingLoader:
class Loader:
def __init__(self, engine: str = "", **kwargs):
self.engine = engine
self.user = kwargs.get("user", None)
self.kwargs = kwargs
def load(
@@ -263,6 +265,7 @@ class Loader:
url=self.kwargs.get("EXTERNAL_DOCUMENT_LOADER_URL"),
api_key=self.kwargs.get("EXTERNAL_DOCUMENT_LOADER_API_KEY"),
mime_type=file_content_type,
user=self.user,
)
elif self.engine == "tika" and self.kwargs.get("TIKA_SERVER_URL"):
if self._is_text_file(file_ext, file_content_type):
@@ -271,7 +274,6 @@ class Loader:
loader = TikaLoader(
url=self.kwargs.get("TIKA_SERVER_URL"),
file_path=file_path,
mime_type=file_content_type,
extract_images=self.kwargs.get("PDF_EXTRACT_IMAGES"),
)
elif (
@@ -346,11 +348,9 @@ class Loader:
self.engine == "document_intelligence"
and self.kwargs.get("DOCUMENT_INTELLIGENCE_ENDPOINT") != ""
and (
file_ext in ["pdf", "xls", "xlsx", "docx", "ppt", "pptx"]
file_ext in ["pdf", "docx", "ppt", "pptx"]
or file_content_type
in [
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-powerpoint",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
@@ -369,6 +369,16 @@ class Loader:
api_endpoint=self.kwargs.get("DOCUMENT_INTELLIGENCE_ENDPOINT"),
azure_credential=DefaultAzureCredential(),
)
elif self.engine == "mineru" and file_ext in [
"pdf"
]: # MinerU currently only supports PDF
loader = MinerULoader(
file_path=file_path,
api_mode=self.kwargs.get("MINERU_API_MODE", "local"),
api_url=self.kwargs.get("MINERU_API_URL", "http://localhost:8000"),
api_key=self.kwargs.get("MINERU_API_KEY", ""),
params=self.kwargs.get("MINERU_PARAMS", {}),
)
elif (
self.engine == "mistral_ocr"
and self.kwargs.get("MISTRAL_OCR_API_KEY") != ""
@@ -376,16 +386,9 @@ class Loader:
in ["pdf"] # Mistral OCR currently only supports PDF and images
):
loader = MistralLoader(
api_key=self.kwargs.get("MISTRAL_OCR_API_KEY"), file_path=file_path
)
elif (
self.engine == "external"
and self.kwargs.get("MISTRAL_OCR_API_KEY") != ""
and file_ext
in ["pdf"] # Mistral OCR currently only supports PDF and images
):
loader = MistralLoader(
api_key=self.kwargs.get("MISTRAL_OCR_API_KEY"), file_path=file_path
base_url=self.kwargs.get("MISTRAL_OCR_API_BASE_URL"),
api_key=self.kwargs.get("MISTRAL_OCR_API_KEY"),
file_path=file_path,
)
else:
if file_ext == "pdf":
@@ -0,0 +1,541 @@
import os
import time
import requests
import logging
import tempfile
import zipfile
from typing import List, Optional
from langchain_core.documents import Document
from fastapi import HTTPException, status
log = logging.getLogger(__name__)
class MinerULoader:
"""
MinerU document parser loader supporting both Cloud API and Local API modes.
Cloud API: Uses MinerU managed service with async task-based processing
Local API: Uses self-hosted MinerU API with synchronous processing
"""
def __init__(
self,
file_path: str,
api_mode: str = "local",
api_url: str = "http://localhost:8000",
api_key: str = "",
params: dict = None,
):
self.file_path = file_path
self.api_mode = api_mode.lower()
self.api_url = api_url.rstrip("/")
self.api_key = api_key
# Parse params dict with defaults
params = params or {}
self.enable_ocr = params.get("enable_ocr", False)
self.enable_formula = params.get("enable_formula", True)
self.enable_table = params.get("enable_table", True)
self.language = params.get("language", "en")
self.model_version = params.get("model_version", "pipeline")
self.page_ranges = params.get("page_ranges", "")
# Validate API mode
if self.api_mode not in ["local", "cloud"]:
raise ValueError(
f"Invalid API mode: {self.api_mode}. Must be 'local' or 'cloud'"
)
# Validate Cloud API requirements
if self.api_mode == "cloud" and not self.api_key:
raise ValueError("API key is required for Cloud API mode")
def load(self) -> List[Document]:
"""
Main entry point for loading and parsing the document.
Routes to Cloud or Local API based on api_mode.
"""
try:
if self.api_mode == "cloud":
return self._load_cloud_api()
else:
return self._load_local_api()
except Exception as e:
log.error(f"Error loading document with MinerU: {e}")
raise
def _load_local_api(self) -> List[Document]:
"""
Load document using Local API (synchronous).
Posts file to /file_parse endpoint and gets immediate response.
"""
log.info(f"Using MinerU Local API at {self.api_url}")
filename = os.path.basename(self.file_path)
# Build form data for Local API
form_data = {
"return_md": "true",
"formula_enable": str(self.enable_formula).lower(),
"table_enable": str(self.enable_table).lower(),
}
# Parse method based on OCR setting
if self.enable_ocr:
form_data["parse_method"] = "ocr"
else:
form_data["parse_method"] = "auto"
# Language configuration (Local API uses lang_list array)
if self.language:
form_data["lang_list"] = self.language
# Backend/model version (Local API uses "backend" parameter)
if self.model_version == "vlm":
form_data["backend"] = "vlm-vllm-engine"
else:
form_data["backend"] = "pipeline"
# Page ranges (Local API uses start_page_id and end_page_id)
if self.page_ranges:
# For simplicity, if page_ranges is specified, log a warning
# Full page range parsing would require parsing the string
log.warning(
f"Page ranges '{self.page_ranges}' specified but Local API uses different format. "
"Consider using start_page_id/end_page_id parameters if needed."
)
try:
with open(self.file_path, "rb") as f:
files = {"files": (filename, f, "application/octet-stream")}
log.info(f"Sending file to MinerU Local API: {filename}")
log.debug(f"Local API parameters: {form_data}")
response = requests.post(
f"{self.api_url}/file_parse",
data=form_data,
files=files,
timeout=300, # 5 minute timeout for large documents
)
response.raise_for_status()
except FileNotFoundError:
raise HTTPException(
status.HTTP_404_NOT_FOUND, detail=f"File not found: {self.file_path}"
)
except requests.Timeout:
raise HTTPException(
status.HTTP_504_GATEWAY_TIMEOUT,
detail="MinerU Local API request timed out",
)
except requests.HTTPError as e:
error_detail = f"MinerU Local API request failed: {e}"
if e.response is not None:
try:
error_data = e.response.json()
error_detail += f" - {error_data}"
except:
error_detail += f" - {e.response.text}"
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=error_detail)
except Exception as e:
raise HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error calling MinerU Local API: {str(e)}",
)
# Parse response
try:
result = response.json()
except ValueError as e:
raise HTTPException(
status.HTTP_502_BAD_GATEWAY,
detail=f"Invalid JSON response from MinerU Local API: {e}",
)
# Extract markdown content from response
if "results" not in result:
raise HTTPException(
status.HTTP_502_BAD_GATEWAY,
detail="MinerU Local API response missing 'results' field",
)
results = result["results"]
if not results:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail="MinerU returned empty results",
)
# Get the first (and typically only) result
file_result = list(results.values())[0]
markdown_content = file_result.get("md_content", "")
if not markdown_content:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail="MinerU returned empty markdown content",
)
log.info(f"Successfully parsed document with MinerU Local API: {filename}")
# Create metadata
metadata = {
"source": filename,
"api_mode": "local",
"backend": result.get("backend", "unknown"),
"version": result.get("version", "unknown"),
}
return [Document(page_content=markdown_content, metadata=metadata)]
def _load_cloud_api(self) -> List[Document]:
"""
Load document using Cloud API (asynchronous).
Uses batch upload endpoint to avoid need for public file URLs.
"""
log.info(f"Using MinerU Cloud API at {self.api_url}")
filename = os.path.basename(self.file_path)
# Step 1: Request presigned upload URL
batch_id, upload_url = self._request_upload_url(filename)
# Step 2: Upload file to presigned URL
self._upload_to_presigned_url(upload_url)
# Step 3: Poll for results
result = self._poll_batch_status(batch_id, filename)
# Step 4: Download and extract markdown from ZIP
markdown_content = self._download_and_extract_zip(
result["full_zip_url"], filename
)
log.info(f"Successfully parsed document with MinerU Cloud API: {filename}")
# Create metadata
metadata = {
"source": filename,
"api_mode": "cloud",
"batch_id": batch_id,
}
return [Document(page_content=markdown_content, metadata=metadata)]
def _request_upload_url(self, filename: str) -> tuple:
"""
Request presigned upload URL from Cloud API.
Returns (batch_id, upload_url).
"""
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
}
# Build request body
request_body = {
"enable_formula": self.enable_formula,
"enable_table": self.enable_table,
"language": self.language,
"model_version": self.model_version,
"files": [
{
"name": filename,
"is_ocr": self.enable_ocr,
}
],
}
# Add page ranges if specified
if self.page_ranges:
request_body["files"][0]["page_ranges"] = self.page_ranges
log.info(f"Requesting upload URL for: {filename}")
log.debug(f"Cloud API request body: {request_body}")
try:
response = requests.post(
f"{self.api_url}/file-urls/batch",
headers=headers,
json=request_body,
timeout=30,
)
response.raise_for_status()
except requests.HTTPError as e:
error_detail = f"Failed to request upload URL: {e}"
if e.response is not None:
try:
error_data = e.response.json()
error_detail += f" - {error_data.get('msg', error_data)}"
except:
error_detail += f" - {e.response.text}"
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=error_detail)
except Exception as e:
raise HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error requesting upload URL: {str(e)}",
)
try:
result = response.json()
except ValueError as e:
raise HTTPException(
status.HTTP_502_BAD_GATEWAY,
detail=f"Invalid JSON response: {e}",
)
# Check for API error response
if result.get("code") != 0:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail=f"MinerU Cloud API error: {result.get('msg', 'Unknown error')}",
)
data = result.get("data", {})
batch_id = data.get("batch_id")
file_urls = data.get("file_urls", [])
if not batch_id or not file_urls:
raise HTTPException(
status.HTTP_502_BAD_GATEWAY,
detail="MinerU Cloud API response missing batch_id or file_urls",
)
upload_url = file_urls[0]
log.info(f"Received upload URL for batch: {batch_id}")
return batch_id, upload_url
def _upload_to_presigned_url(self, upload_url: str) -> None:
"""
Upload file to presigned URL (no authentication needed).
"""
log.info(f"Uploading file to presigned URL")
try:
with open(self.file_path, "rb") as f:
response = requests.put(
upload_url,
data=f,
timeout=300, # 5 minute timeout for large files
)
response.raise_for_status()
except FileNotFoundError:
raise HTTPException(
status.HTTP_404_NOT_FOUND, detail=f"File not found: {self.file_path}"
)
except requests.Timeout:
raise HTTPException(
status.HTTP_504_GATEWAY_TIMEOUT,
detail="File upload to presigned URL timed out",
)
except requests.HTTPError as e:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail=f"Failed to upload file to presigned URL: {e}",
)
except Exception as e:
raise HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error uploading file: {str(e)}",
)
log.info("File uploaded successfully")
def _poll_batch_status(self, batch_id: str, filename: str) -> dict:
"""
Poll batch status until completion.
Returns the result dict for the file.
"""
headers = {
"Authorization": f"Bearer {self.api_key}",
}
max_iterations = 300 # 10 minutes max (2 seconds per iteration)
poll_interval = 2 # seconds
log.info(f"Polling batch status: {batch_id}")
for iteration in range(max_iterations):
try:
response = requests.get(
f"{self.api_url}/extract-results/batch/{batch_id}",
headers=headers,
timeout=30,
)
response.raise_for_status()
except requests.HTTPError as e:
error_detail = f"Failed to poll batch status: {e}"
if e.response is not None:
try:
error_data = e.response.json()
error_detail += f" - {error_data.get('msg', error_data)}"
except:
error_detail += f" - {e.response.text}"
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=error_detail)
except Exception as e:
raise HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error polling batch status: {str(e)}",
)
try:
result = response.json()
except ValueError as e:
raise HTTPException(
status.HTTP_502_BAD_GATEWAY,
detail=f"Invalid JSON response while polling: {e}",
)
# Check for API error response
if result.get("code") != 0:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail=f"MinerU Cloud API error: {result.get('msg', 'Unknown error')}",
)
data = result.get("data", {})
extract_result = data.get("extract_result", [])
# Find our file in the batch results
file_result = None
for item in extract_result:
if item.get("file_name") == filename:
file_result = item
break
if not file_result:
raise HTTPException(
status.HTTP_502_BAD_GATEWAY,
detail=f"File {filename} not found in batch results",
)
state = file_result.get("state")
if state == "done":
log.info(f"Processing complete for {filename}")
return file_result
elif state == "failed":
error_msg = file_result.get("err_msg", "Unknown error")
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail=f"MinerU processing failed: {error_msg}",
)
elif state in ["waiting-file", "pending", "running", "converting"]:
# Still processing
if iteration % 10 == 0: # Log every 20 seconds
log.info(
f"Processing status: {state} (iteration {iteration + 1}/{max_iterations})"
)
time.sleep(poll_interval)
else:
log.warning(f"Unknown state: {state}")
time.sleep(poll_interval)
# Timeout
raise HTTPException(
status.HTTP_504_GATEWAY_TIMEOUT,
detail="MinerU processing timed out after 10 minutes",
)
def _download_and_extract_zip(self, zip_url: str, filename: str) -> str:
"""
Download ZIP file from CDN and extract markdown content.
Returns the markdown content as a string.
"""
log.info(f"Downloading results from: {zip_url}")
try:
response = requests.get(zip_url, timeout=60)
response.raise_for_status()
except requests.HTTPError as e:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail=f"Failed to download results ZIP: {e}",
)
except Exception as e:
raise HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error downloading results: {str(e)}",
)
# Save ZIP to temporary file and extract
try:
with tempfile.NamedTemporaryFile(delete=False, suffix=".zip") as tmp_zip:
tmp_zip.write(response.content)
tmp_zip_path = tmp_zip.name
with tempfile.TemporaryDirectory() as tmp_dir:
# Extract ZIP
with zipfile.ZipFile(tmp_zip_path, "r") as zip_ref:
zip_ref.extractall(tmp_dir)
# Find markdown file - search recursively for any .md file
markdown_content = None
found_md_path = None
# First, list all files in the ZIP for debugging
all_files = []
for root, dirs, files in os.walk(tmp_dir):
for file in files:
full_path = os.path.join(root, file)
all_files.append(full_path)
# Look for any .md file
if file.endswith(".md"):
found_md_path = full_path
log.info(f"Found markdown file at: {full_path}")
try:
with open(full_path, "r", encoding="utf-8") as f:
markdown_content = f.read()
if (
markdown_content
): # Use the first non-empty markdown file
break
except Exception as e:
log.warning(f"Failed to read {full_path}: {e}")
if markdown_content:
break
if markdown_content is None:
log.error(f"Available files in ZIP: {all_files}")
# Try to provide more helpful error message
md_files = [f for f in all_files if f.endswith(".md")]
if md_files:
error_msg = (
f"Found .md files but couldn't read them: {md_files}"
)
else:
error_msg = (
f"No .md files found in ZIP. Available files: {all_files}"
)
raise HTTPException(
status.HTTP_502_BAD_GATEWAY,
detail=error_msg,
)
# Clean up temporary ZIP file
os.unlink(tmp_zip_path)
except zipfile.BadZipFile as e:
raise HTTPException(
status.HTTP_502_BAD_GATEWAY,
detail=f"Invalid ZIP file received: {e}",
)
except Exception as e:
raise HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error extracting ZIP: {str(e)}",
)
if not markdown_content:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail="Extracted markdown content is empty",
)
log.info(
f"Successfully extracted markdown content ({len(markdown_content)} characters)"
)
return markdown_content
+12 -10
View File
@@ -30,10 +30,9 @@ class MistralLoader:
- Enhanced error handling with retryable error classification
"""
BASE_API_URL = "https://api.mistral.ai/v1"
def __init__(
self,
base_url: str,
api_key: str,
file_path: str,
timeout: int = 300, # 5 minutes default
@@ -55,6 +54,9 @@ class MistralLoader:
if not os.path.exists(file_path):
raise FileNotFoundError(f"File not found at {file_path}")
self.base_url = (
base_url.rstrip("/") if base_url else "https://api.mistral.ai/v1"
)
self.api_key = api_key
self.file_path = file_path
self.timeout = timeout
@@ -240,7 +242,7 @@ class MistralLoader:
in a context manager to minimize memory usage duration.
"""
log.info("Uploading file to Mistral API")
url = f"{self.BASE_API_URL}/files"
url = f"{self.base_url}/files"
def upload_request():
# MEMORY OPTIMIZATION: Use context manager to minimize file handle lifetime
@@ -275,7 +277,7 @@ class MistralLoader:
async def _upload_file_async(self, session: aiohttp.ClientSession) -> str:
"""Async file upload with streaming for better memory efficiency."""
url = f"{self.BASE_API_URL}/files"
url = f"{self.base_url}/files"
async def upload_request():
# Create multipart writer for streaming upload
@@ -321,7 +323,7 @@ class MistralLoader:
def _get_signed_url(self, file_id: str) -> str:
"""Retrieves a temporary signed URL for the uploaded file (sync version)."""
log.info(f"Getting signed URL for file ID: {file_id}")
url = f"{self.BASE_API_URL}/files/{file_id}/url"
url = f"{self.base_url}/files/{file_id}/url"
params = {"expiry": 1}
signed_url_headers = {**self.headers, "Accept": "application/json"}
@@ -346,7 +348,7 @@ class MistralLoader:
self, session: aiohttp.ClientSession, file_id: str
) -> str:
"""Async signed URL retrieval."""
url = f"{self.BASE_API_URL}/files/{file_id}/url"
url = f"{self.base_url}/files/{file_id}/url"
params = {"expiry": 1}
headers = {**self.headers, "Accept": "application/json"}
@@ -373,7 +375,7 @@ class MistralLoader:
def _process_ocr(self, signed_url: str) -> Dict[str, Any]:
"""Sends the signed URL to the OCR endpoint for processing (sync version)."""
log.info("Processing OCR via Mistral API")
url = f"{self.BASE_API_URL}/ocr"
url = f"{self.base_url}/ocr"
ocr_headers = {
**self.headers,
"Content-Type": "application/json",
@@ -407,7 +409,7 @@ class MistralLoader:
self, session: aiohttp.ClientSession, signed_url: str
) -> Dict[str, Any]:
"""Async OCR processing with timing metrics."""
url = f"{self.BASE_API_URL}/ocr"
url = f"{self.base_url}/ocr"
headers = {
**self.headers,
@@ -446,7 +448,7 @@ class MistralLoader:
def _delete_file(self, file_id: str) -> None:
"""Deletes the file from Mistral storage (sync version)."""
log.info(f"Deleting uploaded file ID: {file_id}")
url = f"{self.BASE_API_URL}/files/{file_id}"
url = f"{self.base_url}/files/{file_id}"
try:
response = requests.delete(
@@ -467,7 +469,7 @@ class MistralLoader:
async def delete_request():
self._debug_log(f"Deleting file ID: {file_id}")
async with session.delete(
url=f"{self.BASE_API_URL}/files/{file_id}",
url=f"{self.base_url}/files/{file_id}",
headers=self.headers,
timeout=aiohttp.ClientTimeout(
total=self.cleanup_timeout
@@ -83,6 +83,7 @@ class YoutubeLoader:
TranscriptsDisabled,
YouTubeTranscriptApi,
)
from youtube_transcript_api.proxies import GenericProxyConfig
except ImportError:
raise ImportError(
'Could not import "youtube_transcript_api" Python package. '
@@ -90,10 +91,9 @@ class YoutubeLoader:
)
if self.proxy_url:
youtube_proxies = {
"http": self.proxy_url,
"https": self.proxy_url,
}
youtube_proxies = GenericProxyConfig(
http_url=self.proxy_url, https_url=self.proxy_url
)
log.debug(f"Using proxy URL: {self.proxy_url[:14]}...")
else:
youtube_proxies = None
@@ -157,3 +157,10 @@ class YoutubeLoader:
f"No transcript found for any of the specified languages: {languages_tried}. Verify if the video has transcripts, add more languages if needed."
)
raise NoTranscriptFound(self.video_id, self.language, list(transcript_list))
async def aload(self) -> Generator[Document, None, None]:
"""Asynchronously load YouTube transcripts into `Document` objects."""
import asyncio
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, self.load)
+90 -35
View File
@@ -6,6 +6,7 @@ import requests
import hashlib
from concurrent.futures import ThreadPoolExecutor
import time
import re
from urllib.parse import quote
from huggingface_hub import snapshot_download
@@ -16,6 +17,7 @@ from langchain_core.documents import Document
from open_webui.config import VECTOR_DB
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
@@ -27,6 +29,9 @@ from open_webui.retrieval.vector.main import GetResult
from open_webui.utils.access_control import has_access
from open_webui.utils.misc import get_message_list
from open_webui.retrieval.web.utils import get_web_loader
from open_webui.retrieval.loaders.youtube import YoutubeLoader
from open_webui.env import (
SRC_LOG_LEVELS,
@@ -49,6 +54,34 @@ from langchain_core.callbacks import CallbackManagerForRetrieverRun
from langchain_core.retrievers import BaseRetriever
def is_youtube_url(url: str) -> bool:
youtube_regex = r"^(https?://)?(www\.)?(youtube\.com|youtu\.be)/.+$"
return re.match(youtube_regex, url) is not None
def get_loader(request, url: str):
if is_youtube_url(url):
return YoutubeLoader(
url,
language=request.app.state.config.YOUTUBE_LOADER_LANGUAGE,
proxy_url=request.app.state.config.YOUTUBE_LOADER_PROXY_URL,
)
else:
return get_web_loader(
url,
verify_ssl=request.app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION,
requests_per_second=request.app.state.config.WEB_LOADER_CONCURRENT_REQUESTS,
trust_env=request.app.state.config.WEB_SEARCH_TRUST_ENV,
)
def get_content_from_url(request, url: str) -> str:
loader = get_loader(request, url)
docs = loader.load()
content = " ".join([doc.page_content for doc in docs])
return content, docs
class VectorSearchRetriever(BaseRetriever):
collection_name: Any
embedding_function: Any
@@ -188,7 +221,11 @@ def query_doc_with_hybrid_search(
zip(distances, metadatas, documents), key=lambda x: x[0], reverse=True
)
sorted_items = sorted_items[:k]
distances, documents, metadatas = map(list, zip(*sorted_items))
if sorted_items:
distances, documents, metadatas = map(list, zip(*sorted_items))
else:
distances, documents, metadatas = [], [], []
result = {
"distances": [distances],
@@ -232,6 +269,13 @@ def merge_and_sort_query_results(query_results: list[dict], k: int) -> dict:
combined = dict() # To store documents with unique document hashes
for data in query_results:
if (
len(data.get("distances", [])) == 0
or len(data.get("documents", [])) == 0
or len(data.get("metadatas", [])) == 0
):
continue
distances = data["distances"][0]
documents = data["documents"][0]
metadatas = data["metadatas"][0]
@@ -571,6 +615,13 @@ def get_sources_from_items(
"metadatas": [[{"file_id": chat.id, "name": chat.title}]],
}
elif item.get("type") == "url":
content, docs = get_content_from_url(request, item.get("url"))
if docs:
query_result = {
"documents": [[content]],
"metadatas": [[{"url": item.get("url"), "name": item.get("url")}]],
}
elif item.get("type") == "file":
if (
item.get("context") == "full"
@@ -618,46 +669,51 @@ def get_sources_from_items(
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 knowledge_base.user_id == user.id
or has_access(user.id, "read", knowledge_base.access_control)
):
# Manual Full Mode Toggle for Collection
knowledge_base = Knowledges.get_knowledge_by_id(item.get("id"))
if knowledge_base and (
user.role == "admin"
or knowledge_base.user_id == user.id
or has_access(user.id, "read", knowledge_base.access_control)
if (
item.get("context") == "full"
or request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL
):
if knowledge_base and (
user.role == "admin"
or knowledge_base.user_id == user.id
or has_access(user.id, "read", knowledge_base.access_control)
):
file_ids = knowledge_base.data.get("file_ids", [])
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)
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,
}
)
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", [])
query_result = {
"documents": [documents],
"metadatas": [metadatas],
}
else:
collection_names.append(item["id"])
# 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
@@ -736,7 +792,6 @@ def get_sources_from_items(
sources.append(source)
except Exception as e:
log.exception(e)
return sources
@@ -717,7 +717,7 @@ class Oracle23aiClient(VectorDBBase):
)
try:
limit = limit or 1000
limit = 1000 # Hardcoded limit for get operation
with self.get_connection() as connection:
with connection.cursor() as cursor:
+9 -18
View File
@@ -1,11 +1,11 @@
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
from firecrawl import Firecrawl
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
@@ -18,27 +18,18 @@ def search_firecrawl(
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,
},
firecrawl = Firecrawl(api_key=firecrawl_api_key, api_url=firecrawl_url)
response = firecrawl.search(
query=query, limit=count, ignore_invalid_urls=True, timeout=count * 3
)
response.raise_for_status()
results = response.json().get("data", [])
results = response.web
if filter_list:
results = get_filtered_results(results, filter_list)
results = [
SearchResult(
link=result.get("url"),
title=result.get("title"),
snippet=result.get("description"),
link=result.url,
title=result.title,
snippet=result.description,
)
for result in results[:count]
]
@@ -15,6 +15,7 @@ def search_google_pse(
query: str,
count: int,
filter_list: Optional[list[str]] = None,
referer: Optional[str] = None,
) -> list[SearchResult]:
"""Search using Google's Programmable Search Engine API and return the results as a list of SearchResult objects.
Handles pagination for counts greater than 10.
@@ -30,7 +31,11 @@ def search_google_pse(
list[SearchResult]: A list of SearchResult objects.
"""
url = "https://www.googleapis.com/customsearch/v1"
headers = {"Content-Type": "application/json"}
if referer:
headers["Referer"] = referer
all_results = []
start_index = 1 # Google PSE start parameter is 1-based
+83 -47
View File
@@ -4,7 +4,6 @@ import socket
import ssl
import urllib.parse
import urllib.request
from collections import defaultdict
from datetime import datetime, time, timedelta
from typing import (
Any,
@@ -17,11 +16,12 @@ from typing import (
Union,
Literal,
)
from fastapi.concurrency import run_in_threadpool
import aiohttp
import certifi
import validators
from langchain_community.document_loaders import PlaywrightURLLoader, WebBaseLoader
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
@@ -39,7 +39,9 @@ from open_webui.config import (
EXTERNAL_WEB_LOADER_URL,
EXTERNAL_WEB_LOADER_API_KEY,
)
from open_webui.env import SRC_LOG_LEVELS, AIOHTTP_CLIENT_SESSION_SSL
from open_webui.env import SRC_LOG_LEVELS
from firecrawl import Firecrawl
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
@@ -75,7 +77,8 @@ def safe_validate_urls(url: Sequence[str]) -> Sequence[str]:
try:
if validate_url(u):
valid_urls.append(u)
except ValueError:
except Exception as e:
log.debug(f"Invalid URL {u}: {str(e)}")
continue
return valid_urls
@@ -141,13 +144,13 @@ class RateLimitMixin:
class URLProcessingMixin:
def _verify_ssl_cert(self, url: str) -> bool:
async def _verify_ssl_cert(self, url: str) -> bool:
"""Verify SSL certificate for a URL."""
return verify_ssl_cert(url)
return await run_in_threadpool(verify_ssl_cert, url)
async def _safe_process_url(self, url: str) -> bool:
"""Perform safety checks before processing a URL."""
if self.verify_ssl and not self._verify_ssl_cert(url):
if self.verify_ssl and not await self._verify_ssl_cert(url):
raise ValueError(f"SSL certificate verification failed for {url}")
await self._wait_for_rate_limit()
return True
@@ -188,13 +191,12 @@ class SafeFireCrawlLoader(BaseLoader, RateLimitMixin, URLProcessingMixin):
(uses FIRE_CRAWL_API_KEY environment variable if not provided).
api_url: Base URL for FireCrawl API. Defaults to official API endpoint.
mode: Operation mode selection:
- 'crawl': Website crawling mode (default)
- 'scrape': Direct page scraping
- 'crawl': Website crawling mode
- 'scrape': Direct page scraping (default)
- 'map': Site map generation
proxy: Proxy override settings for the FireCrawl API.
params: The parameters to pass to the Firecrawl API.
Examples include crawlerOptions.
For more details, visit: https://github.com/mendableai/firecrawl-py
For more details, visit: https://docs.firecrawl.dev/sdks/python#batch-scrape
"""
proxy_server = proxy.get("server") if proxy else None
if trust_env and not proxy_server:
@@ -214,50 +216,84 @@ class SafeFireCrawlLoader(BaseLoader, RateLimitMixin, URLProcessingMixin):
self.api_key = api_key
self.api_url = api_url
self.mode = mode
self.params = params
self.params = params or {}
def lazy_load(self) -> Iterator[Document]:
"""Load documents concurrently using FireCrawl."""
for url in self.web_paths:
try:
self._safe_process_url_sync(url)
loader = FireCrawlLoader(
url=url,
api_key=self.api_key,
api_url=self.api_url,
mode=self.mode,
params=self.params,
"""Load documents using FireCrawl batch_scrape."""
log.debug(
"Starting FireCrawl batch scrape for %d URLs, mode: %s, params: %s",
len(self.web_paths),
self.mode,
self.params,
)
try:
firecrawl = Firecrawl(api_key=self.api_key, api_url=self.api_url)
result = firecrawl.batch_scrape(
self.web_paths,
formats=["markdown"],
skip_tls_verification=not self.verify_ssl,
ignore_invalid_urls=True,
remove_base64_images=True,
max_age=300000, # 5 minutes https://docs.firecrawl.dev/features/fast-scraping#common-maxage-values
wait_timeout=len(self.web_paths) * 3,
**self.params,
)
if result.status != "completed":
raise RuntimeError(
f"FireCrawl batch scrape did not complete successfully. result: {result}"
)
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}")
continue
for data in result.data:
metadata = data.metadata or {}
yield Document(
page_content=data.markdown or "",
metadata={"source": metadata.url or metadata.source_url or ""},
)
except Exception as e:
if self.continue_on_failure:
log.exception(f"Error extracting content from URLs: {e}")
else:
raise e
async def alazy_load(self):
"""Async version of lazy_load."""
for url in self.web_paths:
try:
await self._safe_process_url(url)
loader = FireCrawlLoader(
url=url,
api_key=self.api_key,
api_url=self.api_url,
mode=self.mode,
params=self.params,
log.debug(
"Starting FireCrawl batch scrape for %d URLs, mode: %s, params: %s",
len(self.web_paths),
self.mode,
self.params,
)
try:
firecrawl = Firecrawl(api_key=self.api_key, api_url=self.api_url)
result = firecrawl.batch_scrape(
self.web_paths,
formats=["markdown"],
skip_tls_verification=not self.verify_ssl,
ignore_invalid_urls=True,
remove_base64_images=True,
max_age=300000, # 5 minutes https://docs.firecrawl.dev/features/fast-scraping#common-maxage-values
wait_timeout=len(self.web_paths) * 3,
**self.params,
)
if result.status != "completed":
raise RuntimeError(
f"FireCrawl batch scrape did not complete successfully. result: {result}"
)
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:
log.exception(f"Error loading {url}: {e}")
continue
for data in result.data:
metadata = data.metadata or {}
yield Document(
page_content=data.markdown or "",
metadata={"source": metadata.url or metadata.source_url or ""},
)
except Exception as e:
if self.continue_on_failure:
log.exception(f"Error extracting content from URLs: {e}")
else:
raise e
+216 -8
View File
@@ -3,6 +3,8 @@ import json
import logging
import os
import uuid
import html
import base64
from functools import lru_cache
from pydub import AudioSegment
from pydub.silence import split_on_silence
@@ -38,13 +40,14 @@ from open_webui.config import (
WHISPER_MODEL_DIR,
CACHE_DIR,
WHISPER_LANGUAGE,
ELEVENLABS_API_BASE_URL,
)
from open_webui.constants import ERROR_MESSAGES
from open_webui.env import (
ENV,
AIOHTTP_CLIENT_SESSION_SSL,
AIOHTTP_CLIENT_TIMEOUT,
ENV,
SRC_LOG_LEVELS,
DEVICE_TYPE,
ENABLE_FORWARD_USER_INFO_HEADERS,
@@ -153,6 +156,7 @@ def set_faster_whisper_model(model: str, auto_update: bool = False):
class TTSConfigForm(BaseModel):
OPENAI_API_BASE_URL: str
OPENAI_API_KEY: str
OPENAI_PARAMS: Optional[dict] = None
API_KEY: str
ENGINE: str
MODEL: str
@@ -176,6 +180,9 @@ class STTConfigForm(BaseModel):
AZURE_LOCALES: str
AZURE_BASE_URL: str
AZURE_MAX_SPEAKERS: str
MISTRAL_API_KEY: str
MISTRAL_API_BASE_URL: str
MISTRAL_USE_CHAT_COMPLETIONS: bool
class AudioConfigUpdateForm(BaseModel):
@@ -189,6 +196,7 @@ async def get_audio_config(request: Request, user=Depends(get_admin_user)):
"tts": {
"OPENAI_API_BASE_URL": request.app.state.config.TTS_OPENAI_API_BASE_URL,
"OPENAI_API_KEY": request.app.state.config.TTS_OPENAI_API_KEY,
"OPENAI_PARAMS": request.app.state.config.TTS_OPENAI_PARAMS,
"API_KEY": request.app.state.config.TTS_API_KEY,
"ENGINE": request.app.state.config.TTS_ENGINE,
"MODEL": request.app.state.config.TTS_MODEL,
@@ -211,6 +219,9 @@ async def get_audio_config(request: Request, user=Depends(get_admin_user)):
"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,
"MISTRAL_API_KEY": request.app.state.config.AUDIO_STT_MISTRAL_API_KEY,
"MISTRAL_API_BASE_URL": request.app.state.config.AUDIO_STT_MISTRAL_API_BASE_URL,
"MISTRAL_USE_CHAT_COMPLETIONS": request.app.state.config.AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS,
},
}
@@ -221,6 +232,7 @@ async def update_audio_config(
):
request.app.state.config.TTS_OPENAI_API_BASE_URL = form_data.tts.OPENAI_API_BASE_URL
request.app.state.config.TTS_OPENAI_API_KEY = form_data.tts.OPENAI_API_KEY
request.app.state.config.TTS_OPENAI_PARAMS = form_data.tts.OPENAI_PARAMS
request.app.state.config.TTS_API_KEY = form_data.tts.API_KEY
request.app.state.config.TTS_ENGINE = form_data.tts.ENGINE
request.app.state.config.TTS_MODEL = form_data.tts.MODEL
@@ -251,6 +263,13 @@ async def update_audio_config(
request.app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS = (
form_data.stt.AZURE_MAX_SPEAKERS
)
request.app.state.config.AUDIO_STT_MISTRAL_API_KEY = form_data.stt.MISTRAL_API_KEY
request.app.state.config.AUDIO_STT_MISTRAL_API_BASE_URL = (
form_data.stt.MISTRAL_API_BASE_URL
)
request.app.state.config.AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS = (
form_data.stt.MISTRAL_USE_CHAT_COMPLETIONS
)
if request.app.state.config.STT_ENGINE == "":
request.app.state.faster_whisper_model = set_faster_whisper_model(
@@ -261,12 +280,13 @@ async def update_audio_config(
return {
"tts": {
"OPENAI_API_BASE_URL": request.app.state.config.TTS_OPENAI_API_BASE_URL,
"OPENAI_API_KEY": request.app.state.config.TTS_OPENAI_API_KEY,
"API_KEY": request.app.state.config.TTS_API_KEY,
"ENGINE": request.app.state.config.TTS_ENGINE,
"MODEL": request.app.state.config.TTS_MODEL,
"VOICE": request.app.state.config.TTS_VOICE,
"OPENAI_API_BASE_URL": request.app.state.config.TTS_OPENAI_API_BASE_URL,
"OPENAI_API_KEY": request.app.state.config.TTS_OPENAI_API_KEY,
"OPENAI_PARAMS": request.app.state.config.TTS_OPENAI_PARAMS,
"API_KEY": request.app.state.config.TTS_API_KEY,
"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,
@@ -285,6 +305,9 @@ async def update_audio_config(
"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,
"MISTRAL_API_KEY": request.app.state.config.AUDIO_STT_MISTRAL_API_KEY,
"MISTRAL_API_BASE_URL": request.app.state.config.AUDIO_STT_MISTRAL_API_BASE_URL,
"MISTRAL_USE_CHAT_COMPLETIONS": request.app.state.config.AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS,
},
}
@@ -336,6 +359,11 @@ async def speech(request: Request, user=Depends(get_verified_user)):
async with aiohttp.ClientSession(
timeout=timeout, trust_env=True
) as session:
payload = {
**payload,
**(request.app.state.config.TTS_OPENAI_PARAMS or {}),
}
r = await session.post(
url=f"{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech",
json=payload,
@@ -403,7 +431,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
timeout=timeout, trust_env=True
) as session:
async with session.post(
f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}",
f"{ELEVENLABS_API_BASE_URL}/v1/text-to-speech/{voice_id}",
json={
"text": payload["input"],
"model_id": request.app.state.config.TTS_MODEL,
@@ -458,7 +486,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
try:
data = f"""<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="{locale}">
<voice name="{language}">{payload["input"]}</voice>
<voice name="{language}">{html.escape(payload["input"])}</voice>
</speak>"""
timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
async with aiohttp.ClientSession(
@@ -818,6 +846,186 @@ def transcription_handler(request, file_path, metadata):
detail=detail if detail else "Open WebUI: Server Connection Error",
)
elif request.app.state.config.STT_ENGINE == "mistral":
# Check file exists
if not os.path.exists(file_path):
raise HTTPException(status_code=400, detail="Audio file not found")
# Check file size
file_size = os.path.getsize(file_path)
if file_size > MAX_FILE_SIZE:
raise HTTPException(
status_code=400,
detail=f"File size exceeds limit of {MAX_FILE_SIZE_MB}MB",
)
api_key = request.app.state.config.AUDIO_STT_MISTRAL_API_KEY
api_base_url = (
request.app.state.config.AUDIO_STT_MISTRAL_API_BASE_URL
or "https://api.mistral.ai/v1"
)
use_chat_completions = (
request.app.state.config.AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS
)
if not api_key:
raise HTTPException(
status_code=400,
detail="Mistral API key is required for Mistral STT",
)
r = None
try:
# Use voxtral-mini-latest as the default model for transcription
model = request.app.state.config.STT_MODEL or "voxtral-mini-latest"
log.info(
f"Mistral STT - model: {model}, "
f"method: {'chat_completions' if use_chat_completions else 'transcriptions'}"
)
if use_chat_completions:
# Use chat completions API with audio input
# This method requires mp3 or wav format
audio_file_to_use = file_path
if is_audio_conversion_required(file_path):
log.debug("Converting audio to mp3 for chat completions API")
converted_path = convert_audio_to_mp3(file_path)
if converted_path:
audio_file_to_use = converted_path
else:
log.error("Audio conversion failed")
raise HTTPException(
status_code=500,
detail="Audio conversion failed. Chat completions API requires mp3 or wav format.",
)
# Read and encode audio file as base64
with open(audio_file_to_use, "rb") as audio_file:
audio_base64 = base64.b64encode(audio_file.read()).decode("utf-8")
# Prepare chat completions request
url = f"{api_base_url}/chat/completions"
# Add language instruction if specified
language = metadata.get("language", None) if metadata else None
if language:
text_instruction = f"Transcribe this audio exactly as spoken in {language}. Do not translate it."
else:
text_instruction = "Transcribe this audio exactly as spoken in its original language. Do not translate it to another language."
payload = {
"model": model,
"messages": [
{
"role": "user",
"content": [
{
"type": "input_audio",
"input_audio": audio_base64,
},
{"type": "text", "text": text_instruction},
],
}
],
}
r = requests.post(
url=url,
json=payload,
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
)
r.raise_for_status()
response = r.json()
# Extract transcript from chat completion response
transcript = (
response.get("choices", [{}])[0]
.get("message", {})
.get("content", "")
.strip()
)
if not transcript:
raise ValueError("Empty transcript in response")
data = {"text": transcript}
else:
# Use dedicated transcriptions API
url = f"{api_base_url}/audio/transcriptions"
# Determine the MIME type
mime_type, _ = mimetypes.guess_type(file_path)
if not mime_type:
mime_type = "audio/webm"
# Use context manager to ensure file is properly closed
with open(file_path, "rb") as audio_file:
files = {"file": (filename, audio_file, mime_type)}
data_form = {"model": model}
# Add language if specified in metadata
language = metadata.get("language", None) if metadata else None
if language:
data_form["language"] = language
r = requests.post(
url=url,
files=files,
data=data_form,
headers={
"Authorization": f"Bearer {api_key}",
},
)
r.raise_for_status()
response = r.json()
# Extract transcript from response
transcript = response.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 ValueError as e:
log.exception("Error parsing Mistral response")
raise HTTPException(
status_code=500,
detail=f"Failed to parse Mistral 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', '')}"
else:
detail = f"External: {r.text}"
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}")
@@ -1027,7 +1235,7 @@ def get_available_models(request: Request) -> list[dict]:
elif request.app.state.config.TTS_ENGINE == "elevenlabs":
try:
response = requests.get(
"https://api.elevenlabs.io/v1/models",
f"{ELEVENLABS_API_BASE_URL}/v1/models",
headers={
"xi-api-key": request.app.state.config.TTS_API_KEY,
"Content-Type": "application/json",
@@ -1131,7 +1339,7 @@ def get_elevenlabs_voices(api_key: str) -> dict:
try:
# TODO: Add retries
response = requests.get(
"https://api.elevenlabs.io/v1/voices",
f"{ELEVENLABS_API_BASE_URL}/v1/voices",
headers={
"xi-api-key": api_key,
"Content-Type": "application/json",
+9
View File
@@ -508,6 +508,15 @@ async def signin(request: Request, response: Response, form_data: SigninForm):
user = Auths.authenticate_user(admin_email.lower(), admin_password)
else:
password_bytes = form_data.password.encode("utf-8")
if len(password_bytes) > 72:
# TODO: Implement other hashing algorithms that support longer passwords
log.info("Password too long, truncating to 72 bytes for bcrypt")
password_bytes = password_bytes[:72]
# decode safely — ignore incomplete UTF-8 sequences
form_data.password = password_bytes.decode("utf-8", errors="ignore")
user = Auths.authenticate_user(form_data.email.lower(), form_data.password)
if user:
+37 -21
View File
@@ -340,11 +340,12 @@ async def model_response_handler(request, channel, message, user):
if file.get("type", "") == "image":
images.append(file.get("url", ""))
thread_history_string = "\n\n".join(thread_history)
system_message = {
"role": "system",
"content": f"You are {model.get('name', model_id)}, participating in a threaded conversation. Be concise and conversational."
+ (
f"Here's the thread history:\n\n{''.join([f'{msg}' for msg in thread_history])}\n\nContinue the conversation naturally as {model.get('name', model_id)}, addressing the most recent message while being aware of the full context."
f"Here's the thread history:\n\n\n{thread_history_string}\n\n\nContinue the conversation naturally as {model.get('name', model_id)}, addressing the most recent message while being aware of the full context."
if thread_history
else ""
),
@@ -384,19 +385,34 @@ async def model_response_handler(request, channel, message, user):
)
if res:
await update_message_by_id(
channel.id,
response_message.id,
MessageForm(
**{
"content": res["choices"][0]["message"]["content"],
"meta": {
"done": True,
},
}
),
user,
)
if res.get("choices", []) and len(res["choices"]) > 0:
await update_message_by_id(
channel.id,
response_message.id,
MessageForm(
**{
"content": res["choices"][0]["message"]["content"],
"meta": {
"done": True,
},
}
),
user,
)
elif res.get("error", None):
await update_message_by_id(
channel.id,
response_message.id,
MessageForm(
**{
"content": f"Error: {res['error']}",
"meta": {
"done": True,
},
}
),
user,
)
except Exception as e:
log.info(e)
pass
@@ -436,7 +452,7 @@ async def new_message_handler(
}
await sio.emit(
"channel-events",
"events:channel",
event_data,
to=f"channel:{channel.id}",
)
@@ -447,7 +463,7 @@ async def new_message_handler(
if parent_message:
await sio.emit(
"channel-events",
"events:channel",
{
"channel_id": channel.id,
"message_id": parent_message.id,
@@ -644,7 +660,7 @@ async def update_message_by_id(
if message:
await sio.emit(
"channel-events",
"events:channel",
{
"channel_id": channel.id,
"message_id": message.id,
@@ -708,7 +724,7 @@ async def add_reaction_to_message(
message = Messages.get_message_by_id(message_id)
await sio.emit(
"channel-events",
"events:channel",
{
"channel_id": channel.id,
"message_id": message.id,
@@ -774,7 +790,7 @@ async def remove_reaction_by_id_and_user_id_and_name(
message = Messages.get_message_by_id(message_id)
await sio.emit(
"channel-events",
"events:channel",
{
"channel_id": channel.id,
"message_id": message.id,
@@ -839,7 +855,7 @@ async def delete_message_by_id(
try:
Messages.delete_message_by_id(message_id)
await sio.emit(
"channel-events",
"events:channel",
{
"channel_id": channel.id,
"message_id": message.id,
@@ -862,7 +878,7 @@ async def delete_message_by_id(
if parent_message:
await sio.emit(
"channel-events",
"events:channel",
{
"channel_id": channel.id,
"message_id": parent_message.id,
+7 -2
View File
@@ -39,6 +39,7 @@ router = APIRouter()
def get_session_user_chat_list(
user=Depends(get_verified_user),
page: Optional[int] = None,
include_pinned: Optional[bool] = False,
include_folders: Optional[bool] = False,
):
try:
@@ -47,11 +48,15 @@ def get_session_user_chat_list(
skip = (page - 1) * limit
return Chats.get_chat_title_id_list_by_user_id(
user.id, include_folders=include_folders, skip=skip, limit=limit
user.id,
include_folders=include_folders,
include_pinned=include_pinned,
skip=skip,
limit=limit,
)
else:
return Chats.get_chat_title_id_list_by_user_id(
user.id, include_folders=include_folders
user.id, include_folders=include_folders, include_pinned=include_pinned
)
except Exception as e:
log.exception(e)
+22 -4
View File
@@ -1,4 +1,5 @@
import logging
import copy
from fastapi import APIRouter, Depends, Request, HTTPException
from pydantic import BaseModel, ConfigDict
import aiohttp
@@ -15,6 +16,7 @@ from open_webui.utils.tools import (
set_tool_servers,
)
from open_webui.utils.mcp.client import MCPClient
from open_webui.models.oauth_sessions import OAuthSessions
from open_webui.env import SRC_LOG_LEVELS
@@ -165,6 +167,21 @@ async def set_tool_servers_config(
form_data: ToolServersConfigForm,
user=Depends(get_admin_user),
):
for connection in request.app.state.config.TOOL_SERVER_CONNECTIONS:
server_type = connection.get("type", "openapi")
auth_type = connection.get("auth_type", "none")
if auth_type == "oauth_2.1":
# Remove existing OAuth clients for tool servers
server_id = connection.get("info", {}).get("id")
client_key = f"{server_type}:{server_id}"
try:
request.app.state.oauth_client_manager.remove_client(client_key)
except:
pass
# Set new tool server connections
request.app.state.config.TOOL_SERVER_CONNECTIONS = [
connection.model_dump() for connection in form_data.TOOL_SERVER_CONNECTIONS
]
@@ -176,6 +193,7 @@ async def set_tool_servers_config(
if server_type == "mcp":
server_id = connection.get("info", {}).get("id")
auth_type = connection.get("auth_type", "none")
if auth_type == "oauth_2.1" and server_id:
try:
oauth_client_info = connection.get("info", {}).get(
@@ -183,7 +201,7 @@ async def set_tool_servers_config(
)
oauth_client_info = decrypt_data(oauth_client_info)
await request.app.state.oauth_client_manager.add_client(
request.app.state.oauth_client_manager.add_client(
f"{server_type}:{server_id}",
OAuthClientInformationFull(**oauth_client_info),
)
@@ -211,9 +229,9 @@ async def verify_tool_servers_config(
log.debug(
f"Trying to fetch OAuth 2.1 discovery document from {discovery_url}"
)
async with aiohttp.ClientSession() as session:
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get(
discovery_urls[0]
discovery_url
) as oauth_server_metadata_response:
if oauth_server_metadata_response.status == 200:
try:
@@ -234,7 +252,7 @@ async def verify_tool_servers_config(
)
raise HTTPException(
status_code=400,
detail=f"Failed to parse OAuth 2.1 discovery document from {discovery_urls[0]}",
detail=f"Failed to parse OAuth 2.1 discovery document from {discovery_url}",
)
raise HTTPException(
+4
View File
@@ -115,6 +115,10 @@ def process_uploaded_file(request, file, file_path, file_item, file_metadata, us
request.app.state.config.CONTENT_EXTRACTION_ENGINE == "external"
):
process_file(request, ProcessFileForm(file_id=file_item.id), user=user)
else:
raise Exception(
f"File type {file.content_type} is not supported for processing"
)
else:
log.info(
f"File type {file.content_type} is not provided, but trying to process anyway"
+35 -20
View File
@@ -50,7 +50,15 @@ async def get_folders(user=Depends(get_verified_user)):
folders = Folders.get_folders_by_user_id(user.id)
# Verify folder data integrity
folder_list = []
for folder in folders:
if folder.parent_id and not Folders.get_folder_by_id_and_user_id(
folder.parent_id, user.id
):
folder = Folders.update_folder_parent_id_by_id_and_user_id(
folder.id, user.id, None
)
if folder.data:
if "files" in folder.data:
valid_files = []
@@ -74,12 +82,9 @@ async def get_folders(user=Depends(get_verified_user)):
folder.id, user.id, FolderUpdateForm(data=folder.data)
)
return [
{
**folder.model_dump(),
}
for folder in folders
]
folder_list.append(FolderNameIdResponse(**folder.model_dump()))
return folder_list
############################
@@ -265,21 +270,31 @@ async def delete_folder_by_id(
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
folder = Folders.get_folder_by_id_and_user_id(id, user.id)
if folder:
try:
folder_ids = Folders.delete_folder_by_id_and_user_id(id, user.id)
for folder_id in folder_ids:
Chats.delete_chats_by_user_id_and_folder_id(user.id, folder_id)
folders = []
folders.append(Folders.get_folder_by_id_and_user_id(id, user.id))
while folders:
folder = folders.pop()
if folder:
try:
folder_ids = Folders.delete_folder_by_id_and_user_id(id, user.id)
for folder_id in folder_ids:
Chats.delete_chats_by_user_id_and_folder_id(user.id, folder_id)
return True
except Exception as e:
log.exception(e)
log.error(f"Error deleting folder: {id}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error deleting folder"),
)
finally:
# Get all subfolders
subfolders = Folders.get_folders_by_parent_id_and_user_id(
folder.id, user.id
)
folders.extend(subfolders)
return True
except Exception as e:
log.exception(e)
log.error(f"Error deleting folder: {id}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error deleting folder"),
)
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
+6
View File
@@ -10,6 +10,7 @@ from open_webui.models.functions import (
FunctionForm,
FunctionModel,
FunctionResponse,
FunctionUserResponse,
FunctionWithValvesModel,
Functions,
)
@@ -42,6 +43,11 @@ async def get_functions(user=Depends(get_verified_user)):
return Functions.get_functions()
@router.get("/list", response_model=list[FunctionUserResponse])
async def get_function_list(user=Depends(get_admin_user)):
return Functions.get_function_list()
############################
# ExportFunctions
############################
File diff suppressed because it is too large Load Diff
+15 -2
View File
@@ -35,12 +35,18 @@ log = logging.getLogger(__name__)
router = APIRouter()
def validate_model_id(model_id: str) -> bool:
return model_id and len(model_id) <= 256
###########################
# GetModels
###########################
@router.get("/", response_model=list[ModelUserResponse])
@router.get(
"/list", response_model=list[ModelUserResponse]
) # do NOT use "/" as path, conflicts with main.py
async def get_models(id: Optional[str] = None, user=Depends(get_verified_user)):
if user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL:
return Models.get_models()
@@ -84,6 +90,12 @@ async def create_new_model(
detail=ERROR_MESSAGES.MODEL_ID_TAKEN,
)
if not validate_model_id(form_data.id):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.MODEL_ID_TOO_LONG,
)
else:
model = Models.insert_new_model(form_data, user.id)
if model:
@@ -124,7 +136,8 @@ async def import_models(
for model_data in data:
# Here, you can add logic to validate model_data if needed
model_id = model_data.get("id")
if model_id:
if model_id and validate_model_id(model_id):
existing_model = Models.get_model_by_id(model_id)
if existing_model:
# Update existing model
+4
View File
@@ -1020,6 +1020,10 @@ class GenerateEmbedForm(BaseModel):
options: Optional[dict] = None
keep_alive: Optional[Union[int, str]] = None
model_config = ConfigDict(
extra="allow",
)
@router.post("/api/embed")
@router.post("/api/embed/{url_idx}")
+39 -31
View File
@@ -190,6 +190,9 @@ async def get_headers_and_cookies(
if token:
headers["Authorization"] = f"Bearer {token}"
if config.get("headers") and isinstance(config.get("headers"), dict):
headers = {**headers, **config.get("headers")}
return headers, cookies
@@ -498,50 +501,55 @@ async def get_all_models(request: Request, user: UserModel) -> dict[str, list]:
return response
return None
def merge_models_lists(model_lists):
def is_supported_openai_models(model_id):
if any(
name in model_id
for name in [
"babbage",
"dall-e",
"davinci",
"embedding",
"tts",
"whisper",
]
):
return False
return True
def get_merged_models(model_lists):
log.debug(f"merge_models_lists {model_lists}")
merged_list = []
models = {}
for idx, models in enumerate(model_lists):
if models is not None and "error" not in models:
for idx, model_list in enumerate(model_lists):
if model_list is not None and "error" not in model_list:
for model in model_list:
model_id = model.get("id") or model.get("name")
merged_list.extend(
[
{
if (
"api.openai.com"
in request.app.state.config.OPENAI_API_BASE_URLS[idx]
and not is_supported_openai_models(model_id)
):
# Skip unwanted OpenAI models
continue
if model_id and model_id not in models:
models[model_id] = {
**model,
"name": model.get("name", model["id"]),
"name": model.get("name", model_id),
"owned_by": "openai",
"openai": model,
"connection_type": model.get("connection_type", "external"),
"urlIdx": idx,
}
for model in models
if (model.get("id") or model.get("name"))
and (
"api.openai.com"
not in request.app.state.config.OPENAI_API_BASE_URLS[idx]
or not any(
name in model["id"]
for name in [
"babbage",
"dall-e",
"davinci",
"embedding",
"tts",
"whisper",
]
)
)
]
)
return merged_list
return models
models = {"data": merge_models_lists(map(extract_data, responses))}
models = get_merged_models(map(extract_data, responses))
log.debug(f"models: {models}")
request.app.state.OPENAI_MODELS = {model["id"]: model for model in models["data"]}
return models
request.app.state.OPENAI_MODELS = models
return {"data": list(models.values())}
@router.get("/models")
+100 -53
View File
@@ -5,6 +5,7 @@ import os
import shutil
import asyncio
import re
import uuid
from datetime import datetime
from pathlib import Path
@@ -70,6 +71,7 @@ from open_webui.retrieval.web.firecrawl import search_firecrawl
from open_webui.retrieval.web.external import search_external
from open_webui.retrieval.utils import (
get_content_from_url,
get_embedding_function,
get_reranking_function,
get_model_path,
@@ -189,6 +191,26 @@ def get_rf(
log.error(f"CrossEncoder: {e}")
raise Exception(ERROR_MESSAGES.DEFAULT("CrossEncoder error"))
# Safely adjust pad_token_id if missing as some models do not have this in config
try:
model_cfg = getattr(rf, "model", None)
if model_cfg and hasattr(model_cfg, "config"):
cfg = model_cfg.config
if getattr(cfg, "pad_token_id", None) is None:
# Fallback to eos_token_id when available
eos = getattr(cfg, "eos_token_id", None)
if eos is not None:
cfg.pad_token_id = eos
log.debug(
f"Missing pad_token_id detected; set to eos_token_id={eos}"
)
else:
log.warning(
"Neither pad_token_id nor eos_token_id present in model config"
)
except Exception as e2:
log.warning(f"Failed to adjust pad_token_id on CrossEncoder: {e2}")
return rf
@@ -429,6 +451,7 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
"EXTERNAL_DOCUMENT_LOADER_API_KEY": request.app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY,
"TIKA_SERVER_URL": request.app.state.config.TIKA_SERVER_URL,
"DOCLING_SERVER_URL": request.app.state.config.DOCLING_SERVER_URL,
"DOCLING_PARAMS": request.app.state.config.DOCLING_PARAMS,
"DOCLING_DO_OCR": request.app.state.config.DOCLING_DO_OCR,
"DOCLING_FORCE_OCR": request.app.state.config.DOCLING_FORCE_OCR,
"DOCLING_OCR_ENGINE": request.app.state.config.DOCLING_OCR_ENGINE,
@@ -442,7 +465,13 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
"DOCLING_PICTURE_DESCRIPTION_API": request.app.state.config.DOCLING_PICTURE_DESCRIPTION_API,
"DOCUMENT_INTELLIGENCE_ENDPOINT": request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT,
"DOCUMENT_INTELLIGENCE_KEY": request.app.state.config.DOCUMENT_INTELLIGENCE_KEY,
"MISTRAL_OCR_API_BASE_URL": request.app.state.config.MISTRAL_OCR_API_BASE_URL,
"MISTRAL_OCR_API_KEY": request.app.state.config.MISTRAL_OCR_API_KEY,
# MinerU settings
"MINERU_API_MODE": request.app.state.config.MINERU_API_MODE,
"MINERU_API_URL": request.app.state.config.MINERU_API_URL,
"MINERU_API_KEY": request.app.state.config.MINERU_API_KEY,
"MINERU_PARAMS": request.app.state.config.MINERU_PARAMS,
# Reranking settings
"RAG_RERANKING_MODEL": request.app.state.config.RAG_RERANKING_MODEL,
"RAG_RERANKING_ENGINE": request.app.state.config.RAG_RERANKING_ENGINE,
@@ -590,6 +619,7 @@ class ConfigForm(BaseModel):
# Content extraction settings
CONTENT_EXTRACTION_ENGINE: Optional[str] = None
PDF_EXTRACT_IMAGES: Optional[bool] = None
DATALAB_MARKER_API_KEY: Optional[str] = None
DATALAB_MARKER_API_BASE_URL: Optional[str] = None
DATALAB_MARKER_ADDITIONAL_CONFIG: Optional[str] = None
@@ -601,11 +631,13 @@ class ConfigForm(BaseModel):
DATALAB_MARKER_FORMAT_LINES: Optional[bool] = None
DATALAB_MARKER_USE_LLM: Optional[bool] = None
DATALAB_MARKER_OUTPUT_FORMAT: Optional[str] = None
EXTERNAL_DOCUMENT_LOADER_URL: Optional[str] = None
EXTERNAL_DOCUMENT_LOADER_API_KEY: Optional[str] = None
TIKA_SERVER_URL: Optional[str] = None
DOCLING_SERVER_URL: Optional[str] = None
DOCLING_PARAMS: Optional[dict] = None
DOCLING_DO_OCR: Optional[bool] = None
DOCLING_FORCE_OCR: Optional[bool] = None
DOCLING_OCR_ENGINE: Optional[str] = None
@@ -619,8 +651,15 @@ class ConfigForm(BaseModel):
DOCLING_PICTURE_DESCRIPTION_API: Optional[dict] = None
DOCUMENT_INTELLIGENCE_ENDPOINT: Optional[str] = None
DOCUMENT_INTELLIGENCE_KEY: Optional[str] = None
MISTRAL_OCR_API_BASE_URL: Optional[str] = None
MISTRAL_OCR_API_KEY: Optional[str] = None
# MinerU settings
MINERU_API_MODE: Optional[str] = None
MINERU_API_URL: Optional[str] = None
MINERU_API_KEY: Optional[str] = None
MINERU_PARAMS: Optional[dict] = None
# Reranking settings
RAG_RERANKING_MODEL: Optional[str] = None
RAG_RERANKING_ENGINE: Optional[str] = None
@@ -782,6 +821,11 @@ async def update_rag_config(
if form_data.DOCLING_SERVER_URL is not None
else request.app.state.config.DOCLING_SERVER_URL
)
request.app.state.config.DOCLING_PARAMS = (
form_data.DOCLING_PARAMS
if form_data.DOCLING_PARAMS is not None
else request.app.state.config.DOCLING_PARAMS
)
request.app.state.config.DOCLING_DO_OCR = (
form_data.DOCLING_DO_OCR
if form_data.DOCLING_DO_OCR is not None
@@ -849,12 +893,40 @@ async def update_rag_config(
if form_data.DOCUMENT_INTELLIGENCE_KEY is not None
else request.app.state.config.DOCUMENT_INTELLIGENCE_KEY
)
request.app.state.config.MISTRAL_OCR_API_BASE_URL = (
form_data.MISTRAL_OCR_API_BASE_URL
if form_data.MISTRAL_OCR_API_BASE_URL is not None
else request.app.state.config.MISTRAL_OCR_API_BASE_URL
)
request.app.state.config.MISTRAL_OCR_API_KEY = (
form_data.MISTRAL_OCR_API_KEY
if form_data.MISTRAL_OCR_API_KEY is not None
else request.app.state.config.MISTRAL_OCR_API_KEY
)
# MinerU settings
request.app.state.config.MINERU_API_MODE = (
form_data.MINERU_API_MODE
if form_data.MINERU_API_MODE is not None
else request.app.state.config.MINERU_API_MODE
)
request.app.state.config.MINERU_API_URL = (
form_data.MINERU_API_URL
if form_data.MINERU_API_URL is not None
else request.app.state.config.MINERU_API_URL
)
request.app.state.config.MINERU_API_KEY = (
form_data.MINERU_API_KEY
if form_data.MINERU_API_KEY is not None
else request.app.state.config.MINERU_API_KEY
)
request.app.state.config.MINERU_PARAMS = (
form_data.MINERU_PARAMS
if form_data.MINERU_PARAMS is not None
else request.app.state.config.MINERU_PARAMS
)
# Reranking settings
if request.app.state.config.RAG_RERANKING_ENGINE == "":
# Unloading the internal reranker and clear VRAM memory
@@ -1104,6 +1176,7 @@ async def update_rag_config(
"EXTERNAL_DOCUMENT_LOADER_API_KEY": request.app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY,
"TIKA_SERVER_URL": request.app.state.config.TIKA_SERVER_URL,
"DOCLING_SERVER_URL": request.app.state.config.DOCLING_SERVER_URL,
"DOCLING_PARAMS": request.app.state.config.DOCLING_PARAMS,
"DOCLING_DO_OCR": request.app.state.config.DOCLING_DO_OCR,
"DOCLING_FORCE_OCR": request.app.state.config.DOCLING_FORCE_OCR,
"DOCLING_OCR_ENGINE": request.app.state.config.DOCLING_OCR_ENGINE,
@@ -1117,7 +1190,13 @@ async def update_rag_config(
"DOCLING_PICTURE_DESCRIPTION_API": request.app.state.config.DOCLING_PICTURE_DESCRIPTION_API,
"DOCUMENT_INTELLIGENCE_ENDPOINT": request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT,
"DOCUMENT_INTELLIGENCE_KEY": request.app.state.config.DOCUMENT_INTELLIGENCE_KEY,
"MISTRAL_OCR_API_BASE_URL": request.app.state.config.MISTRAL_OCR_API_BASE_URL,
"MISTRAL_OCR_API_KEY": request.app.state.config.MISTRAL_OCR_API_KEY,
# MinerU settings
"MINERU_API_MODE": request.app.state.config.MINERU_API_MODE,
"MINERU_API_URL": request.app.state.config.MINERU_API_URL,
"MINERU_API_KEY": request.app.state.config.MINERU_API_KEY,
"MINERU_PARAMS": request.app.state.config.MINERU_PARAMS,
# Reranking settings
"RAG_RERANKING_MODEL": request.app.state.config.RAG_RERANKING_MODEL,
"RAG_RERANKING_ENGINE": request.app.state.config.RAG_RERANKING_ENGINE,
@@ -1495,6 +1574,7 @@ def process_file(
file_path = Storage.get_file(file_path)
loader = Loader(
engine=request.app.state.config.CONTENT_EXTRACTION_ENGINE,
user=user,
DATALAB_MARKER_API_KEY=request.app.state.config.DATALAB_MARKER_API_KEY,
DATALAB_MARKER_API_BASE_URL=request.app.state.config.DATALAB_MARKER_API_BASE_URL,
DATALAB_MARKER_ADDITIONAL_CONFIG=request.app.state.config.DATALAB_MARKER_ADDITIONAL_CONFIG,
@@ -1522,11 +1602,17 @@ def process_file(
"picture_description_mode": request.app.state.config.DOCLING_PICTURE_DESCRIPTION_MODE,
"picture_description_local": request.app.state.config.DOCLING_PICTURE_DESCRIPTION_LOCAL,
"picture_description_api": request.app.state.config.DOCLING_PICTURE_DESCRIPTION_API,
**request.app.state.config.DOCLING_PARAMS,
},
PDF_EXTRACT_IMAGES=request.app.state.config.PDF_EXTRACT_IMAGES,
DOCUMENT_INTELLIGENCE_ENDPOINT=request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT,
DOCUMENT_INTELLIGENCE_KEY=request.app.state.config.DOCUMENT_INTELLIGENCE_KEY,
MISTRAL_OCR_API_BASE_URL=request.app.state.config.MISTRAL_OCR_API_BASE_URL,
MISTRAL_OCR_API_KEY=request.app.state.config.MISTRAL_OCR_API_KEY,
MINERU_API_MODE=request.app.state.config.MINERU_API_MODE,
MINERU_API_URL=request.app.state.config.MINERU_API_URL,
MINERU_API_KEY=request.app.state.config.MINERU_API_KEY,
MINERU_PARAMS=request.app.state.config.MINERU_PARAMS,
)
docs = loader.load(
file.filename, file.meta.get("content_type"), file_path
@@ -1680,49 +1766,6 @@ def process_text(
@router.post("/process/youtube")
def process_youtube_video(
request: Request, form_data: ProcessUrlForm, user=Depends(get_verified_user)
):
try:
collection_name = form_data.collection_name
if not collection_name:
collection_name = calculate_sha256_string(form_data.url)[:63]
loader = YoutubeLoader(
form_data.url,
language=request.app.state.config.YOUTUBE_LOADER_LANGUAGE,
proxy_url=request.app.state.config.YOUTUBE_LOADER_PROXY_URL,
)
docs = loader.load()
content = " ".join([doc.page_content for doc in docs])
log.debug(f"text_content: {content}")
save_docs_to_vector_db(
request, docs, collection_name, overwrite=True, user=user
)
return {
"status": True,
"collection_name": collection_name,
"filename": form_data.url,
"file": {
"data": {
"content": content,
},
"meta": {
"name": form_data.url,
},
},
}
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
@router.post("/process/web")
def process_web(
request: Request, form_data: ProcessUrlForm, user=Depends(get_verified_user)
@@ -1732,19 +1775,16 @@ def process_web(
if not collection_name:
collection_name = calculate_sha256_string(form_data.url)[:63]
loader = get_web_loader(
form_data.url,
verify_ssl=request.app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION,
requests_per_second=request.app.state.config.WEB_LOADER_CONCURRENT_REQUESTS,
)
docs = loader.load()
content = " ".join([doc.page_content for doc in docs])
content, docs = get_content_from_url(request, form_data.url)
log.debug(f"text_content: {content}")
if not request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL:
save_docs_to_vector_db(
request, docs, collection_name, overwrite=True, user=user
request,
docs,
collection_name,
overwrite=True,
user=user,
)
else:
collection_name = None
@@ -1846,6 +1886,7 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
query,
request.app.state.config.WEB_SEARCH_RESULT_COUNT,
request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
referer=request.app.state.config.WEBUI_URL,
)
else:
raise Exception(
@@ -2047,7 +2088,7 @@ async def process_web_search(
result_items = []
try:
logging.info(
logging.debug(
f"trying to web search with {request.app.state.config.WEB_SEARCH_ENGINE, form_data.queries}"
)
@@ -2081,6 +2122,12 @@ async def process_web_search(
detail=ERROR_MESSAGES.WEB_SEARCH_ERROR(e),
)
if len(urls) == 0:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.DEFAULT("No results found from web search"),
)
try:
if request.app.state.config.BYPASS_WEB_SEARCH_WEB_LOADER:
search_results = [
+1 -1
View File
@@ -361,7 +361,7 @@ async def get_user_by_id(user_id: str, user=Depends(get_verified_user)):
)
@router.get("/{user_id}/oauth/sessions", response_model=Optional[dict])
@router.get("/{user_id}/oauth/sessions")
async def get_user_oauth_sessions_by_id(user_id: str, user=Depends(get_admin_user)):
sessions = OAuthSessions.get_sessions_by_user_id(user_id)
if sessions and len(sessions) > 0:
+21 -10
View File
@@ -18,7 +18,12 @@ from open_webui.utils.redis import (
get_sentinel_url_from_env,
)
from open_webui.config import (
CORS_ALLOW_ORIGIN,
)
from open_webui.env import (
VERSION,
ENABLE_WEBSOCKET_SUPPORT,
WEBSOCKET_MANAGER,
WEBSOCKET_REDIS_URL,
@@ -58,7 +63,7 @@ if WEBSOCKET_MANAGER == "redis":
else:
mgr = socketio.AsyncRedisManager(WEBSOCKET_REDIS_URL)
sio = socketio.AsyncServer(
cors_allowed_origins=[],
cors_allowed_origins=CORS_ALLOW_ORIGIN,
async_mode="asgi",
transports=(["websocket"] if ENABLE_WEBSOCKET_SUPPORT else ["polling"]),
allow_upgrades=ENABLE_WEBSOCKET_SUPPORT,
@@ -67,7 +72,7 @@ if WEBSOCKET_MANAGER == "redis":
)
else:
sio = socketio.AsyncServer(
cors_allowed_origins=[],
cors_allowed_origins=CORS_ALLOW_ORIGIN,
async_mode="asgi",
transports=(["websocket"] if ENABLE_WEBSOCKET_SUPPORT else ["polling"]),
allow_upgrades=ENABLE_WEBSOCKET_SUPPORT,
@@ -356,7 +361,7 @@ async def join_note(sid, data):
await sio.enter_room(sid, f"note:{note.id}")
@sio.on("channel-events")
@sio.on("events:channel")
async def channel_events(sid, data):
room = f"channel:{data['channel_id']}"
participants = sio.manager.get_participants(
@@ -373,7 +378,7 @@ async def channel_events(sid, data):
if event_type == "typing":
await sio.emit(
"channel-events",
"events:channel",
{
"channel_id": data["channel_id"],
"message_id": data.get("message_id", None),
@@ -653,12 +658,15 @@ def get_event_emitter(request_info, update_db=True):
)
)
chat_id = request_info.get("chat_id", None)
message_id = request_info.get("message_id", None)
emit_tasks = [
sio.emit(
"chat-events",
"events",
{
"chat_id": request_info.get("chat_id", None),
"message_id": request_info.get("message_id", None),
"chat_id": chat_id,
"message_id": message_id,
"data": event_data,
},
to=session_id,
@@ -667,8 +675,11 @@ def get_event_emitter(request_info, update_db=True):
]
await asyncio.gather(*emit_tasks)
if update_db:
if (
update_db
and message_id
and not request_info.get("chat_id", "").startswith("local:")
):
if "type" in event_data and event_data["type"] == "status":
Chats.add_message_status_to_chat_by_id_and_message_id(
request_info["chat_id"],
@@ -764,7 +775,7 @@ def get_event_emitter(request_info, update_db=True):
def get_event_call(request_info):
async def __event_caller__(event_data):
response = await sio.call(
"chat-events",
"events",
{
"chat_id": request_info.get("chat_id", None),
"message_id": request_info.get("message_id", None),
+4 -1
View File
@@ -164,7 +164,10 @@ async def stop_task(redis, task_id: str):
# Task successfully canceled
return {"status": True, "message": f"Task {task_id} successfully stopped."}
return {"status": False, "message": f"Failed to stop task {task_id}."}
if task.cancelled() or task.done():
return {"status": True, "message": f"Task {task_id} successfully cancelled."}
return {"status": True, "message": f"Cancellation requested for {task_id}."}
async def stop_item_tasks(redis: Redis, item_id: str):
+14 -11
View File
@@ -6,7 +6,7 @@ import hmac
import hashlib
import requests
import os
import bcrypt
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.asymmetric import ed25519
@@ -38,11 +38,8 @@ from open_webui.env import (
from fastapi import BackgroundTasks, Depends, HTTPException, Request, Response, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from passlib.context import CryptContext
logging.getLogger("passlib").setLevel(logging.ERROR)
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["OAUTH"])
@@ -155,19 +152,25 @@ def get_license_data(app, key):
bearer_security = HTTPBearer(auto_error=False)
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def verify_password(plain_password, hashed_password):
def get_password_hash(password: str) -> str:
"""Hash a password using bcrypt"""
return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Verify a password against its hash"""
return (
pwd_context.verify(plain_password, hashed_password) if hashed_password else None
bcrypt.checkpw(
plain_password.encode("utf-8"),
hashed_password.encode("utf-8"),
)
if hashed_password
else None
)
def get_password_hash(password):
return pwd_context.hash(password)
def create_token(data: dict, expires_delta: Union[timedelta, None] = None) -> str:
payload = data.copy()
+5 -1
View File
@@ -80,6 +80,7 @@ async def generate_direct_chat_completion(
event_caller = get_event_call(metadata)
channel = f"{user_id}:{session_id}:{request_id}"
logging.info(f"WebSocket channel: {channel}")
if form_data.get("stream"):
q = asyncio.Queue()
@@ -121,7 +122,10 @@ async def generate_direct_chat_completion(
yield f"data: {json.dumps(data)}\n\n"
elif isinstance(data, str):
yield data
if "data:" in data:
yield f"{data}\n\n"
else:
yield f"data: {data}\n\n"
except Exception as e:
log.debug(f"Error in event generator: {e}")
pass
+2 -2
View File
@@ -1,5 +1,5 @@
from open_webui.routers.images import (
load_b64_image_data,
get_image_data,
upload_image,
)
@@ -22,7 +22,7 @@ def get_image_url_from_base64(request, base64_image_string, metadata, user):
if "data:image/png;base64" in base64_image_string:
image_url = ""
# Extract base64 image data from the line
image_data, content_type = load_b64_image_data(base64_image_string)
image_data, content_type = get_image_data(base64_image_string)
if image_data is not None:
image_url = upload_image(
request,
+11
View File
@@ -0,0 +1,11 @@
from urllib.parse import quote
def include_user_info_headers(headers, user):
return {
**headers,
"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,
}
+123 -3
View File
@@ -2,6 +2,8 @@ import asyncio
import json
import logging
import random
import requests
import aiohttp
import urllib.parse
import urllib.request
from typing import Optional
@@ -91,6 +93,25 @@ def get_images(ws, prompt, client_id, base_url, api_key):
return {"data": output_images}
async def comfyui_upload_image(image_file_item, base_url, api_key):
url = f"{base_url}/api/upload/image"
headers = {}
if api_key:
headers["Authorization"] = f"Bearer {api_key}"
_, (filename, file_bytes, mime_type) = image_file_item
form = aiohttp.FormData()
form.add_field("image", file_bytes, filename=filename, content_type=mime_type)
form.add_field("type", "input") # required by ComfyUI
async with aiohttp.ClientSession() as session:
async with session.post(url, data=form, headers=headers) as resp:
resp.raise_for_status()
return await resp.json()
class ComfyUINodeInput(BaseModel):
type: Optional[str] = None
node_ids: list[str] = []
@@ -103,7 +124,7 @@ class ComfyUIWorkflow(BaseModel):
nodes: list[ComfyUINodeInput]
class ComfyUIGenerateImageForm(BaseModel):
class ComfyUICreateImageForm(BaseModel):
workflow: ComfyUIWorkflow
prompt: str
@@ -116,8 +137,8 @@ class ComfyUIGenerateImageForm(BaseModel):
seed: Optional[int] = None
async def comfyui_generate_image(
model: str, payload: ComfyUIGenerateImageForm, client_id, base_url, api_key
async def comfyui_create_image(
model: str, payload: ComfyUICreateImageForm, client_id, base_url, api_key
):
ws_url = base_url.replace("http://", "ws://").replace("https://", "wss://")
workflow = json.loads(payload.workflow.workflow)
@@ -191,3 +212,102 @@ async def comfyui_generate_image(
ws.close()
return images
class ComfyUIEditImageForm(BaseModel):
workflow: ComfyUIWorkflow
image: str | list[str]
prompt: str
width: Optional[int] = None
height: Optional[int] = None
n: Optional[int] = None
steps: Optional[int] = None
seed: Optional[int] = None
async def comfyui_edit_image(
model: str, payload: ComfyUIEditImageForm, client_id, base_url, api_key
):
ws_url = base_url.replace("http://", "ws://").replace("https://", "wss://")
workflow = json.loads(payload.workflow.workflow)
for node in payload.workflow.nodes:
if node.type:
if node.type == "model":
for node_id in node.node_ids:
workflow[node_id]["inputs"][node.key] = model
elif node.type == "image":
if isinstance(payload.image, list):
# check if multiple images are provided
for idx, node_id in enumerate(node.node_ids):
if idx < len(payload.image):
workflow[node_id]["inputs"][node.key] = payload.image[idx]
else:
for node_id in node.node_ids:
workflow[node_id]["inputs"][node.key] = payload.image
elif node.type == "prompt":
for node_id in node.node_ids:
workflow[node_id]["inputs"][
node.key if node.key else "text"
] = payload.prompt
elif node.type == "negative_prompt":
for node_id in node.node_ids:
workflow[node_id]["inputs"][
node.key if node.key else "text"
] = payload.negative_prompt
elif node.type == "width":
for node_id in node.node_ids:
workflow[node_id]["inputs"][
node.key if node.key else "width"
] = payload.width
elif node.type == "height":
for node_id in node.node_ids:
workflow[node_id]["inputs"][
node.key if node.key else "height"
] = payload.height
elif node.type == "n":
for node_id in node.node_ids:
workflow[node_id]["inputs"][
node.key if node.key else "batch_size"
] = payload.n
elif node.type == "steps":
for node_id in node.node_ids:
workflow[node_id]["inputs"][
node.key if node.key else "steps"
] = payload.steps
elif node.type == "seed":
seed = (
payload.seed
if payload.seed
else random.randint(0, 1125899906842624)
)
for node_id in node.node_ids:
workflow[node_id]["inputs"][node.key] = seed
else:
for node_id in node.node_ids:
workflow[node_id]["inputs"][node.key] = node.value
try:
ws = websocket.WebSocket()
headers = {"Authorization": f"Bearer {api_key}"}
ws.connect(f"{ws_url}/ws?clientId={client_id}", header=headers)
log.info("WebSocket connection established.")
except Exception as e:
log.exception(f"Failed to connect to WebSocket server: {e}")
return None
try:
log.info("Sending workflow to WebSocket server.")
log.info(f"Workflow: {workflow}")
images = await asyncio.to_thread(
get_images, ws, workflow, client_id, base_url, api_key
)
except Exception as e:
log.exception(f"Error while receiving images: {e}")
images = None
ws.close()
return images
+20 -15
View File
@@ -2,6 +2,8 @@ import asyncio
from typing import Optional
from contextlib import AsyncExitStack
import anyio
from mcp import ClientSession
from mcp.client.auth import OAuthClientProvider, TokenStorage
from mcp.client.streamable_http import streamablehttp_client
@@ -11,26 +13,29 @@ from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAu
class MCPClient:
def __init__(self):
self.session: Optional[ClientSession] = None
self.exit_stack = AsyncExitStack()
self.exit_stack = None
async def connect(self, url: str, headers: Optional[dict] = None):
try:
self._streams_context = streamablehttp_client(url, headers=headers)
async with AsyncExitStack() as exit_stack:
try:
self._streams_context = streamablehttp_client(url, headers=headers)
transport = await self.exit_stack.enter_async_context(self._streams_context)
read_stream, write_stream, _ = transport
transport = await exit_stack.enter_async_context(self._streams_context)
read_stream, write_stream, _ = transport
self._session_context = ClientSession(
read_stream, write_stream
) # pylint: disable=W0201
self._session_context = ClientSession(
read_stream, write_stream
) # pylint: disable=W0201
self.session = await self.exit_stack.enter_async_context(
self._session_context
)
await self.session.initialize()
except Exception as e:
await self.disconnect()
raise e
self.session = await exit_stack.enter_async_context(
self._session_context
)
with anyio.fail_after(10):
await self.session.initialize()
self.exit_stack = exit_stack.pop_all()
except Exception as e:
await asyncio.shield(self.disconnect())
raise e
async def list_tool_specs(self) -> Optional[dict]:
if not self.session:
+362 -208
View File
@@ -40,12 +40,15 @@ from open_webui.routers.tasks import (
generate_image_prompt,
generate_chat_tags,
)
from open_webui.routers.retrieval import process_web_search, SearchForm
from open_webui.routers.retrieval import (
process_web_search,
SearchForm,
)
from open_webui.routers.images import (
load_b64_image_data,
image_generations,
GenerateImageForm,
upload_image,
CreateImageForm,
image_edits,
EditImageForm,
)
from open_webui.routers.pipelines import (
process_pipeline_inlet_filter,
@@ -76,16 +79,19 @@ from open_webui.utils.task import (
)
from open_webui.utils.misc import (
deep_update,
extract_urls,
get_message_list,
add_or_update_system_message,
add_or_update_user_message,
get_last_user_message,
get_last_user_message_item,
get_last_assistant_message,
get_system_message,
prepend_to_first_user_message_content,
convert_logit_bias_input_to_json,
get_content_from_message,
)
from open_webui.utils.tools import get_tools
from open_webui.utils.tools import get_tools, get_updated_tool_function
from open_webui.utils.plugin import load_function_module_by_id
from open_webui.utils.filter import (
get_sorted_filter_ids,
@@ -147,7 +153,7 @@ def process_tool_result(
if isinstance(tool_result, HTMLResponse):
content_disposition = tool_result.headers.get("Content-Disposition", "")
if "inline" in content_disposition:
content = tool_result.body.decode("utf-8")
content = tool_result.body.decode("utf-8", "replace")
tool_result_embeds.append(content)
if 200 <= tool_result.status_code < 300:
@@ -175,7 +181,7 @@ def process_tool_result(
"message": f"{tool_function_name}: Unexpected status code {tool_result.status_code} from embedded UI result.",
}
else:
tool_result = tool_result.body.decode("utf-8")
tool_result = tool_result.body.decode("utf-8", "replace")
elif (tool_type == "external" and isinstance(tool_result, tuple)) or (
direct_tool and isinstance(tool_result, list) and len(tool_result) == 2
@@ -283,7 +289,7 @@ async def chat_completion_tools_handler(
content = None
if hasattr(response, "body_iterator"):
async for chunk in response.body_iterator:
data = json.loads(chunk.decode("utf-8"))
data = json.loads(chunk.decode("utf-8", "replace"))
content = data["choices"][0]["message"]["content"]
# Cleanup any remaining background tasks if necessary
@@ -298,7 +304,7 @@ async def chat_completion_tools_handler(
recent_messages = messages[-4:] if len(messages) > 4 else messages
chat_history = "\n".join(
f"{message['role'].upper()}: \"\"\"{message['content']}\"\"\""
f"{message['role'].upper()}: \"\"\"{get_content_from_message(message)}\"\"\""
for message in recent_messages
)
@@ -712,9 +718,31 @@ async def chat_web_search_handler(
return form_data
def get_last_images(message_list):
images = []
for message in reversed(message_list):
images_flag = False
for file in message.get("files", []):
if file.get("type") == "image":
images.append(file.get("url"))
images_flag = True
if images_flag:
break
return images
async def chat_image_generation_handler(
request: Request, form_data: dict, extra_params: dict, user
):
metadata = extra_params.get("__metadata__", {})
chat_id = metadata.get("chat_id", None)
if not chat_id:
return form_data
chat = Chats.get_chat_by_id_and_user_id(chat_id, user.id)
__event_emitter__ = extra_params["__event_emitter__"]
await __event_emitter__(
{
@@ -723,87 +751,151 @@ async def chat_image_generation_handler(
}
)
messages = form_data["messages"]
user_message = get_last_user_message(messages)
messages_map = chat.chat.get("history", {}).get("messages", {})
message_id = chat.chat.get("history", {}).get("currentId")
message_list = get_message_list(messages_map, message_id)
user_message = get_last_user_message(message_list)
prompt = user_message
negative_prompt = ""
if request.app.state.config.ENABLE_IMAGE_PROMPT_GENERATION:
try:
res = await generate_image_prompt(
request,
{
"model": form_data["model"],
"messages": messages,
},
user,
)
response = res["choices"][0]["message"]["content"]
try:
bracket_start = response.find("{")
bracket_end = response.rfind("}") + 1
if bracket_start == -1 or bracket_end == -1:
raise Exception("No JSON object found in the response")
response = response[bracket_start:bracket_end]
response = json.loads(response)
prompt = response.get("prompt", [])
except Exception as e:
prompt = user_message
except Exception as e:
log.exception(e)
prompt = user_message
input_images = get_last_images(message_list)
system_message_content = ""
if len(input_images) == 0:
# Create image(s)
if request.app.state.config.ENABLE_IMAGE_PROMPT_GENERATION:
try:
res = await generate_image_prompt(
request,
{
"model": form_data["model"],
"messages": form_data["messages"],
},
user,
)
try:
images = await image_generations(
request=request,
form_data=GenerateImageForm(**{"prompt": prompt}),
user=user,
)
response = res["choices"][0]["message"]["content"]
await __event_emitter__(
{
"type": "status",
"data": {"description": "Image created", "done": True},
}
)
try:
bracket_start = response.find("{")
bracket_end = response.rfind("}") + 1
await __event_emitter__(
{
"type": "files",
"data": {
"files": [
{
"type": "image",
"url": image["url"],
}
for image in images
]
},
}
)
if bracket_start == -1 or bracket_end == -1:
raise Exception("No JSON object found in the response")
system_message_content = "<context>User is shown the generated image, tell the user that the image has been generated</context>"
except Exception as e:
log.exception(e)
await __event_emitter__(
{
"type": "status",
"data": {
"description": f"An error occurred while generating an image",
"done": True,
},
}
)
response = response[bracket_start:bracket_end]
response = json.loads(response)
prompt = response.get("prompt", [])
except Exception as e:
prompt = user_message
system_message_content = "<context>Unable to generate an image, tell the user that an error occurred</context>"
except Exception as e:
log.exception(e)
prompt = user_message
try:
images = await image_generations(
request=request,
form_data=CreateImageForm(**{"prompt": prompt}),
user=user,
)
await __event_emitter__(
{
"type": "status",
"data": {"description": "Image created", "done": True},
}
)
await __event_emitter__(
{
"type": "files",
"data": {
"files": [
{
"type": "image",
"url": image["url"],
}
for image in images
]
},
}
)
system_message_content = "<context>The requested image has been created and is now being shown to the user. Let them know that it has been generated.</context>"
except Exception as e:
log.debug(e)
error_message = ""
if isinstance(e, HTTPException):
if e.detail and isinstance(e.detail, dict):
error_message = e.detail.get("message", str(e.detail))
else:
error_message = str(e.detail)
await __event_emitter__(
{
"type": "status",
"data": {
"description": f"An error occurred while generating an image",
"done": True,
},
}
)
system_message_content = f"<context>Image generation was attempted but failed. The system is currently unable to generate the image. Tell the user that an error occurred: {error_message}</context>"
else:
# Edit image(s)
try:
images = await image_edits(
request=request,
form_data=EditImageForm(**{"prompt": prompt, "image": input_images}),
user=user,
)
await __event_emitter__(
{
"type": "status",
"data": {"description": "Image created", "done": True},
}
)
await __event_emitter__(
{
"type": "files",
"data": {
"files": [
{
"type": "image",
"url": image["url"],
}
for image in images
]
},
}
)
system_message_content = "<context>The requested image has been created and is now being shown to the user. Let them know that it has been generated.</context>"
except Exception as e:
log.debug(e)
error_message = ""
if isinstance(e, HTTPException):
if e.detail and isinstance(e.detail, dict):
error_message = e.detail.get("message", str(e.detail))
else:
error_message = str(e.detail)
await __event_emitter__(
{
"type": "status",
"data": {
"description": f"An error occurred while generating an image",
"done": True,
},
}
)
system_message_content = f"<context>Image generation was attempted but failed. The system is currently unable to generate the image. Tell the user that an error occurred: {error_message}</context>"
if system_message_content:
form_data["messages"] = add_or_update_system_message(
@@ -853,10 +945,6 @@ async def chat_completion_files_handler(
except:
pass
if len(queries) == 0:
queries = [get_last_user_message(body["messages"])]
if not all_full_context:
await __event_emitter__(
{
"type": "status",
@@ -868,6 +956,9 @@ async def chat_completion_files_handler(
}
)
if len(queries) == 0:
queries = [get_last_user_message(body["messages"])]
try:
# Offload get_sources_from_items to a separate thread
loop = asyncio.get_running_loop()
@@ -906,7 +997,6 @@ async def chat_completion_files_handler(
log.debug(f"rag_contexts:sources: {sources}")
unique_ids = set()
for source in sources or []:
if not source or len(source.keys()) == 0:
continue
@@ -925,7 +1015,6 @@ async def chat_completion_files_handler(
unique_ids.add(_id)
sources_count = len(unique_ids)
await __event_emitter__(
{
"type": "status",
@@ -999,11 +1088,11 @@ async def process_chat_payload(request, form_data, user, metadata, model):
log.debug(f"form_data: {form_data}")
system_message = get_system_message(form_data.get("messages", []))
if system_message:
if system_message: # Chat Controls/User Settings
try:
form_data = apply_system_prompt_to_body(
system_message.get("content"), form_data, metadata, user
)
system_message.get("content"), form_data, metadata, user, replace=True
) # Required to handle system prompt variables
except:
pass
@@ -1168,8 +1257,28 @@ async def process_chat_payload(request, form_data, user, metadata, model):
tool_ids = form_data.pop("tool_ids", None)
files = form_data.pop("files", None)
# Remove files duplicates
prompt = get_last_user_message(form_data["messages"])
# TODO: re-enable URL extraction from prompt
# urls = []
# if prompt and len(prompt or "") < 500 and (not files or len(files) == 0):
# urls = extract_urls(prompt)
if files:
if not files:
files = []
for file_item in files:
if file_item.get("type", "file") == "folder":
# Get folder files
folder_id = file_item.get("id", None)
if folder_id:
folder = Folders.get_folder_by_id_and_user_id(folder_id, user.id)
if folder and folder.data and "files" in folder.data:
files = [f for f in files if f.get("id", None) != folder_id]
files = [*files, *folder.data["files"]]
# files = [*files, *[{"type": "url", "url": url, "name": url} for url in urls]]
# Remove duplicate files based on their content
files = list({json.dumps(f, sort_keys=True): f for f in files}.values())
metadata = {
@@ -1261,9 +1370,6 @@ async def process_chat_payload(request, form_data, user, metadata, model):
def make_tool_function(client, function_name):
async def tool_function(**kwargs):
print(kwargs)
print(client)
print(await client.list_tool_specs())
return await client.call_tool(
function_name,
function_args=kwargs,
@@ -1287,6 +1393,17 @@ async def process_chat_payload(request, form_data, user, metadata, model):
}
except Exception as e:
log.debug(e)
if event_emitter:
await event_emitter(
{
"type": "chat:message:error",
"data": {
"error": {
"content": f"Failed to connect to MCP server '{server_id}'"
}
},
}
)
continue
tools_dict = await get_tools(
@@ -1370,8 +1487,6 @@ async def process_chat_payload(request, form_data, user, metadata, model):
)
context_string = context_string.strip()
prompt = get_last_user_message(form_data["messages"])
if prompt is None:
raise Exception("No user message found")
@@ -1410,10 +1525,6 @@ async def process_chat_payload(request, form_data, user, metadata, model):
}
)
print("Final form_data:", form_data)
print("Final metadata:", metadata)
print("Final events:", events)
return form_data, metadata, events
@@ -1421,10 +1532,13 @@ async def process_chat_response(
request, response, form_data, user, metadata, model, events, tasks
):
async def background_tasks_handler():
messages_map = Chats.get_messages_map_by_chat_id(metadata["chat_id"])
message = messages_map.get(metadata["message_id"]) if messages_map else None
message = None
messages = []
if "chat_id" in metadata and not metadata["chat_id"].startswith("local:"):
messages_map = Chats.get_messages_map_by_chat_id(metadata["chat_id"])
message = messages_map.get(metadata["message_id"]) if messages_map else None
if message:
message_list = get_message_list(messages_map, metadata["message_id"])
# Remove details tags and files from the messages.
@@ -1457,7 +1571,14 @@ async def process_chat_response(
"content": content,
}
)
else:
# Local temp chat, get the model and message from the form_data
message = get_last_user_message_item(form_data.get("messages", []))
messages = form_data.get("messages", [])
if message:
message["model"] = form_data.get("model")
if message and "model" in message:
if tasks and messages:
if (
TASKS.FOLLOW_UP_GENERATION in tasks
@@ -1476,11 +1597,13 @@ async def process_chat_response(
if res and isinstance(res, dict):
if len(res.get("choices", [])) == 1:
follow_ups_string = (
res.get("choices", [])[0]
.get("message", {})
.get("content", "")
response_message = res.get("choices", [])[0].get(
"message", {}
)
follow_ups_string = response_message.get(
"content"
) or response_message.get("reasoning_content", "")
else:
follow_ups_string = ""
@@ -1493,15 +1616,6 @@ async def process_chat_response(
follow_ups = json.loads(follow_ups_string).get(
"follow_ups", []
)
Chats.upsert_message_to_chat_by_id_and_message_id(
metadata["chat_id"],
metadata["message_id"],
{
"followUps": follow_ups,
},
)
await event_emitter(
{
"type": "chat:message:follow_ups",
@@ -1510,17 +1624,94 @@ async def process_chat_response(
},
}
)
if not metadata.get("chat_id", "").startswith("local:"):
Chats.upsert_message_to_chat_by_id_and_message_id(
metadata["chat_id"],
metadata["message_id"],
{
"followUps": follow_ups,
},
)
except Exception as e:
pass
if TASKS.TITLE_GENERATION in tasks:
user_message = get_last_user_message(messages)
if user_message and len(user_message) > 100:
user_message = user_message[:100] + "..."
if not metadata.get("chat_id", "").startswith(
"local:"
): # Only update titles and tags for non-temp chats
if TASKS.TITLE_GENERATION in tasks:
user_message = get_last_user_message(messages)
if user_message and len(user_message) > 100:
user_message = user_message[:100] + "..."
if tasks[TASKS.TITLE_GENERATION]:
title = None
if tasks[TASKS.TITLE_GENERATION]:
res = await generate_title(
request,
{
"model": message["model"],
"messages": messages,
"chat_id": metadata["chat_id"],
},
user,
)
res = await generate_title(
if res and isinstance(res, dict):
if len(res.get("choices", [])) == 1:
response_message = res.get("choices", [])[0].get(
"message", {}
)
title_string = (
response_message.get("content")
or response_message.get(
"reasoning_content",
)
or message.get("content", user_message)
)
else:
title_string = ""
title_string = title_string[
title_string.find("{") : title_string.rfind("}") + 1
]
try:
title = json.loads(title_string).get(
"title", user_message
)
except Exception as e:
title = ""
if not title:
title = messages[0].get("content", user_message)
Chats.update_chat_title_by_id(
metadata["chat_id"], title
)
await event_emitter(
{
"type": "chat:title",
"data": title,
}
)
if title == None and len(messages) == 2:
title = messages[0].get("content", user_message)
Chats.update_chat_title_by_id(metadata["chat_id"], title)
await event_emitter(
{
"type": "chat:title",
"data": message.get("content", user_message),
}
)
if TASKS.TAGS_GENERATION in tasks and tasks[TASKS.TAGS_GENERATION]:
res = await generate_chat_tags(
request,
{
"model": message["model"],
@@ -1532,89 +1723,34 @@ async def process_chat_response(
if res and isinstance(res, dict):
if len(res.get("choices", [])) == 1:
title_string = (
res.get("choices", [])[0]
.get("message", {})
.get(
"content", message.get("content", user_message)
)
response_message = res.get("choices", [])[0].get(
"message", {}
)
else:
title_string = ""
title_string = title_string[
title_string.find("{") : title_string.rfind("}") + 1
tags_string = response_message.get(
"content"
) or response_message.get("reasoning_content", "")
else:
tags_string = ""
tags_string = tags_string[
tags_string.find("{") : tags_string.rfind("}") + 1
]
try:
title = json.loads(title_string).get(
"title", user_message
tags = json.loads(tags_string).get("tags", [])
Chats.update_chat_tags_by_id(
metadata["chat_id"], tags, user
)
await event_emitter(
{
"type": "chat:tags",
"data": tags,
}
)
except Exception as e:
title = ""
if not title:
title = messages[0].get("content", user_message)
Chats.update_chat_title_by_id(metadata["chat_id"], title)
await event_emitter(
{
"type": "chat:title",
"data": title,
}
)
elif len(messages) == 2:
title = messages[0].get("content", user_message)
Chats.update_chat_title_by_id(metadata["chat_id"], title)
await event_emitter(
{
"type": "chat:title",
"data": message.get("content", user_message),
}
)
if TASKS.TAGS_GENERATION in tasks and tasks[TASKS.TAGS_GENERATION]:
res = await generate_chat_tags(
request,
{
"model": message["model"],
"messages": messages,
"chat_id": metadata["chat_id"],
},
user,
)
if res and isinstance(res, dict):
if len(res.get("choices", [])) == 1:
tags_string = (
res.get("choices", [])[0]
.get("message", {})
.get("content", "")
)
else:
tags_string = ""
tags_string = tags_string[
tags_string.find("{") : tags_string.rfind("}") + 1
]
try:
tags = json.loads(tags_string).get("tags", [])
Chats.update_chat_tags_by_id(
metadata["chat_id"], tags, user
)
await event_emitter(
{
"type": "chat:tags",
"data": tags,
}
)
except Exception as e:
pass
pass
event_emitter = None
event_caller = None
@@ -1642,7 +1778,9 @@ async def process_chat_response(
response.body, bytes
):
try:
response_data = json.loads(response.body.decode("utf-8"))
response_data = json.loads(
response.body.decode("utf-8", "replace")
)
except json.JSONDecodeError:
response_data = {
"error": {"detail": "Invalid JSON response"}
@@ -1896,9 +2034,11 @@ async def process_chat_response(
content = f"{content}{tool_calls_display_content}"
elif block["type"] == "reasoning":
reasoning_display_content = "\n".join(
(f"> {line}" if not line.startswith(">") else line)
for line in block["content"].splitlines()
reasoning_display_content = html.escape(
"\n".join(
(f"> {line}" if not line.startswith(">") else line)
for line in block["content"].splitlines()
)
)
reasoning_duration = block.get("duration", None)
@@ -2276,7 +2416,11 @@ async def process_chat_response(
last_delta_data = None
async for line in response.body_iterator:
line = line.decode("utf-8") if isinstance(line, bytes) else line
line = (
line.decode("utf-8", "replace")
if isinstance(line, bytes)
else line
)
data = line
# Skip empty lines
@@ -2302,7 +2446,9 @@ async def process_chat_response(
)
if data:
if "event" in data:
if "event" in data and not getattr(
request.state, "direct", False
):
await event_emitter(data.get("event", {}))
if "selected_model_id" in data:
@@ -2619,8 +2765,6 @@ async def process_chat_response(
results = []
for tool_call in response_tool_calls:
print("tool_call", tool_call)
tool_call_id = tool_call.get("id", "")
tool_function_name = tool_call.get("function", {}).get(
"name", ""
@@ -2695,7 +2839,16 @@ async def process_chat_response(
)
else:
tool_function = tool["callable"]
tool_function = await get_updated_tool_function(
function=tool["callable"],
extra_params={
"__messages__": form_data.get(
"messages", []
),
"__files__": metadata.get("files", []),
},
)
tool_result = await tool_function(
**tool_function_params
)
@@ -2751,9 +2904,9 @@ async def process_chat_response(
try:
new_form_data = {
**form_data,
"model": model_id,
"stream": True,
"tools": form_data["tools"],
"messages": [
*form_data["messages"],
*convert_content_blocks_to_messages(
@@ -2927,6 +3080,7 @@ async def process_chat_response(
try:
new_form_data = {
**form_data,
"model": model_id,
"stream": True,
"messages": [
+16
View File
@@ -136,6 +136,14 @@ def update_message_content(message: dict, content: str, append: bool = True) ->
return message
def replace_system_message_content(content: str, messages: list[dict]) -> dict:
for message in messages:
if message["role"] == "system":
message["content"] = content
break
return messages
def add_or_update_system_message(
content: str, messages: list[dict], append: bool = False
):
@@ -523,3 +531,11 @@ def throttle(interval: float = 10.0):
return wrapper
return decorator
def extract_urls(text: str) -> list[str]:
# Regex pattern to match URLs
url_pattern = re.compile(
r"(https?://[^\s]+)", re.IGNORECASE
) # Matches http and https URLs
return url_pattern.findall(text)
+44 -31
View File
@@ -166,13 +166,18 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None)
action_ids = []
filter_ids = []
if "info" in model and "meta" in model["info"]:
action_ids.extend(
model["info"]["meta"].get("actionIds", [])
)
filter_ids.extend(
model["info"]["meta"].get("filterIds", [])
)
if "info" in model:
if "meta" in model["info"]:
action_ids.extend(
model["info"]["meta"].get("actionIds", [])
)
filter_ids.extend(
model["info"]["meta"].get("filterIds", [])
)
if "params" in model["info"]:
# Remove params to avoid exposing sensitive info
del model["info"]["params"]
model["action_ids"] = action_ids
model["filter_ids"] = filter_ids
@@ -182,22 +187,40 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None)
elif custom_model.is_active and (
custom_model.id not in [model["id"] for model in models]
):
# Custom model based on a base model
owned_by = "openai"
pipe = None
for m in models:
if (
custom_model.base_model_id == m["id"]
or custom_model.base_model_id == m["id"].split(":")[0]
):
owned_by = m.get("owned_by", "unknown")
if "pipe" in m:
pipe = m["pipe"]
break
model = {
"id": f"{custom_model.id}",
"name": custom_model.name,
"object": "model",
"created": custom_model.created_at,
"owned_by": owned_by,
"preset": True,
**({"pipe": pipe} if pipe is not None else {}),
}
info = custom_model.model_dump()
if "params" in info:
# Remove params to avoid exposing sensitive info
del info["params"]
model["info"] = info
action_ids = []
filter_ids = []
for model in models:
if (
custom_model.base_model_id == model["id"]
or custom_model.base_model_id == model["id"].split(":")[0]
):
owned_by = model.get("owned_by", "unknown owner")
if "pipe" in model:
pipe = model["pipe"]
break
if custom_model.meta:
meta = custom_model.meta.model_dump()
@@ -207,20 +230,10 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None)
if "filterIds" in meta:
filter_ids.extend(meta["filterIds"])
models.append(
{
"id": f"{custom_model.id}",
"name": custom_model.name,
"object": "model",
"created": custom_model.created_at,
"owned_by": owned_by,
"info": custom_model.model_dump(),
"preset": True,
**({"pipe": pipe} if pipe is not None else {}),
"action_ids": action_ids,
"filter_ids": filter_ids,
}
)
model["action_ids"] = action_ids
model["filter_ids"] = filter_ids
models.append(model)
# Process action_ids to get the actions
def get_action_items_from_module(function, module):
+188 -28
View File
@@ -1,4 +1,5 @@
import base64
import copy
import hashlib
import logging
import mimetypes
@@ -62,6 +63,7 @@ from open_webui.env import (
WEBUI_AUTH_COOKIE_SAME_SITE,
WEBUI_AUTH_COOKIE_SECURE,
ENABLE_OAUTH_ID_TOKEN_COOKIE,
ENABLE_OAUTH_EMAIL_FALLBACK,
OAUTH_CLIENT_INFO_ENCRYPTION_KEY,
)
from open_webui.utils.misc import parse_duration
@@ -73,6 +75,8 @@ from mcp.shared.auth import (
OAuthMetadata,
)
from authlib.oauth2.rfc6749.errors import OAuth2Error
class OAuthClientInformationFull(OAuthClientMetadata):
issuer: Optional[str] = None # URL of the OAuth server that issued this client
@@ -82,6 +86,8 @@ class OAuthClientInformationFull(OAuthClientMetadata):
client_id_issued_at: int | None = None
client_secret_expires_at: int | None = None
server_metadata: Optional[OAuthMetadata] = None # Fetched from the OAuth server
from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL
@@ -147,6 +153,37 @@ def decrypt_data(data: str):
raise
def _build_oauth_callback_error_message(e: Exception) -> str:
"""
Produce a user-facing callback error string with actionable context.
Keeps the message short and strips newlines for safe redirect usage.
"""
if isinstance(e, OAuth2Error):
parts = [p for p in [e.error, e.description] if p]
detail = " - ".join(parts)
elif isinstance(e, HTTPException):
detail = e.detail if isinstance(e.detail, str) else str(e.detail)
elif isinstance(e, aiohttp.ClientResponseError):
detail = f"Upstream provider returned {e.status}: {e.message}"
elif isinstance(e, aiohttp.ClientError):
detail = str(e)
elif isinstance(e, KeyError):
missing = str(e).strip("'")
if missing.lower() == "state":
detail = "Missing state parameter in callback (session may have expired)"
else:
detail = f"Missing expected key '{missing}' in OAuth response"
else:
detail = str(e)
detail = detail.replace("\n", " ").strip()
if not detail:
detail = e.__class__.__name__
message = f"OAuth callback failed: {detail}"
return message[:197] + "..." if len(message) > 200 else message
def is_in_blocked_groups(group_name: str, groups: list) -> bool:
"""
Check if a group name matches any blocked pattern.
@@ -248,7 +285,7 @@ async def get_oauth_client_info_with_dynamic_client_registration(
# Attempt to fetch OAuth server metadata to get registration endpoint & scopes
discovery_urls = get_discovery_urls(oauth_server_url)
for url in discovery_urls:
async with aiohttp.ClientSession() as session:
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get(
url, ssl=AIOHTTP_CLIENT_SESSION_SSL
) as oauth_server_metadata_response:
@@ -284,7 +321,7 @@ async def get_oauth_client_info_with_dynamic_client_registration(
)
# Perform dynamic client registration and return client info
async with aiohttp.ClientSession() as session:
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.post(
registration_url, json=registration_data, ssl=AIOHTTP_CLIENT_SESSION_SSL
) as oauth_client_registration_response:
@@ -296,6 +333,7 @@ async def get_oauth_client_info_with_dynamic_client_registration(
{
**registration_response_json,
**{"issuer": oauth_server_metadata_url},
**{"server_metadata": oauth_server_metadata},
}
)
log.info(
@@ -331,20 +369,34 @@ class OAuthClientManager:
self.clients = {}
def add_client(self, client_id, oauth_client_info: OAuthClientInformationFull):
self.clients[client_id] = {
"client": self.oauth.register(
name=client_id,
client_id=oauth_client_info.client_id,
client_secret=oauth_client_info.client_secret,
client_kwargs=(
{"scope": oauth_client_info.scope}
if oauth_client_info.scope
else {}
),
server_metadata_url=(
oauth_client_info.issuer if oauth_client_info.issuer else None
),
kwargs = {
"name": client_id,
"client_id": oauth_client_info.client_id,
"client_secret": oauth_client_info.client_secret,
"client_kwargs": (
{"scope": oauth_client_info.scope} if oauth_client_info.scope else {}
),
"server_metadata_url": (
oauth_client_info.issuer if oauth_client_info.issuer else None
),
}
if (
oauth_client_info.server_metadata
and oauth_client_info.server_metadata.code_challenge_methods_supported
):
if (
isinstance(
oauth_client_info.server_metadata.code_challenge_methods_supported,
list,
)
and "S256"
in oauth_client_info.server_metadata.code_challenge_methods_supported
):
kwargs["code_challenge_method"] = "S256"
self.clients[client_id] = {
"client": self.oauth.register(**kwargs),
"client_info": oauth_client_info,
}
return self.clients[client_id]
@@ -353,6 +405,82 @@ class OAuthClientManager:
if client_id in self.clients:
del self.clients[client_id]
log.info(f"Removed OAuth client {client_id}")
if hasattr(self.oauth, "_clients"):
if client_id in self.oauth._clients:
self.oauth._clients.pop(client_id, None)
if hasattr(self.oauth, "_registry"):
if client_id in self.oauth._registry:
self.oauth._registry.pop(client_id, None)
return True
async def _preflight_authorization_url(
self, client, client_info: OAuthClientInformationFull
) -> bool:
# TODO: Replace this logic with a more robust OAuth client registration validation
# Only perform preflight checks for Starlette OAuth clients
if not hasattr(client, "create_authorization_url"):
return True
redirect_uri = None
if client_info.redirect_uris:
redirect_uri = str(client_info.redirect_uris[0])
try:
auth_data = await client.create_authorization_url(redirect_uri=redirect_uri)
authorization_url = auth_data.get("url")
if not authorization_url:
return True
except Exception as e:
log.debug(
f"Skipping OAuth preflight for client {client_info.client_id}: {e}",
)
return True
try:
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get(
authorization_url,
allow_redirects=False,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as resp:
if resp.status < 400:
return True
response_text = await resp.text()
error = None
error_description = ""
content_type = resp.headers.get("content-type", "")
if "application/json" in content_type:
try:
payload = json.loads(response_text)
error = payload.get("error")
error_description = payload.get("error_description", "")
except:
pass
else:
error_description = response_text
error_message = f"{error or ''} {error_description or ''}".lower()
if any(
keyword in error_message
for keyword in ("invalid_client", "invalid client", "client id")
):
log.warning(
f"OAuth client preflight detected invalid registration for {client_info.client_id}: {error} {error_description}"
)
return False
except Exception as e:
log.debug(
f"Skipping OAuth preflight network check for client {client_info.client_id}: {e}"
)
return True
def get_client(self, client_id):
@@ -367,8 +495,8 @@ class OAuthClientManager:
if client_id in self.clients:
client = self.clients[client_id]
return (
client.server_metadata_url
if hasattr(client, "server_metadata_url")
client._server_metadata_url
if hasattr(client, "_server_metadata_url")
else None
)
return None
@@ -543,7 +671,6 @@ class OAuthClientManager:
client = self.get_client(client_id)
if client is None:
raise HTTPException(404)
client_info = self.get_client_info(client_id)
if client_info is None:
raise HTTPException(404)
@@ -551,7 +678,8 @@ class OAuthClientManager:
redirect_uri = (
client_info.redirect_uris[0] if client_info.redirect_uris else None
)
return await client.authorize_redirect(request, str(redirect_uri))
redirect_uri_str = str(redirect_uri) if redirect_uri else None
return await client.authorize_redirect(request, redirect_uri_str)
async def handle_callback(self, request, client_id: str, user_id: str, response):
client = self.get_client(client_id)
@@ -560,7 +688,17 @@ class OAuthClientManager:
error_message = None
try:
token = await client.authorize_access_token(request)
client_info = self.get_client_info(client_id)
token_params = {}
if (
client_info
and hasattr(client_info, "client_id")
and hasattr(client_info, "client_secret")
):
token_params["client_id"] = client_info.client_id
token_params["client_secret"] = client_info.client_secret
token = await client.authorize_access_token(request, **token_params)
if token:
try:
# Add timestamp for tracking
@@ -593,8 +731,14 @@ class OAuthClientManager:
error_message = "Failed to obtain OAuth token"
log.warning(error_message)
except Exception as e:
error_message = "OAuth callback error"
log.warning(f"OAuth callback error: {e}")
error_message = _build_oauth_callback_error_message(e)
log.warning(
"OAuth callback error for user_id=%s client_id=%s: %s",
user_id,
client_id,
error_message,
exc_info=True,
)
redirect_url = (
str(request.app.state.config.WEBUI_URL or request.base_url)
@@ -602,7 +746,9 @@ class OAuthClientManager:
if error_message:
log.debug(error_message)
redirect_url = f"{redirect_url}/?error={error_message}"
redirect_url = (
f"{redirect_url}/?error={urllib.parse.quote_plus(error_message)}"
)
return RedirectResponse(url=redirect_url, headers=response.headers)
response = RedirectResponse(url=redirect_url, headers=response.headers)
@@ -615,8 +761,14 @@ class OAuthManager:
self.app = app
self._clients = {}
for _, provider_config in OAUTH_PROVIDERS.items():
provider_config["register"](self.oauth)
for name, provider_config in OAUTH_PROVIDERS.items():
if "register" not in provider_config:
log.error(f"OAuth provider {name} missing register function")
continue
client = provider_config["register"](self.oauth)
self._clients[name] = client
def get_client(self, provider_name):
if provider_name not in self._clients:
@@ -627,8 +779,8 @@ class OAuthManager:
if provider_name in self._clients:
client = self._clients[provider_name]
return (
client.server_metadata_url
if hasattr(client, "server_metadata_url")
client._server_metadata_url
if hasattr(client, "_server_metadata_url")
else None
)
return None
@@ -1070,7 +1222,13 @@ class OAuthManager:
try:
token = await client.authorize_access_token(request)
except Exception as e:
log.warning(f"OAuth callback error: {e}")
detailed_error = _build_oauth_callback_error_message(e)
log.warning(
"OAuth callback error during authorize_access_token for provider %s: %s",
provider,
detailed_error,
exc_info=True,
)
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
# Try to get userinfo from the token first, some providers include it there
@@ -1147,6 +1305,8 @@ class OAuthManager:
except Exception as e:
log.warning(f"Error fetching GitHub email: {e}")
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
elif ENABLE_OAUTH_EMAIL_FALLBACK:
email = f"{provider_sub}.local"
else:
log.warning(f"OAuth callback failed, email is missing: {user_data}")
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
+19 -4
View File
@@ -2,6 +2,7 @@ from open_webui.utils.task import prompt_template, prompt_variables_template
from open_webui.utils.misc import (
deep_update,
add_or_update_system_message,
replace_system_message_content,
)
from typing import Callable, Optional
@@ -10,7 +11,11 @@ import json
# inplace function: form_data is modified
def apply_system_prompt_to_body(
system: Optional[str], form_data: dict, metadata: Optional[dict] = None, user=None
system: Optional[str],
form_data: dict,
metadata: Optional[dict] = None,
user=None,
replace: bool = False,
) -> dict:
if not system:
return form_data
@@ -24,9 +29,15 @@ def apply_system_prompt_to_body(
# Legacy (API Usage)
system = prompt_template(system, user)
form_data["messages"] = add_or_update_system_message(
system, form_data.get("messages", [])
)
if replace:
form_data["messages"] = replace_system_message_content(
system, form_data.get("messages", [])
)
else:
form_data["messages"] = add_or_update_system_message(
system, form_data.get("messages", [])
)
return form_data
@@ -286,6 +297,10 @@ def convert_payload_openai_to_ollama(openai_payload: dict) -> dict:
if "tools" in openai_payload:
ollama_payload["tools"] = openai_payload["tools"]
if "max_tokens" in openai_payload:
ollama_payload["num_predict"] = openai_payload["max_tokens"]
del openai_payload["max_tokens"]
# If there are advanced parameters in the payload, format them in Ollama's options field
if openai_payload.get("options"):
ollama_payload["options"] = openai_payload["options"]
+18 -5
View File
@@ -85,9 +85,26 @@ def get_async_tool_function_and_apply_extra_params(
update_wrapper(new_function, function)
new_function.__signature__ = new_sig
new_function.__function__ = function # type: ignore
new_function.__extra_params__ = extra_params # type: ignore
return new_function
async def get_updated_tool_function(function: Callable, extra_params: dict):
# Get the original function and merge updated params
__function__ = getattr(function, "__function__", None)
__extra_params__ = getattr(function, "__extra_params__", None)
if __function__ is not None and __extra_params__ is not None:
return await get_async_tool_function_and_apply_extra_params(
__function__,
{**__extra_params__, **extra_params},
)
return function
async def get_tools(
request: Request, tool_ids: list[str], user: UserModel, extra_params: dict
) -> dict[str, dict]:
@@ -748,17 +765,13 @@ async def execute_tool_server(
if operation.get("requestBody", {}).get("content"):
if params:
body_params = params
else:
raise Exception(
f"Request body expected for operation '{name}' but none found."
)
async with aiohttp.ClientSession(
trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
) as session:
request_method = getattr(session, http_method.lower())
if http_method in ["post", "put", "patch"]:
if http_method in ["post", "put", "patch", "delete"]:
async with request_method(
final_url,
json=body_params,
+1 -1
View File
@@ -51,7 +51,7 @@ async def post_webhook(name: str, url: str, message: str, event_data: dict) -> b
payload = {**event_data}
log.debug(f"payload: {payload}")
async with aiohttp.ClientSession() as session:
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.post(url, json=payload) as r:
r_text = await r.text()
r.raise_for_status()
+46 -49
View File
@@ -1,17 +1,16 @@
fastapi==0.115.7
uvicorn[standard]==0.35.0
pydantic==2.11.7
fastapi==0.118.0
uvicorn[standard]==0.37.0
pydantic==2.11.9
python-multipart==0.0.20
itsdangerous==2.2.0
python-socketio==5.13.0
python-jose==3.4.0
passlib[bcrypt]==1.7.4
python-jose==3.5.0
cryptography
bcrypt==4.3.0
bcrypt==5.0.0
argon2-cffi==25.1.0
PyJWT[crypto]==2.10.1
authlib==1.6.3
authlib==1.6.5
requests==2.32.5
aiohttp==3.12.15
@@ -30,14 +29,6 @@ peewee-migrate==1.12.2
pycrdt==0.12.25
redis
pymongo
psycopg2-binary==2.9.10
pgvector==0.4.1
PyMySQL==1.1.1
boto3==1.40.5
APScheduler==3.10.4
RestrictedPython==8.0
@@ -57,34 +48,24 @@ langchain==0.3.27
langchain-community==0.3.29
fake-useragent==2.2.0
chromadb==1.0.20
chromadb==1.1.0
opensearch-py==2.8.0
pymilvus==2.5.0
qdrant-client==1.14.3
playwright==1.49.1 # Caution: version must match docker-compose.playwright.yaml
elasticsearch==9.1.0
pinecone==6.0.2
oracledb==3.2.0
av==14.0.1 # Caution: Set due to FATAL FIPS SELFTEST FAILURE, see discussion https://github.com/open-webui/open-webui/discussions/15720
transformers
sentence-transformers==5.1.1
accelerate
pyarrow==20.0.0 # fix: pin pyarrow version to 20 for rpi compatibility #15897
einops==0.8.1
colbert-ai==0.2.21
ftfy==6.2.3
pypdf==6.0.0
fpdf2==2.8.2
pymdown-extensions==10.14.2
docx2txt==0.8
python-pptx==1.0.2
unstructured==0.16.17
unstructured==0.18.15
nltk==3.9.1
Markdown==3.8.2
Markdown==3.9
pypandoc==1.15
pandas==2.2.3
openpyxl==3.1.5
@@ -105,7 +86,7 @@ onnxruntime==1.20.1
faster-whisper==1.1.1
black==25.1.0
black==25.9.0
youtube-transcript-api==1.2.2
pytube==15.0.0
@@ -117,11 +98,6 @@ google-api-python-client
google-auth-httplib2
google-auth-oauthlib
## Tests
docker~=7.1.0
pytest~=8.4.1
pytest-docker~=3.1.1
googleapis-common-protos==1.70.0
google-cloud-storage==2.19.0
@@ -129,24 +105,45 @@ azure-identity==1.25.0
azure-storage-blob==12.24.1
pymongo
psycopg2-binary==2.9.10
pgvector==0.4.1
PyMySQL==1.1.1
boto3==1.40.5
pymilvus==2.6.2
qdrant-client==1.14.3
playwright==1.49.1 # Caution: version must match docker-compose.playwright.yaml
elasticsearch==9.1.0
pinecone==6.0.2
oracledb==3.2.0
av==14.0.1 # Caution: Set due to FATAL FIPS SELFTEST FAILURE, see discussion https://github.com/open-webui/open-webui/discussions/15720
colbert-ai==0.2.21
## Tests
docker~=7.1.0
pytest~=8.4.1
pytest-docker~=3.1.1
## LDAP
ldap3==2.9.1
## Firecrawl
firecrawl-py==1.12.0
# Sougou API SDK(Tencentcloud SDK)
tencentcloud-sdk-python==3.0.1336
firecrawl-py==4.5.0
## Trace
opentelemetry-api==1.36.0
opentelemetry-sdk==1.36.0
opentelemetry-exporter-otlp==1.36.0
opentelemetry-instrumentation==0.57b0
opentelemetry-instrumentation-fastapi==0.57b0
opentelemetry-instrumentation-sqlalchemy==0.57b0
opentelemetry-instrumentation-redis==0.57b0
opentelemetry-instrumentation-requests==0.57b0
opentelemetry-instrumentation-logging==0.57b0
opentelemetry-instrumentation-httpx==0.57b0
opentelemetry-instrumentation-aiohttp-client==0.57b0
opentelemetry-api==1.37.0
opentelemetry-sdk==1.37.0
opentelemetry-exporter-otlp==1.37.0
opentelemetry-instrumentation==0.58b0
opentelemetry-instrumentation-fastapi==0.58b0
opentelemetry-instrumentation-sqlalchemy==0.58b0
opentelemetry-instrumentation-redis==0.58b0
opentelemetry-instrumentation-requests==0.58b0
opentelemetry-instrumentation-logging==0.58b0
opentelemetry-instrumentation-httpx==0.58b0
opentelemetry-instrumentation-aiohttp-client==0.58b0
+14 -1
View File
@@ -70,5 +70,18 @@ if [ -n "$SPACE_ID" ]; then
fi
PYTHON_CMD=$(command -v python3 || command -v python)
UVICORN_WORKERS="${UVICORN_WORKERS:-1}"
WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" exec "$PYTHON_CMD" -m uvicorn open_webui.main:app --host "$HOST" --port "$PORT" --forwarded-allow-ips '*' --workers "${UVICORN_WORKERS:-1}"
# If script is called with arguments, use them; otherwise use default workers
if [ "$#" -gt 0 ]; then
ARGS=("$@")
else
ARGS=(--workers "$UVICORN_WORKERS")
fi
# Run uvicorn
WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" exec "$PYTHON_CMD" -m uvicorn open_webui.main:app \
--host "$HOST" \
--port "$PORT" \
--forwarded-allow-ips '*' \
"${ARGS[@]}"
-2
View File
@@ -11,8 +11,6 @@ services:
open-webui:
build:
context: .
args:
OLLAMA_BASE_URL: '/ollama'
dockerfile: Dockerfile
image: ghcr.io/open-webui/open-webui:${WEBUI_DOCKER_TAG-main}
container_name: open-webui
+17 -2
View File
@@ -24,6 +24,10 @@ Noticed something off? Have an idea? Check our [Issues tab](https://github.com/o
> - **Template Compliance:** Please be aware that failure to follow the provided issue template, or not providing the requested information at all, will likely result in your issue being closed without further consideration. This approach is critical for maintaining the manageability and integrity of issue tracking.
> - **Detail is Key:** To ensure your issue is understood and can be effectively addressed, it's imperative to include comprehensive details. Descriptions should be clear, including steps to reproduce, expected outcomes, and actual results. Lack of sufficient detail may hinder our ability to resolve your issue.
> [!WARNING]
> Reporting vulnerabilities is not wanted through Issues!
> Instead, [use the security reporting functionality](https://github.com/open-webui/open-webui/security) and ensure you comply with the outlined requirements.
### 🧭 Scope of Support
We've noticed an uptick in issues not directly related to Open WebUI but rather to the environment it's run in, especially Docker setups. While we strive to support Docker deployment, understanding Docker fundamentals is crucial for a smooth experience.
@@ -32,6 +36,8 @@ We've noticed an uptick in issues not directly related to Open WebUI but rather
- **Advanced Configurations**: Setting up reverse proxies for HTTPS and managing Docker deployments requires foundational knowledge. There are numerous online resources available to learn these skills. Ensuring you have this knowledge will greatly enhance your experience with Open WebUI and similar projects.
- **Check the documentation and help improve it**: [Our documentation](https://docs.openwebui.com) has ever growing troubleshooting guides and detailed installation tutorials. Please verify if it is of help to your issue and help expand it by submitting issues and PRs on our [Docs Repository](https://github.com/open-webui/docs).
## 💡 Contributing
Looking to contribute? Great! Here's how you can help:
@@ -46,9 +52,15 @@ We welcome pull requests. Before submitting one, please:
4. Write clear, descriptive commit messages.
5. It's essential to complete your pull request in a timely manner. We move fast, and having PRs hang around too long is not feasible. If you can't get it done within a reasonable time frame, we may have to close it to keep the project moving forward.
> [!NOTE]
> The Pull Request Template has various requirements outlined. Go through the PR-checklist one by one and ensure you completed all steps before submitting your PR for review (you can open it as draft otherwise!).
### 📚 Documentation & Tutorials
Help us make Open WebUI more accessible by improving documentation, writing tutorials, or creating guides on setting up and optimizing the web UI.
Help us make Open WebUI more accessible by improving the documentation, writing tutorials, or creating guides on setting up and optimizing the Web UI.
Help expand our documentation by submitting issues and PRs on our [Docs Repository](https://github.com/open-webui/docs).
We welcome tutorials, guides and other documentation improvements!
### 🌐 Translations and Internationalization
@@ -62,9 +74,12 @@ To add a new language:
- Copy the American English translation file(s) (from `en-US` directory in `src/lib/i18n/locale`) to this new directory and update the string values in JSON format according to your language. Make sure to preserve the structure of the JSON object.
- Add the language code and its respective title to languages file at `src/lib/i18n/locales/languages.json`.
> [!NOTE]
> When adding new translations, do so in a standalone PR! Feature PRs or PRs fixing a bug should not contain translation updates. Always keep the scope of a PR narrow.
### 🤔 Questions & Feedback
Got questions or feedback? Join our [Discord community](https://discord.gg/5rJgQTnV4s) or open an issue. We're here to help!
Got questions or feedback? Join our [Discord community](https://discord.gg/5rJgQTnV4s) or open an issue or discussion. We're here to help!
## 🙏 Thank You!
+100 -15
View File
@@ -4,10 +4,11 @@ Our primary goal is to ensure the protection and confidentiality of sensitive da
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| main | :white_check_mark: |
| others | :x: |
| Version (Branch) | Supported |
| ---------------- | ------------------ |
| main | :white_check_mark: |
| dev | :x: |
| others | :x: |
## Zero Tolerance for External Platforms
@@ -17,28 +18,112 @@ Any reports or solicitations arriving from sources other than our designated Git
## Reporting a Vulnerability
We appreciate the community's interest in identifying potential vulnerabilities. However, effective immediately, we will **not** accept low-effort vulnerability reports. To ensure that submissions are constructive and actionable, please adhere to the following guidelines:
Reports not submitted through our designated GitHub repository will be disregarded, and we will categorically reject invitations to collaborate on external platforms. Our aggressive stance on this matter underscores our commitment to a secure, transparent, and open community where all operations are visible and contributors are accountable.
1. **No Vague Reports**: Submissions such as "I found a vulnerability" without any details will be treated as spam and will not be accepted.
We appreciate the community's interest in identifying potential vulnerabilities. However, effective immediately, we will **not** accept low-effort vulnerability reports. Ensure that **submissions are constructive, actionable, reproducible, well documented and adhere to the following guidelines**:
2. **In-Depth Understanding Required**: Reports must reflect a clear understanding of the codebase and provide specific details about the vulnerability, including the affected components and potential impacts.
1. **Report MUST be a vulnerability:** A security vulnerability is an exploitable weakness where the system behaves in an unintended way, allowing attackers to bypass security controls, gain unauthorized access, execute arbitrary code, or escalate privileges. Configuration options, missing features, and expected protocol behavior are **not vulnerabilities**.
3. **Proof of Concept (PoC) is Mandatory**: Each submission must include a well-documented proof of concept (PoC) that demonstrates the vulnerability. If confidentiality is a concern, reporters are encouraged to create a private fork of the repository and share access with the maintainers. Reports lacking valid evidence will be disregarded.
2. **No Vague Reports**: Submissions such as "I found a vulnerability" without any details will be treated as spam and will not be accepted.
4. **Required Patch Submission**: Along with the PoC, reporters must provide a patch or actionable steps to remediate the identified vulnerability. This helps us evaluate and implement fixes rapidly.
3. **In-Depth Understanding Required**: Reports must reflect a clear understanding of the codebase and provide specific details about the vulnerability, including the affected components and potential impacts.
5. **Streamlined Merging Process**: When vulnerability reports meet the above criteria, we can consider them for immediate merging, similar to regular pull requests. Well-structured and thorough submissions will expedite the process of enhancing our security.
4. **Proof of Concept (PoC) is Mandatory**: Each submission must include a well-documented proof of concept (PoC) that demonstrates the vulnerability. If confidentiality is a concern, reporters are encouraged to create a private fork of the repository and share access with the maintainers. Reports lacking valid evidence may be disregarded.
**Non-compliant submissions will be closed, and repeat violators may be banned.** Our goal is to foster a constructive reporting environment where quality submissions promote better security for all users.
> [!NOTE]
> A PoC (Proof of Concept) is a **demonstration of exploitation of a vulnerability**. Your PoC must show:
>
> 1. What security boundary was crossed (Confidentiality, Integrity, Availability, Authenticity, Non-repudiation)
> 2. How this vulnerability was abused
> 3. What actions the attacker can now perform
>
> **Examples of valid PoCs:**
>
> - Step-by-step reproduction instructions with exact commands
> - Complete exploit code with detailed execution instructions
> - Screenshots/videos demonstrating the exploit (supplementary to written steps)
>
> **Failure to provide a reproducible PoC may lead to closure of the report**
>
> We will notify you, if we struggle to reproduce the exploit using your PoC to allow you to improve your PoC.
> However, if we repeatedly cannot reproduce the exploit using the PoC, the report may be closed.
## Product Security
5. **Required Patch or Actionable Remediation Plan Submission**: Along with the PoC, reporters must provide a patch or some actionable steps to remediate the identified vulnerability. This helps us evaluate and implement fixes rapidly.
6. **Streamlined Merging Process**: When vulnerability reports meet the above criteria, we can consider provided patches for immediate merging, similar to regular pull requests. Well-structured and thorough submissions will expedite the process of enhancing our security.
7. **Default Configuration Testing**: All vulnerability reports MUST be tested and reproducible using Open WebUI's out-of-the-box default configuration. Claims of vulnerabilities that only manifest with explicitly weakened security settings may be discarded, unless they are covered by the following exception:
> [!NOTE]
> **Note**: If you believe you have found a security issue that
>
> 1. affects default configurations, **or**
> 2. represents a genuine bypass of intended security controls, **or**
> 3. works only with non-default configurations, **but the configuration in question is likely to be used by production deployments**,
> **then we absolutely want to hear about it.** This policy is intended to filter configuration issues and deployment problems, not to discourage legitimate security research.
8. **Threat Model Understanding Required**: Reports must demonstrate understanding of Open WebUI's self-hosted, authenticated, role-based access control architecture. Comparing Open WebUI to services with fundamentally different security models without acknowledging the architectural differences may result in report rejection.
9. **CVSS Scoring Accuracy:** If you include a CVSS score with your report, it must accurately reflect the vulnerability according to CVSS methodology. Common errors include 1) rating PR:N (None) when authentication is required, 2) scoring hypothetical attack chains instead of the actual vulnerability, or 3) inflating severity without evidence. **We will adjust inaccurate CVSS scores.** Intentionally inflated scores may result in report rejection.
> [!WARNING] > **Using CVE Precedents:** If you cite other CVEs to support your report, ensure they are **genuinely comparable** in vulnerability type, threat model, and attack vector. Citing CVEs from different product categories, different vulnerability classes or different deployment models will lead us to suspect the use of AI in your report.
10. **Admin Actions Are Out of Scope:** Vulnerabilities that require an administrator to actively perform unsafe actions are **not considered valid vulnerabilities**. Admins have full system control and are expected to understand the security implications of their actions and configurations. This includes but is not limited to: adding malicious external servers (models, tools, webhooks), pasting untrusted code into Functions/Tools, or intentionally weakening security settings. **Reports requiring admin negligence or social engineering of admins may be rejected.**
> [!NOTE]
> Similar to rule "Default Configuration Testing": If you believe you have found a vulnerability that affects admins and is NOT caused by admin negligence or intentionally malicious actions,
> **then we absolutely want to hear about it.** This policy is intended to filter social engineering attacks on admins, malicious plugins being deployed by admins and similar malicious actions, not to discourage legitimate security research.
11. **AI report transparency:** Due to an extreme spike in AI-aided vulnerability reports **YOU MUST DISCLOSE if AI was used in any capacity** - whether for writing the report, generating the PoC, or identifying the vulnerability. If AI helped you in any way shape or form in the creation of the report, PoC or finding the vulnerability, you MUST disclose it.
> [!NOTE]
> AI-aided vulnerability reports **will not be rejected by us by default**. But:
>
> - If we suspect you used AI (but you did not disclose it to us), we will be asking tough follow-up questions to validate your understanding of the reported vulnerability and Open WebUI itself.
> - If we suspect you used AI (but you did not disclose it to us) **and** your report ends up being invalid/not a vulnerability/not reproducible, then you **may be banned** from reporting future vulnerabilities.
>
> This measure was necessary due to the extreme rise in clearly AI written vulnerability reports, where the vast majority of them
>
> - were not a vulnerability
> - were faulty configurations rather than a real vulnerability
> - did not provide a PoC
> - violated any of the rules outlined here
> - had a clear lack of understanding of Open WebUI
> - wrote comments with conflicting information
> - used illogical arguments
**Non-compliant submissions will be closed, and repeat extreme violators may be banned.** Our goal is to foster a constructive reporting environment where quality submissions promote better security for all users.
## Where to report the vulnerability
If you want to report a vulnerability and can meet the outlined requirements, [open a vulnerability report here](https://github.com/open-webui/open-webui/security/advisories/new).
If you feel like you are not able to follow ALL outlined requirements for vulnerability-specific reasons, still do report it, we will check every report either way.
## Product Security And For Non-Vulnerability Related Security Concerns:
If your concern does not meet the vulnerability requirements outlined above, is not a vulnerability, **but is still related to security concerns**, then use the following channels instead:
- **Documentation issues/improvement ideas:** Open an issue on our [Documentation Repository](https://github.com/open-webui/docs)
- **Feature requests:** Create a discussion in [GitHub Discussions - Ideas](https://github.com/open-webui/open-webui/discussions/) to discuss with the community if this feature request is wanted by multiple people
- **Configuration help:** Ask the community for help and guidance on our [Discord Server](https://discord.gg/5rJgQTnV4s) or on [Reddit](https://www.reddit.com/r/OpenWebUI/)
- **General issues:** Use our [Issue Tracker](https://github.com/open-webui/open-webui/issues)
**Examples of non-vulnerability, still security related concerns:**
- Suggestions for better default configuration values
- Security hardening recommendations
- Deployment best practices guidance
- Unclear configuration instructions
- Need for additional security documentation
- Feature requests for optional security enhancements (2FA, audit logging, etc.)
- General security questions about production deployment
Please use the adequate channel for your specific issue - e.g. best-practice guidance or additional documentation needs into the Documentation Repository, and feature requests into the Main Repository as an issue or discussion.
We regularly audit our internal processes and system architecture for vulnerabilities using a combination of automated and manual testing techniques. We are also planning to implement SAST and SCA scans in our project soon.
For immediate concerns or detailed reports that meet our guidelines, please create an issue in our [issue tracker](/open-webui/open-webui/issues) or contact us on [Discord](https://discord.gg/5rJgQTnV4s).
For any other immediate concerns, please create an issue in our [issue tracker](https://github.com/open-webui/open-webui/issues) or contact our team on [Discord](https://discord.gg/5rJgQTnV4s).
---
_Last updated on **2024-08-19**._
_Last updated on **2025-11-06**._
+955 -412
View File
File diff suppressed because it is too large Load Diff
+8 -6
View File
@@ -1,6 +1,6 @@
{
"name": "open-webui",
"version": "0.6.32",
"version": "0.6.35",
"private": true,
"scripts": {
"dev": "npm run pyodide:fetch && vite dev --host",
@@ -24,8 +24,8 @@
"devDependencies": {
"@sveltejs/adapter-auto": "3.2.2",
"@sveltejs/adapter-static": "^3.0.2",
"@sveltejs/kit": "^2.5.20",
"@sveltejs/vite-plugin-svelte": "^3.1.1",
"@sveltejs/kit": "^2.5.27",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/postcss": "^4.0.0",
"@tailwindcss/typography": "^0.5.13",
@@ -35,14 +35,14 @@
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-cypress": "^3.4.0",
"eslint-plugin-svelte": "^2.43.0",
"eslint-plugin-svelte": "^2.45.1",
"i18next-parser": "^9.0.1",
"postcss": "^8.4.31",
"prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.2.6",
"sass-embedded": "^1.81.0",
"svelte": "^4.2.18",
"svelte-check": "^3.8.5",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"svelte-confetti": "^1.3.2",
"tailwindcss": "^4.0.0",
"tslib": "^2.4.1",
@@ -137,6 +137,8 @@
"turndown-plugin-gfm": "^1.0.2",
"undici": "^7.3.0",
"uuid": "^9.0.1",
"vega": "^6.2.0",
"vega-lite": "^6.4.1",
"vite-plugin-static-copy": "^2.2.0",
"y-prosemirror": "^1.3.7",
"yaml": "^2.7.1",
+12 -20
View File
@@ -6,20 +6,19 @@ authors = [
]
license = { file = "LICENSE" }
dependencies = [
"fastapi==0.115.7",
"uvicorn[standard]==0.35.0",
"pydantic==2.11.7",
"fastapi==0.118.0",
"uvicorn[standard]==0.37.0",
"pydantic==2.11.9",
"python-multipart==0.0.20",
"itsdangerous==2.2.0",
"python-socketio==5.13.0",
"python-jose==3.4.0",
"passlib[bcrypt]==1.7.4",
"python-jose==3.5.0",
"cryptography",
"bcrypt==4.3.0",
"bcrypt==5.0.0",
"argon2-cffi==25.1.0",
"PyJWT[crypto]==2.10.1",
"authlib==1.6.3",
"authlib==1.6.5",
"requests==2.32.5",
"aiohttp==3.12.15",
@@ -74,9 +73,9 @@ dependencies = [
"pymdown-extensions==10.14.2",
"docx2txt==0.8",
"python-pptx==1.0.2",
"unstructured==0.16.17",
"unstructured==0.18.15",
"nltk==3.9.1",
"Markdown==3.8.2",
"Markdown==3.9",
"pypandoc==1.15",
"pandas==2.2.3",
"openpyxl==3.1.5",
@@ -96,8 +95,8 @@ dependencies = [
"onnxruntime==1.20.1",
"faster-whisper==1.1.1",
"black==25.1.0",
"youtube-transcript-api==1.1.0",
"black==25.9.0",
"youtube-transcript-api==1.2.2",
"pytube==15.0.0",
"pydub",
@@ -107,8 +106,6 @@ dependencies = [
"google-auth-httplib2",
"google-auth-oauthlib",
"googleapis-common-protos==1.70.0",
"google-cloud-storage==2.19.0",
@@ -116,12 +113,6 @@ dependencies = [
"azure-storage-blob==12.24.1",
"ldap3==2.9.1",
"firecrawl-py==1.12.0",
"tencentcloud-sdk-python==3.0.1336",
"oracledb>=3.2.0",
]
readme = "README.md"
requires-python = ">= 3.11, < 3.13.0a1"
@@ -155,11 +146,12 @@ all = [
"elasticsearch==9.1.0",
"qdrant-client==1.14.3",
"pymilvus==2.5.0",
"pymilvus==2.6.2",
"pinecone==6.0.2",
"oracledb==3.2.0",
"colbert-ai==0.2.21",
"firecrawl-py==4.5.0",
]
[project.scripts]
+10 -2
View File
@@ -129,8 +129,8 @@ li p {
}
::-webkit-scrollbar {
height: 0.4rem;
width: 0.4rem;
height: 0.45rem;
width: 0.45rem;
}
::-webkit-scrollbar-track {
@@ -152,6 +152,14 @@ select {
-webkit-appearance: none;
}
.dark select:not([class*='bg-transparent']) {
@apply bg-gray-900 text-gray-300;
}
.dark select option {
@apply bg-gray-850 text-white;
}
@keyframes shimmer {
0% {
background-position: 200% 0;
+1 -6
View File
@@ -23,12 +23,7 @@
href="/static/apple-touch-icon.png"
crossorigin="use-credentials"
/>
<link
rel="manifest"
href="/manifest.json"
crossorigin="use-credentials"
crossorigin="use-credentials"
/>
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover"
+5
View File
@@ -112,6 +112,7 @@ export const importChat = async (
export const getChatList = async (
token: string = '',
page: number | null = null,
include_pinned: boolean = false,
include_folders: boolean = false
) => {
let error = null;
@@ -125,6 +126,10 @@ export const getChatList = async (
searchParams.append('include_folders', 'true');
}
if (include_pinned) {
searchParams.append('include_pinned', 'true');
}
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/?${searchParams.toString()}`, {
method: 'GET',
headers: {
+4
View File
@@ -63,6 +63,10 @@ export const uploadFile = async (token: string, file: File, metadata?: object |
console.error(data.error);
res.error = data.error;
}
if (res?.data) {
res.data = data;
}
}
}
}
+31
View File
@@ -62,6 +62,37 @@ export const getFunctions = async (token: string = '') => {
return res;
};
export const getFunctionList = async (token: string = '') => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/functions/list`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err.detail;
console.error(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const loadFunctionByUrl = async (token: string = '', url: string) => {
let error = null;
+31 -1
View File
@@ -494,7 +494,10 @@ export const executeToolServer = async (
headers
};
if (['post', 'put', 'patch'].includes(httpMethod.toLowerCase()) && operation.requestBody) {
if (
['post', 'put', 'patch', 'delete'].includes(httpMethod.toLowerCase()) &&
operation.requestBody
) {
requestOptions.body = JSON.stringify(bodyParams);
}
@@ -1398,6 +1401,33 @@ export const getChangelog = async () => {
return res;
};
export const getVersion = async (token: string) => {
let error = null;
const res = await fetch(`${WEBUI_BASE_URL}/api/version`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.error(err);
error = err;
return null;
});
if (error) {
throw error;
}
return res?.version ?? null;
};
export const getVersionUpdates = async (token: string) => {
let error = null;
+2 -2
View File
@@ -1,9 +1,9 @@
import { WEBUI_API_BASE_URL } from '$lib/constants';
export const getModels = async (token: string = '') => {
export const getModelItems = async (token: string = '') => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/models/`, {
const res = await fetch(`${WEBUI_API_BASE_URL}/models/list`, {
method: 'GET',
headers: {
Accept: 'application/json',
+67 -1
View File
@@ -17,6 +17,7 @@
import Tags from './common/Tags.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
import XMark from '$lib/components/icons/XMark.svelte';
import Textarea from './common/Textarea.svelte';
export let onSubmit: Function = () => {};
export let onDelete: Function = () => {};
@@ -42,6 +43,8 @@
let enable = true;
let apiVersion = '';
let headers = '';
let tags = [];
let modelId = '';
@@ -69,6 +72,22 @@
// remove trailing slash from url
url = url.replace(/\/$/, '');
let _headers = null;
if (headers) {
try {
_headers = JSON.parse(headers);
if (typeof _headers !== 'object' || Array.isArray(_headers)) {
_headers = null;
throw new Error('Headers must be a valid JSON object');
}
headers = JSON.stringify(_headers, null, 2);
} catch (error) {
toast.error($i18n.t('Headers must be a valid JSON object'));
return;
}
}
const res = await verifyOpenAIConnection(
localStorage.token,
{
@@ -77,7 +96,8 @@
config: {
auth_type,
azure: azure,
api_version: apiVersion
api_version: apiVersion,
...(_headers ? { headers: _headers } : {})
}
},
direct
@@ -136,6 +156,19 @@
}
}
if (headers) {
try {
const _headers = JSON.parse(headers);
if (typeof _headers !== 'object' || Array.isArray(_headers)) {
throw new Error('Headers must be a valid JSON object');
}
headers = JSON.stringify(_headers, null, 2);
} catch (error) {
toast.error($i18n.t('Headers must be a valid JSON object'));
return;
}
}
// remove trailing slash from url
url = url.replace(/\/$/, '');
@@ -149,6 +182,7 @@
model_ids: modelIds,
connection_type: connectionType,
auth_type,
headers: headers ? JSON.parse(headers) : undefined,
...(!ollama && azure ? { azure: true, api_version: apiVersion } : {})
}
};
@@ -172,6 +206,9 @@
key = connection.key;
auth_type = connection.config.auth_type ?? 'bearer';
headers = connection.config?.headers
? JSON.stringify(connection.config.headers, null, 2)
: '';
enable = connection.config?.enable ?? true;
tags = connection.config?.tags ?? [];
@@ -376,6 +413,35 @@
</div>
</div>
{#if !ollama && !direct}
<div class="flex gap-2 mt-2">
<div class="flex flex-col w-full">
<label
for="headers-input"
class={`mb-0.5 text-xs text-gray-500
${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : ''}`}
>{$i18n.t('Headers')}</label
>
<div class="flex-1">
<Tooltip
content={$i18n.t(
'Enter additional headers in JSON format (e.g. {{\'{{"X-Custom-Header": "value"}}\'}})'
)}
>
<Textarea
className="w-full text-sm outline-hidden"
bind:value={headers}
placeholder={$i18n.t('Enter additional headers in JSON format')}
required={false}
minSize={30}
/>
</Tooltip>
</div>
</div>
</div>
{/if}
<div class="flex gap-2 mt-2">
<div class="flex flex-col w-full">
<label
+10 -3
View File
@@ -98,9 +98,16 @@
return;
}
if (path === '') {
toast.error($i18n.t('Please enter a valid path'));
return;
if (['openapi', ''].includes(type)) {
if (spec_type === 'json' && spec === '') {
toast.error($i18n.t('Please enter a valid JSON spec'));
return;
}
if (spec_type === 'url' && path === '') {
toast.error($i18n.t('Please enter a valid path'));
return;
}
}
if (direct) {
+468 -431
View File
@@ -3,7 +3,7 @@
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
import { WEBUI_NAME, config, functions, models, settings } from '$lib/stores';
import { WEBUI_NAME, config, functions as _functions, models, settings, user } from '$lib/stores';
import { onMount, getContext, tick } from 'svelte';
import { goto } from '$app/navigation';
@@ -12,6 +12,7 @@
deleteFunctionById,
exportFunctions,
getFunctionById,
getFunctionList,
getFunctions,
loadFunctionByUrl,
toggleFunctionById,
@@ -36,6 +37,10 @@
import XMark from '../icons/XMark.svelte';
import AddFunctionMenu from './Functions/AddFunctionMenu.svelte';
import ImportModal from '../ImportModal.svelte';
import ViewSelector from '../workspace/common/ViewSelector.svelte';
import TagSelector from '../workspace/common/TagSelector.svelte';
import { capitalizeFirstLetter } from '$lib/utils';
import Spinner from '../common/Spinner.svelte';
const i18n = getContext('i18n');
@@ -44,12 +49,16 @@
let functionsImportInputElement: HTMLInputElement;
let importFiles;
let tagsContainerElement: HTMLDivElement;
let viewOption = '';
let query = '';
let selectedTag = '';
let selectedType = '';
let showImportModal = false;
let showConfirm = false;
let query = '';
let selectedType = 'all';
let showManifestModal = false;
let showValvesModal = false;
@@ -57,17 +66,33 @@
let showDeleteConfirm = false;
let loaded = false;
let functions = null;
let filteredItems = [];
$: filteredItems = $functions
.filter(
(f) =>
(selectedType !== 'all' ? f.type === selectedType : true) &&
(query === '' ||
f.name.toLowerCase().includes(query.toLowerCase()) ||
f.id.toLowerCase().includes(query.toLowerCase()))
)
.sort((a, b) => a.type.localeCompare(b.type) || a.name.localeCompare(b.name));
$: if (
functions &&
query !== undefined &&
selectedType !== undefined &&
viewOption !== undefined
) {
setFilteredItems();
}
const setFilteredItems = () => {
filteredItems = functions
.filter(
(f) =>
(selectedType !== '' ? f.type === selectedType : true) &&
(query === '' ||
f.name.toLowerCase().includes(query.toLowerCase()) ||
f.id.toLowerCase().includes(query.toLowerCase())) &&
(viewOption === '' ||
(viewOption === 'created' && f.user_id === $user?.id) ||
(viewOption === 'shared' && f.user_id !== $user?.id))
)
.sort((a, b) => a.type.localeCompare(b.type) || a.name.localeCompare(b.name));
};
const shareHandler = async (func) => {
const item = await getFunctionById(localStorage.token, func.id).catch((error) => {
toast.error(`${error}`);
@@ -133,8 +158,9 @@
if (res) {
toast.success($i18n.t('Function deleted successfully'));
functions = functions.filter((f) => f.id !== func.id);
functions.set(await getFunctions(localStorage.token));
_functions.set(await getFunctions(localStorage.token));
models.set(
await getModels(
localStorage.token,
@@ -162,7 +188,7 @@
: toast.success($i18n.t('Function is now globally disabled'));
}
functions.set(await getFunctions(localStorage.token));
_functions.set(await getFunctions(localStorage.token));
models.set(
await getModels(
localStorage.token,
@@ -174,7 +200,16 @@
}
};
onMount(() => {
onMount(async () => {
viewOption = localStorage?.workspaceViewOption || '';
functions = await getFunctionList(localStorage.token).catch((error) => {
toast.error(`${error}`);
return [];
});
await tick();
loaded = true;
const onKeyDown = (event) => {
if (event.key === 'Shift') {
shiftKey = true;
@@ -222,424 +257,388 @@
}}
/>
<div class="flex flex-col mt-1.5 mb-0.5 px-[16px]">
<div class="flex justify-between items-center mb-1">
<div class="flex md:self-center text-xl items-center font-medium px-0.5">
{$i18n.t('Functions')}
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
<span class="text-base font-lg text-gray-500 dark:text-gray-300">{filteredItems.length}</span>
</div>
</div>
{#if loaded}
<div class="px-4.5 w-full">
<div class="flex flex-col gap-1 px-1 mt-2.5 mb-2">
<div class="flex justify-between items-center mb-1 w-full">
<input
id="documents-import-input"
bind:this={functionsImportInputElement}
bind:files={importFiles}
type="file"
accept=".json"
hidden
on:change={() => {
console.log(importFiles);
showConfirm = true;
}}
/>
<div class=" flex w-full space-x-2">
<div class="flex flex-1">
<div class=" self-center ml-1 mr-3">
<Search className="size-3.5" />
<div class="flex justify-between items-center w-full">
<div class="flex items-center md:self-center text-xl font-medium px-0.5 gap-2 shrink-0">
<div>
{$i18n.t('Functions')}
</div>
<div class="text-lg font-medium text-gray-500 dark:text-gray-500">
{filteredItems.length}
</div>
</div>
<div class="flex w-full justify-end gap-1.5">
{#if $user?.role === 'admin'}
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-200 transition"
on:click={() => {
functionsImportInputElement.click();
}}
>
<div class=" self-center font-medium line-clamp-1">
{$i18n.t('Import')}
</div>
</button>
{#if functions.length}
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-200 transition"
on:click={async () => {
const _functions = await exportFunctions(localStorage.token).catch((error) => {
toast.error(`${error}`);
return null;
});
if (_functions) {
let blob = new Blob([JSON.stringify(_functions)], {
type: 'application/json'
});
saveAs(blob, `functions-export-${Date.now()}.json`);
}
}}
>
<div class=" self-center font-medium line-clamp-1">
{$i18n.t('Export')}
</div>
</button>
{/if}
{/if}
<AddFunctionMenu
createHandler={() => {
goto('/admin/functions/create');
}}
importFromLinkHandler={() => {
showImportModal = true;
}}
>
<div
class=" px-2 py-1.5 rounded-xl bg-black text-white dark:bg-white dark:text-black transition font-medium text-sm flex items-center"
>
<Plus className="size-3" strokeWidth="2.5" />
<div class=" hidden md:block md:ml-1 text-xs">{$i18n.t('New Function')}</div>
</div>
</AddFunctionMenu>
</div>
</div>
</div>
<input
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
bind:value={query}
placeholder={$i18n.t('Search Functions')}
/>
</div>
{#if query}
<div class="self-center pl-1.5 translate-y-[0.5px] rounded-l-xl bg-transparent">
<button
class="p-0.5 rounded-full hover:bg-gray-100 dark:hover:bg-gray-900 transition"
on:click={() => {
query = '';
<div
class="py-2 bg-white dark:bg-gray-900 rounded-3xl border border-gray-100 dark:border-gray-850"
>
<div class="px-3.5 flex flex-1 items-center w-full space-x-2 py-0.5 pb-2">
<div class="flex flex-1">
<div class=" self-center ml-1 mr-3">
<Search className="size-3.5" />
</div>
<input
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
bind:value={query}
placeholder={$i18n.t('Search Functions')}
/>
{#if query}
<div class="self-center pl-1.5 translate-y-[0.5px] rounded-l-xl bg-transparent">
<button
class="p-0.5 rounded-full hover:bg-gray-100 dark:hover:bg-gray-900 transition"
on:click={() => {
query = '';
}}
>
<XMark className="size-3" strokeWidth="2" />
</button>
</div>
{/if}
</div>
</div>
<div
class="px-3 flex w-full bg-transparent overflow-x-auto scrollbar-none"
on:wheel={(e) => {
if (e.deltaY !== 0) {
e.preventDefault();
e.currentTarget.scrollLeft += e.deltaY;
}
}}
>
<div
class="flex gap-0.5 w-fit text-center text-sm rounded-full bg-transparent px-0.5 whitespace-nowrap"
bind:this={tagsContainerElement}
>
<ViewSelector
bind:value={viewOption}
onChange={async (value) => {
localStorage.workspaceViewOption = value;
await tick();
}}
>
<XMark className="size-3" strokeWidth="2" />
</button>
/>
<TagSelector
bind:value={selectedType}
items={[
{ value: 'pipe', label: $i18n.t('Pipe') },
{ value: 'filter', label: $i18n.t('Filter') },
{ value: 'action', label: $i18n.t('Action') }
]}
/>
</div>
</div>
{#if (filteredItems ?? []).length !== 0}
<div class="px-3 my-2 gap-1 lg:gap-2 grid lg:grid-cols-2">
{#each filteredItems as func (func.id)}
<div
class=" flex space-x-4 cursor-pointer w-full px-2 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
>
<a
class=" flex flex-1 space-x-3.5 cursor-pointer w-full"
href={`/admin/functions/edit?id=${encodeURIComponent(func.id)}`}
>
<div class="flex items-center text-left">
<div class=" flex-1 self-center pl-1">
<Tooltip content={func.id} placement="top-start">
<div class=" flex items-center gap-1.5">
<div
class=" text-xs font-semibold px-1 rounded-sm uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
>
{func.type}
</div>
<div class="line-clamp-1 text-sm">
{func.name}
</div>
{#if func?.meta?.manifest?.version}
<div class=" text-gray-500 text-xs font-medium shrink-0">
v{func?.meta?.manifest?.version ?? ''}
</div>
{/if}
</div>
</Tooltip>
<div class="flex gap-1.5 px-1">
<div class="text-xs text-gray-500 shrink-0">
<Tooltip
content={func?.user?.email ?? $i18n.t('Deleted User')}
className="flex shrink-0"
placement="top-start"
>
{$i18n.t('By {{name}}', {
name: capitalizeFirstLetter(
func?.user?.name ?? func?.user?.email ?? $i18n.t('Deleted User')
)
})}
</Tooltip>
</div>
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
{func.meta.description}
</div>
</div>
</div>
</div>
</a>
<div class="flex flex-row gap-0.5 self-center">
{#if shiftKey}
<Tooltip content={$i18n.t('Delete')}>
<button
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button"
on:click={() => {
deleteHandler(func);
}}
>
<GarbageBin />
</button>
</Tooltip>
{:else}
{#if func?.meta?.manifest?.funding_url ?? false}
<Tooltip content={$i18n.t('Support')}>
<button
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button"
on:click={() => {
selectedFunction = func;
showManifestModal = true;
}}
>
<Heart />
</button>
</Tooltip>
{/if}
<Tooltip content={$i18n.t('Valves')}>
<button
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button"
on:click={() => {
selectedFunction = func;
showValvesModal = true;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
/>
</svg>
</button>
</Tooltip>
<FunctionMenu
{func}
editHandler={() => {
goto(`/admin/functions/edit?id=${encodeURIComponent(func.id)}`);
}}
shareHandler={() => {
shareHandler(func);
}}
cloneHandler={() => {
cloneHandler(func);
}}
exportHandler={() => {
exportHandler(func);
}}
deleteHandler={async () => {
selectedFunction = func;
showDeleteConfirm = true;
}}
toggleGlobalHandler={() => {
if (['filter', 'action'].includes(func.type)) {
toggleGlobalHandler(func);
}
}}
onClose={() => {}}
>
<button
class="self-center w-fit text-sm p-1.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button"
>
<EllipsisHorizontal className="size-5" />
</button>
</FunctionMenu>
{/if}
<div class=" self-center mx-1">
<Tooltip content={func.is_active ? $i18n.t('Enabled') : $i18n.t('Disabled')}>
<Switch
bind:state={func.is_active}
on:change={async (e) => {
toggleFunctionById(localStorage.token, func.id);
models.set(
await getModels(
localStorage.token,
$config?.features?.enable_direct_connections &&
($settings?.directConnections ?? null),
false,
true
)
);
}}
/>
</Tooltip>
</div>
</div>
</div>
{/each}
</div>
{:else}
<div class=" w-full h-full flex flex-col justify-center items-center my-16 mb-24">
<div class="max-w-md text-center">
<div class=" text-3xl mb-3">😕</div>
<div class=" text-lg font-medium mb-1">{$i18n.t('No functions found')}</div>
<div class=" text-gray-500 text-center text-xs">
{$i18n.t('Try adjusting your search or filter to find what you are looking for.')}
</div>
</div>
</div>
{/if}
</div>
<div>
<AddFunctionMenu
createHandler={() => {
goto('/admin/functions/create');
}}
importFromLinkHandler={() => {
showImportModal = true;
}}
>
<div
class=" px-2 py-2 rounded-xl hover:bg-gray-700/10 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition font-medium text-sm flex items-center space-x-1"
>
<Plus className="size-3.5" />
</div>
</AddFunctionMenu>
</div>
</div>
<div class=" flex w-full">
<div
class="flex gap-1 scrollbar-none overflow-x-auto w-fit text-center text-sm font-medium rounded-full bg-transparent"
>
<button
class="min-w-fit p-1.5 {selectedType === 'all'
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
on:click={() => {
selectedType = 'all';
}}>{$i18n.t('All')}</button
>
<button
class="min-w-fit p-1.5 {selectedType === 'pipe'
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
on:click={() => {
selectedType = 'pipe';
}}>{$i18n.t('Pipe')}</button
>
<button
class="min-w-fit p-1.5 {selectedType === 'filter'
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
on:click={() => {
selectedType = 'filter';
}}>{$i18n.t('Filter')}</button
>
<button
class="min-w-fit p-1.5 {selectedType === 'action'
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
on:click={() => {
selectedType = 'action';
}}>{$i18n.t('Action')}</button
>
</div>
</div>
</div>
<div class="mb-5 px-[16px]">
{#each filteredItems as func (func.id)}
<div
class=" flex space-x-4 cursor-pointer w-full px-2 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
>
<a
class=" flex flex-1 space-x-3.5 cursor-pointer w-full"
href={`/admin/functions/edit?id=${encodeURIComponent(func.id)}`}
>
<div class="flex items-center text-left">
<div class=" flex-1 self-center pl-1">
<div class=" font-semibold flex items-center gap-1.5">
<div
class=" text-xs font-semibold px-1 rounded-sm uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
>
{func.type}
</div>
{#if func?.meta?.manifest?.version}
<div
class="text-xs font-semibold px-1 rounded-sm line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
>
v{func?.meta?.manifest?.version ?? ''}
</div>
{/if}
<div class=" line-clamp-1">
{func.name}
</div>
</div>
<div class="flex gap-1.5 px-1">
<div class=" text-gray-500 text-xs font-medium shrink-0">{func.id}</div>
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
{func.meta.description}
</div>
</div>
</div>
</div>
</a>
<div class="flex flex-row gap-0.5 self-center">
{#if shiftKey}
<Tooltip content={$i18n.t('Delete')}>
<button
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button"
on:click={() => {
deleteHandler(func);
}}
>
<GarbageBin />
</button>
</Tooltip>
{:else}
{#if func?.meta?.manifest?.funding_url ?? false}
<Tooltip content={$i18n.t('Support')}>
<button
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button"
on:click={() => {
selectedFunction = func;
showManifestModal = true;
}}
>
<Heart />
</button>
</Tooltip>
{/if}
<Tooltip content={$i18n.t('Valves')}>
<button
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button"
on:click={() => {
selectedFunction = func;
showValvesModal = true;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
/>
</svg>
</button>
</Tooltip>
<FunctionMenu
{func}
editHandler={() => {
goto(`/admin/functions/edit?id=${encodeURIComponent(func.id)}`);
}}
shareHandler={() => {
shareHandler(func);
}}
cloneHandler={() => {
cloneHandler(func);
}}
exportHandler={() => {
exportHandler(func);
}}
deleteHandler={async () => {
selectedFunction = func;
showDeleteConfirm = true;
}}
toggleGlobalHandler={() => {
if (['filter', 'action'].includes(func.type)) {
toggleGlobalHandler(func);
}
}}
onClose={() => {}}
>
<button
class="self-center w-fit text-sm p-1.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button"
>
<EllipsisHorizontal className="size-5" />
</button>
</FunctionMenu>
{/if}
<div class=" self-center mx-1">
<Tooltip content={func.is_active ? $i18n.t('Enabled') : $i18n.t('Disabled')}>
<Switch
bind:state={func.is_active}
on:change={async (e) => {
toggleFunctionById(localStorage.token, func.id);
models.set(
await getModels(
localStorage.token,
$config?.features?.enable_direct_connections &&
($settings?.directConnections ?? null),
false,
true
)
);
}}
/>
</Tooltip>
</div>
</div>
</div>
{/each}
</div>
<!-- <div class=" text-gray-500 text-xs mt-1 mb-2">
<!-- <div class=" text-gray-500 text-xs mt-1 mb-2">
ⓘ {$i18n.t(
'Admins have access to all tools at all times; users need tools assigned per model in the workspace.'
)}
</div> -->
<div class=" flex justify-end w-full mb-2 px-[16px]">
<div class="flex space-x-2">
<input
id="documents-import-input"
bind:this={functionsImportInputElement}
bind:files={importFiles}
type="file"
accept=".json"
hidden
on:change={() => {
console.log(importFiles);
showConfirm = true;
}}
/>
{#if $config?.features.enable_community_sharing}
<div class=" my-16">
<div class=" text-xl font-medium mb-1 line-clamp-1">
{$i18n.t('Made by Open WebUI Community')}
</div>
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
on:click={() => {
functionsImportInputElement.click();
}}
>
<div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Import Functions')}</div>
<div class=" self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
<a
class=" flex cursor-pointer items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-850 w-full mb-2 px-3.5 py-1.5 rounded-xl transition"
href="https://openwebui.com/functions"
target="_blank"
>
<path
fill-rule="evenodd"
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
clip-rule="evenodd"
/>
</svg>
<div class=" self-center">
<div class=" font-semibold line-clamp-1">{$i18n.t('Discover a function')}</div>
<div class=" text-sm line-clamp-1">
{$i18n.t('Discover, download, and explore custom functions')}
</div>
</div>
<div>
<div>
<ChevronRight />
</div>
</div>
</a>
</div>
</button>
{#if $functions.length}
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
on:click={async () => {
const _functions = await exportFunctions(localStorage.token).catch((error) => {
toast.error(`${error}`);
return null;
});
if (_functions) {
let blob = new Blob([JSON.stringify(_functions)], {
type: 'application/json'
});
saveAs(blob, `functions-export-${Date.now()}.json`);
}
}}
>
<div class=" self-center mr-2 font-medium line-clamp-1">
{$i18n.t('Export Functions')} ({$functions.length})
</div>
<div class=" self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
clip-rule="evenodd"
/>
</svg>
</div>
</button>
{/if}
</div>
</div>
{#if $config?.features.enable_community_sharing}
<div class=" my-16 px-[16px]">
<div class=" text-xl font-medium mb-1 line-clamp-1">
{$i18n.t('Made by Open WebUI Community')}
<DeleteConfirmDialog
bind:show={showDeleteConfirm}
title={$i18n.t('Delete function?')}
on:confirm={() => {
deleteHandler(selectedFunction);
}}
>
<div class=" text-sm text-gray-500 truncate">
{$i18n.t('This will delete')} <span class=" font-semibold">{selectedFunction.name}</span>.
</div>
</DeleteConfirmDialog>
<a
class=" flex cursor-pointer items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-850 w-full mb-2 px-3.5 py-1.5 rounded-xl transition"
href="https://openwebui.com/functions"
target="_blank"
>
<div class=" self-center">
<div class=" font-semibold line-clamp-1">{$i18n.t('Discover a function')}</div>
<div class=" text-sm line-clamp-1">
{$i18n.t('Discover, download, and explore custom functions')}
</div>
</div>
<div>
<div>
<ChevronRight />
</div>
</div>
</a>
</div>
{/if}
<DeleteConfirmDialog
bind:show={showDeleteConfirm}
title={$i18n.t('Delete function?')}
on:confirm={() => {
deleteHandler(selectedFunction);
}}
>
<div class=" text-sm text-gray-500 truncate">
{$i18n.t('This will delete')} <span class=" font-semibold">{selectedFunction.name}</span>.
</div>
</DeleteConfirmDialog>
<ManifestModal bind:show={showManifestModal} manifest={selectedFunction?.meta?.manifest ?? {}} />
<ValvesModal
bind:show={showValvesModal}
type="function"
id={selectedFunction?.id ?? null}
on:save={async () => {
await tick();
models.set(
await getModels(
localStorage.token,
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null),
false,
true
)
);
}}
/>
<ConfirmDialog
bind:show={showConfirm}
on:confirm={() => {
const reader = new FileReader();
reader.onload = async (event) => {
const _functions = JSON.parse(event.target.result);
console.log(_functions);
for (let func of _functions) {
if ('function' in func) {
// Required for Community JSON import
func = func.function;
}
const res = await createNewFunction(localStorage.token, func).catch((error) => {
toast.error(`${error}`);
return null;
});
}
toast.success($i18n.t('Functions imported successfully'));
functions.set(await getFunctions(localStorage.token));
<ManifestModal bind:show={showManifestModal} manifest={selectedFunction?.meta?.manifest ?? {}} />
<ValvesModal
bind:show={showValvesModal}
type="function"
id={selectedFunction?.id ?? null}
on:save={async () => {
await tick();
models.set(
await getModels(
localStorage.token,
@@ -648,25 +647,63 @@
true
)
);
};
}}
/>
reader.readAsText(importFiles[0]);
}}
>
<div class="text-sm text-gray-500">
<div class=" bg-yellow-500/20 text-yellow-700 dark:text-yellow-200 rounded-lg px-4 py-3">
<div>{$i18n.t('Please carefully review the following warnings:')}</div>
<ConfirmDialog
bind:show={showConfirm}
on:confirm={() => {
const reader = new FileReader();
reader.onload = async (event) => {
const _functions = JSON.parse(event.target.result);
console.log(_functions);
<ul class=" mt-1 list-disc pl-4 text-xs">
<li>{$i18n.t('Functions allow arbitrary code execution.')}</li>
<li>{$i18n.t('Do not install functions from sources you do not fully trust.')}</li>
</ul>
</div>
<div class="my-3">
{$i18n.t(
'I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.'
)}
for (let func of _functions) {
if ('function' in func) {
// Required for Community JSON import
func = func.function;
}
const res = await createNewFunction(localStorage.token, func).catch((error) => {
toast.error(`${error}`);
return null;
});
}
toast.success($i18n.t('Functions imported successfully'));
functions.set(await getFunctions(localStorage.token));
models.set(
await getModels(
localStorage.token,
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null),
false,
true
)
);
};
reader.readAsText(importFiles[0]);
}}
>
<div class="text-sm text-gray-500">
<div class=" bg-yellow-500/20 text-yellow-700 dark:text-yellow-200 rounded-lg px-4 py-3">
<div>{$i18n.t('Please carefully review the following warnings:')}</div>
<ul class=" mt-1 list-disc pl-4 text-xs">
<li>{$i18n.t('Functions allow arbitrary code execution.')}</li>
<li>{$i18n.t('Do not install functions from sources you do not fully trust.')}</li>
</ul>
</div>
<div class="my-3">
{$i18n.t(
'I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.'
)}
</div>
</div>
</ConfirmDialog>
{:else}
<div class="w-full h-full flex justify-center items-center">
<Spinner className="size-5" />
</div>
</ConfirmDialog>
{/if}
@@ -41,27 +41,27 @@
<div slot="content">
<DropdownMenu.Content
class="w-full max-w-[190px] text-sm rounded-xl p-1 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg font-primary"
sideOffset={-2}
class="w-full max-w-[190px] rounded-2xl px-1 py-1 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
sideOffset={6}
side="bottom"
align="start"
transition={flyAndScale}
>
<button
class="flex rounded-md py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition"
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl w-full"
on:click={async () => {
createHandler();
show = false;
}}
>
<div class=" self-center mr-2">
<PencilSolid />
<Pencil />
</div>
<div class=" self-center truncate">{$i18n.t('New Function')}</div>
</button>
<button
class="flex rounded-md py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition"
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl w-full"
on:click={async () => {
importFromLinkHandler();
show = false;
+105 -5
View File
@@ -19,6 +19,7 @@
import type { Writable } from 'svelte/store';
import type { i18n as i18nType } from 'i18next';
import Textarea from '$lib/components/common/Textarea.svelte';
const i18n = getContext<Writable<i18nType>>('i18n');
@@ -31,6 +32,7 @@
let TTS_ENGINE = '';
let TTS_MODEL = '';
let TTS_VOICE = '';
let TTS_OPENAI_PARAMS = '';
let TTS_SPLIT_ON: TTS_RESPONSE_SPLIT = TTS_RESPONSE_SPLIT.PUNCTUATION;
let TTS_AZURE_SPEECH_REGION = '';
let TTS_AZURE_SPEECH_BASE_URL = '';
@@ -48,6 +50,9 @@
let STT_AZURE_BASE_URL = '';
let STT_AZURE_MAX_SPEAKERS = '';
let STT_DEEPGRAM_API_KEY = '';
let STT_MISTRAL_API_KEY = '';
let STT_MISTRAL_API_BASE_URL = '';
let STT_MISTRAL_USE_CHAT_COMPLETIONS = false;
let STT_WHISPER_MODEL_LOADING = false;
@@ -98,18 +103,28 @@
};
const updateConfigHandler = async () => {
let openaiParams = {};
try {
openaiParams = TTS_OPENAI_PARAMS ? JSON.parse(TTS_OPENAI_PARAMS) : {};
TTS_OPENAI_PARAMS = JSON.stringify(openaiParams, null, 2);
} catch (e) {
toast.error($i18n.t('Invalid JSON format for Parameters'));
return;
}
const res = await updateAudioConfig(localStorage.token, {
tts: {
OPENAI_API_BASE_URL: TTS_OPENAI_API_BASE_URL,
OPENAI_API_KEY: TTS_OPENAI_API_KEY,
OPENAI_PARAMS: openaiParams,
API_KEY: TTS_API_KEY,
ENGINE: TTS_ENGINE,
MODEL: TTS_MODEL,
VOICE: TTS_VOICE,
SPLIT_ON: TTS_SPLIT_ON,
AZURE_SPEECH_REGION: TTS_AZURE_SPEECH_REGION,
AZURE_SPEECH_BASE_URL: TTS_AZURE_SPEECH_BASE_URL,
AZURE_SPEECH_OUTPUT_FORMAT: TTS_AZURE_SPEECH_OUTPUT_FORMAT
AZURE_SPEECH_OUTPUT_FORMAT: TTS_AZURE_SPEECH_OUTPUT_FORMAT,
SPLIT_ON: TTS_SPLIT_ON
},
stt: {
OPENAI_API_BASE_URL: STT_OPENAI_API_BASE_URL,
@@ -123,7 +138,10 @@
AZURE_REGION: STT_AZURE_REGION,
AZURE_LOCALES: STT_AZURE_LOCALES,
AZURE_BASE_URL: STT_AZURE_BASE_URL,
AZURE_MAX_SPEAKERS: STT_AZURE_MAX_SPEAKERS
AZURE_MAX_SPEAKERS: STT_AZURE_MAX_SPEAKERS,
MISTRAL_API_KEY: STT_MISTRAL_API_KEY,
MISTRAL_API_BASE_URL: STT_MISTRAL_API_BASE_URL,
MISTRAL_USE_CHAT_COMPLETIONS: STT_MISTRAL_USE_CHAT_COMPLETIONS
}
});
@@ -146,6 +164,7 @@
console.log(res);
TTS_OPENAI_API_BASE_URL = res.tts.OPENAI_API_BASE_URL;
TTS_OPENAI_API_KEY = res.tts.OPENAI_API_KEY;
TTS_OPENAI_PARAMS = JSON.stringify(res?.tts?.OPENAI_PARAMS ?? '', null, 2);
TTS_API_KEY = res.tts.API_KEY;
TTS_ENGINE = res.tts.ENGINE;
@@ -171,6 +190,9 @@
STT_AZURE_BASE_URL = res.stt.AZURE_BASE_URL;
STT_AZURE_MAX_SPEAKERS = res.stt.AZURE_MAX_SPEAKERS;
STT_DEEPGRAM_API_KEY = res.stt.DEEPGRAM_API_KEY;
STT_MISTRAL_API_KEY = res.stt.MISTRAL_API_KEY;
STT_MISTRAL_API_BASE_URL = res.stt.MISTRAL_API_BASE_URL;
STT_MISTRAL_USE_CHAT_COMPLETIONS = res.stt.MISTRAL_USE_CHAT_COMPLETIONS;
}
await getVoices();
@@ -188,7 +210,7 @@
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
<div class="flex flex-col gap-3">
<div>
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Speech-to-Text')}</div>
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Speech-to-Text')}</div>
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
@@ -222,6 +244,7 @@
<option value="web">{$i18n.t('Web API')}</option>
<option value="deepgram">{$i18n.t('Deepgram')}</option>
<option value="azure">{$i18n.t('Azure AI Speech')}</option>
<option value="mistral">{$i18n.t('MistralAI')}</option>
</select>
</div>
</div>
@@ -354,6 +377,67 @@
</div>
</div>
</div>
{:else if STT_ENGINE === 'mistral'}
<div>
<div class="mt-1 flex gap-2 mb-1">
<input
class="flex-1 w-full bg-transparent outline-hidden"
placeholder={$i18n.t('API Base URL')}
bind:value={STT_MISTRAL_API_BASE_URL}
required
/>
<SensitiveInput placeholder={$i18n.t('API Key')} bind:value={STT_MISTRAL_API_KEY} />
</div>
</div>
<hr class="border-gray-100 dark:border-gray-850 my-2" />
<div>
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('STT Model')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={STT_MODEL}
placeholder="voxtral-mini-latest"
/>
</div>
</div>
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
{$i18n.t('Leave empty to use the default model (voxtral-mini-latest).')}
<a
class=" hover:underline dark:text-gray-200 text-gray-800"
href="https://docs.mistral.ai/capabilities/audio_transcription"
target="_blank"
>
{$i18n.t('Learn more about Voxtral transcription.')}
</a>
</div>
</div>
<hr class="border-gray-100 dark:border-gray-850 my-2" />
<div>
<div class="flex items-center justify-between mb-2">
<div class="text-xs font-medium">{$i18n.t('Use Chat Completions API')}</div>
<label class="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
bind:checked={STT_MISTRAL_USE_CHAT_COMPLETIONS}
class="sr-only peer"
/>
<div
class="w-9 h-5 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"
></div>
</label>
</div>
<div class="text-xs text-gray-400 dark:text-gray-500">
{$i18n.t(
'Use /v1/chat/completions endpoint instead of /v1/audio/transcriptions for potentially better accuracy.'
)}
</div>
</div>
{:else if STT_ENGINE === ''}
<div>
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('STT Model')}</div>
@@ -414,7 +498,7 @@
</div>
<div>
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Text-to-Speech')}</div>
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Text-to-Speech')}</div>
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
@@ -612,6 +696,22 @@
</div>
</div>
</div>
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
<div class="w-full">
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('Additional Parameters')}</div>
<div class="flex w-full">
<div class="flex-1">
<Textarea
className="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={TTS_OPENAI_PARAMS}
placeholder={$i18n.t('Enter additional parameters in JSON format')}
minSize={100}
/>
</div>
</div>
</div>
</div>
{:else if TTS_ENGINE === 'elevenlabs'}
<div class=" flex gap-2">
<div class="w-full">
@@ -41,7 +41,7 @@
{#if config}
<div>
<div class="mb-3.5">
<div class=" mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
@@ -164,7 +164,7 @@
</div>
<div class="mb-3.5">
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Code Interpreter')}</div>
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Code Interpreter')}</div>
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
@@ -219,7 +219,7 @@
<div class=" overflow-y-scroll scrollbar-hidden h-full">
{#if ENABLE_OPENAI_API !== null && ENABLE_OLLAMA_API !== null && connectionsConfig !== null}
<div class="mb-3.5">
<div class=" mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
@@ -171,14 +171,6 @@
return;
}
if (
RAGConfig.CONTENT_EXTRACTION_ENGINE === 'datalab_marker' &&
!RAGConfig.DATALAB_MARKER_API_KEY
) {
toast.error($i18n.t('Datalab Marker API Key required.'));
return;
}
if (
RAGConfig.CONTENT_EXTRACTION_ENGINE === 'datalab_marker' &&
RAGConfig.DATALAB_MARKER_ADDITIONAL_CONFIG &&
@@ -207,10 +199,28 @@
return;
}
if (
RAGConfig.CONTENT_EXTRACTION_ENGINE === 'mineru' &&
RAGConfig.MINERU_API_MODE === 'cloud' &&
RAGConfig.MINERU_API_KEY === ''
) {
toast.error($i18n.t('MinerU API Key required for Cloud API mode.'));
return;
}
if (!RAGConfig.BYPASS_EMBEDDING_AND_RETRIEVAL) {
await embeddingModelUpdateHandler();
}
if (RAGConfig.MINERU_PARAMS) {
try {
JSON.parse(RAGConfig.MINERU_PARAMS);
} catch (e) {
toast.error($i18n.t('Invalid JSON format in MinerU Parameters'));
return;
}
}
const res = await updateRAGConfig(localStorage.token, {
...RAGConfig,
ALLOWED_FILE_EXTENSIONS: RAGConfig.ALLOWED_FILE_EXTENSIONS.split(',')
@@ -219,7 +229,13 @@
DOCLING_PICTURE_DESCRIPTION_LOCAL: JSON.parse(
RAGConfig.DOCLING_PICTURE_DESCRIPTION_LOCAL || '{}'
),
DOCLING_PICTURE_DESCRIPTION_API: JSON.parse(RAGConfig.DOCLING_PICTURE_DESCRIPTION_API || '{}')
DOCLING_PICTURE_DESCRIPTION_API: JSON.parse(
RAGConfig.DOCLING_PICTURE_DESCRIPTION_API || '{}'
),
MINERU_PARAMS:
typeof RAGConfig.MINERU_PARAMS === 'string' && RAGConfig.MINERU_PARAMS.trim() !== ''
? JSON.parse(RAGConfig.MINERU_PARAMS)
: {}
});
dispatch('save');
};
@@ -260,6 +276,11 @@
2
);
config.MINERU_PARAMS =
typeof config.MINERU_PARAMS === 'object'
? JSON.stringify(config.MINERU_PARAMS ?? {}, null, 2)
: config.MINERU_PARAMS;
RAGConfig = config;
});
</script>
@@ -316,7 +337,7 @@
<div class=" space-y-2.5 overflow-y-scroll scrollbar-hidden h-full pr-1.5">
<div class="">
<div class="mb-3">
<div class=" mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
@@ -337,6 +358,7 @@
<option value="datalab_marker">{$i18n.t('Datalab Marker API')}</option>
<option value="document_intelligence">{$i18n.t('Document Intelligence')}</option>
<option value="mistral_ocr">{$i18n.t('Mistral OCR')}</option>
<option value="mineru">{$i18n.t('MinerU')}</option>
</select>
</div>
</div>
@@ -714,6 +736,21 @@
</div>
{/if}
{/if}
<div class="flex justify-between w-full mt-2">
<div class="self-center text-xs font-medium">
<Tooltip content={''} placement="top-start">
{$i18n.t('Parameters')}
</Tooltip>
</div>
<div class="">
<Textarea
bind:value={RAGConfig.DOCLING_PARAMS}
placeholder={$i18n.t('Enter additional parameters in JSON format')}
minSize={100}
/>
</div>
</div>
{:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'document_intelligence'}
<div class="my-0.5 flex gap-2 pr-2">
<input
@@ -729,11 +766,86 @@
</div>
{:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'mistral_ocr'}
<div class="my-0.5 flex gap-2 pr-2">
<input
class="flex-1 w-full text-sm bg-transparent outline-hidden"
placeholder={$i18n.t('Enter Mistral API Base URL')}
bind:value={RAGConfig.MISTRAL_OCR_API_BASE_URL}
/>
<SensitiveInput
placeholder={$i18n.t('Enter Mistral API Key')}
bind:value={RAGConfig.MISTRAL_OCR_API_KEY}
/>
</div>
{:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'mineru'}
<!-- API Mode Selection -->
<div class="flex w-full mt-2">
<div class="flex-1 flex justify-between">
<div class="self-center text-xs font-medium">
{$i18n.t('API Mode')}
</div>
<select
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 text-xs bg-transparent outline-hidden"
bind:value={RAGConfig.MINERU_API_MODE}
on:change={() => {
// Auto-update URL when switching modes if it's empty or matches the opposite mode's default
const cloudUrl = 'https://mineru.net/api/v4';
const localUrl = 'http://localhost:8000';
if (RAGConfig.MINERU_API_MODE === 'cloud') {
if (!RAGConfig.MINERU_API_URL || RAGConfig.MINERU_API_URL === localUrl) {
RAGConfig.MINERU_API_URL = cloudUrl;
}
} else {
if (!RAGConfig.MINERU_API_URL || RAGConfig.MINERU_API_URL === cloudUrl) {
RAGConfig.MINERU_API_URL = localUrl;
}
}
}}
>
<option value="local">{$i18n.t('local')}</option>
<option value="cloud">{$i18n.t('cloud')}</option>
</select>
</div>
</div>
<!-- API URL -->
<div class="flex w-full mt-2">
<input
class="flex-1 w-full text-sm bg-transparent outline-hidden"
placeholder={RAGConfig.MINERU_API_MODE === 'cloud'
? $i18n.t('https://mineru.net/api/v4')
: $i18n.t('http://localhost:8000')}
bind:value={RAGConfig.MINERU_API_URL}
/>
</div>
<div class="flex w-full mt-2">
<SensitiveInput
placeholder={$i18n.t('Enter MinerU API Key')}
bind:value={RAGConfig.MINERU_API_KEY}
/>
</div>
<!-- Parameters -->
<div class="flex flex-col justify-between w-full mt-2">
<div class="text-xs font-medium">
<Tooltip
content={$i18n.t(
'Advanced parameters for MinerU parsing (enable_ocr, enable_formula, enable_table, language, model_version, page_ranges)'
)}
placement="top-start"
>
{$i18n.t('Parameters')}
</Tooltip>
</div>
<div class="mt-1.5">
<Textarea
bind:value={RAGConfig.MINERU_PARAMS}
placeholder={`{\n "enable_ocr": false,\n "enable_formula": true,\n "enable_table": true,\n "language": "en",\n "model_version": "pipeline",\n "page_ranges": ""\n}`}
minSize={100}
/>
</div>
</div>
{/if}
</div>
@@ -814,7 +926,7 @@
{#if !RAGConfig.BYPASS_EMBEDDING_AND_RETRIEVAL}
<div class="mb-3">
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Embedding')}</div>
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Embedding')}</div>
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
@@ -963,7 +1075,7 @@
<div class="mt-1 mb-1 text-xs text-gray-400 dark:text-gray-500">
{$i18n.t(
'Warning: If you update or change your embedding model, you will need to re-import all documents.'
'After updating or changing the embedding model, you must reindex the knowledge base for the changes to take effect. You can do this using the "Reindex" button below.'
)}
</div>
</div>
@@ -989,7 +1101,7 @@
</div>
<div class="mb-3">
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Retrieval')}</div>
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Retrieval')}</div>
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
@@ -1232,7 +1344,7 @@
{/if}
<div class="mb-3">
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Files')}</div>
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Files')}</div>
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
@@ -1344,7 +1456,7 @@
</div>
<div class="mb-3">
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Integration')}</div>
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Integration')}</div>
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
@@ -1364,7 +1476,7 @@
</div>
<div class="mb-3">
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Danger Zone')}</div>
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Danger Zone')}</div>
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
@@ -104,7 +104,7 @@
{#if evaluationConfig !== null}
<div class="">
<div class="mb-3">
<div class=" mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
@@ -119,7 +119,7 @@
{#if evaluationConfig.ENABLE_EVALUATION_ARENA_MODELS}
<div class="mb-3">
<div class=" mb-2.5 text-base font-medium flex justify-between items-center">
<div class=" mt-0.5 mb-2.5 text-base font-medium flex justify-between items-center">
<div>
{$i18n.t('Manage')}
</div>
@@ -118,11 +118,11 @@
updateHandler();
}}
>
<div class="mt-0.5 space-y-3 overflow-y-scroll scrollbar-hidden h-full">
<div class="space-y-3 overflow-y-scroll scrollbar-hidden h-full">
{#if adminConfig !== null}
<div class="">
<div class="mb-3.5">
<div class=" mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
@@ -280,7 +280,7 @@
</div>
<div class="mb-3">
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Authentication')}</div>
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Authentication')}</div>
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
@@ -399,6 +399,26 @@
>{$i18n.t("'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.")}</span
>
</div>
{#if adminConfig.JWT_EXPIRES_IN === '-1'}
<div class="mt-2 text-xs">
<div
class=" bg-yellow-500/20 text-yellow-700 dark:text-yellow-200 rounded-lg px-3 py-2"
>
<div>
<span class=" font-medium">{$i18n.t('Warning')}:</span>
<span
><a
href="https://docs.openwebui.com/getting-started/env-configuration#jwt_expires_in"
target="_blank"
class=" underline"
>{$i18n.t('No expiration can pose security risks.')}
</a></span
>
</div>
</div>
</div>
{/if}
</div>
<div class=" space-y-3">
@@ -617,7 +637,7 @@
</div>
<div class="mb-3">
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Features')}</div>
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Features')}</div>
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
File diff suppressed because it is too large Load Diff
@@ -111,7 +111,7 @@
>
<div class=" overflow-y-scroll scrollbar-hidden h-full pr-1.5">
<div class="mb-3.5">
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Tasks')}</div>
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Tasks')}</div>
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
@@ -384,7 +384,7 @@
</div>
<div class="mb-3.5">
<div class=" mb-2.5 text-base font-medium">{$i18n.t('UI')}</div>
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('UI')}</div>
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
@@ -313,7 +313,7 @@
<div class=" my-2 mb-5" id="model-list">
{#if models.length > 0}
{#each filteredModels as model, modelIdx (model.id)}
{#each filteredModels as model, modelIdx (`${model.id}-${modelIdx}`)}
<div
class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-lg transition {model
?.meta?.hidden
@@ -36,11 +36,13 @@
let updateModelId = null;
let updateProgress = null;
let updateModelsControllers = {};
let updateCancelled = false;
let showExperimentalOllama = false;
const MAX_PARALLEL_DOWNLOADS = 3;
let modelTransferring = false;
let modelLoading = false;
let modelTag = '';
let createModelLoading = false;
@@ -65,17 +67,31 @@
let deleteModelTag = '';
const updateModelsHandler = async () => {
updateCancelled = false;
toast.info('Checking for model updates...');
for (const model of ollamaModels) {
if (updateCancelled) {
break;
}
console.debug(model);
updateModelId = model.id;
const [res, controller] = await pullModel(localStorage.token, model.id, urlIdx).catch(
(error) => {
toast.error(`${error}`);
return null;
if (error.name !== 'AbortError') {
toast.error(`${error}`);
}
return [null, null];
}
);
updateModelsControllers = {
...updateModelsControllers,
[model.id]: controller
};
if (res) {
const reader = res.body
.pipeThrough(new TextDecoderStream())
@@ -108,19 +124,28 @@
} else {
updateProgress = 100;
}
} else {
toast.success(data.status);
}
}
}
}
} catch (err) {
console.error(err);
if (err.name !== 'AbortError') {
console.error(err);
}
break;
}
}
}
delete updateModelsControllers[model.id];
updateModelsControllers = { ...updateModelsControllers };
}
if (updateCancelled) {
toast.info('Model update cancelled');
} else {
toast.success('All models are up to date');
}
updateModelId = null;
updateProgress = null;
};
@@ -143,10 +168,13 @@
return;
}
modelLoading = true;
const [res, controller] = await pullModel(localStorage.token, sanitizedModelTag, urlIdx).catch(
(error) => {
toast.error(`${error}`);
return null;
if (error.name !== 'AbortError') {
toast.error(`${error}`);
}
return [null, null];
}
);
@@ -202,8 +230,6 @@
}
});
} else {
toast.success(data.status);
MODEL_DOWNLOAD_POOL.set({
...$MODEL_DOWNLOAD_POOL,
[sanitizedModelTag]: {
@@ -216,19 +242,23 @@
}
}
} catch (err) {
console.error(err);
if (typeof err !== 'string') {
err = err.message;
}
if (err.name !== 'AbortError') {
console.error(err);
if (typeof err !== 'string') {
err = err.message;
}
toast.error(`${err}`);
// opts.callback({ success: false, error, modelName: opts.modelName });
toast.error(`${err}`);
// opts.callback({ success: false, error, modelName: opts.modelName });
} else {
break;
}
}
}
console.log($MODEL_DOWNLOAD_POOL[sanitizedModelTag]);
if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag].done) {
if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag]?.done) {
toast.success(
$i18n.t(`Model '{{modelName}}' has been successfully downloaded.`, {
modelName: sanitizedModelTag
@@ -253,11 +283,11 @@
}
modelTag = '';
modelTransferring = false;
modelLoading = false;
};
const uploadModelHandler = async () => {
modelTransferring = true;
modelLoading = true;
let uploaded = false;
let fileResponse = null;
@@ -396,7 +426,7 @@
modelUploadInputElement.value = '';
}
modelInputFile = null;
modelTransferring = false;
modelLoading = false;
uploadProgress = null;
models.set(
@@ -425,6 +455,14 @@
);
};
const cancelUpdateModelHandler = async (model: string) => {
const controller = updateModelsControllers[model];
if (controller) {
controller.abort();
updateCancelled = true;
}
};
const cancelModelPullHandler = async (model: string) => {
const { reader, abortController } = $MODEL_DOWNLOAD_POOL[model];
if (abortController) {
@@ -605,59 +643,61 @@
bind:value={modelTag}
/>
</div>
<button
class="px-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
on:click={() => {
pullModelHandler();
}}
disabled={modelTransferring}
>
{#if modelTransferring}
<div class="self-center">
<svg
class=" w-4 h-4"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
<Tooltip content={$i18n.t('Pull Model')} placement="top">
<button
class="px-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
on:click={() => {
pullModelHandler();
}}
disabled={modelLoading || modelTag.trim() === ''}
>
{#if modelLoading}
<div class="self-center">
<svg
class=" w-4 h-4"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
}
</style>
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</style>
<path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/>
<path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/>
</svg>
</div>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z"
/>
<path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
/>
</svg>
</div>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z"
/>
<path
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
/>
</svg>
{/if}
</button>
{/if}
</button>
</Tooltip>
</div>
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
@@ -670,8 +710,35 @@
</div>
{#if updateModelId}
<div class="text-xs">
Updating "{updateModelId}" {updateProgress ? `(${updateProgress}%)` : ''}
<div class="text-xs flex justify-between items-center">
<div>Updating "{updateModelId}" {updateProgress ? `(${updateProgress}%)` : ''}</div>
<Tooltip content={$i18n.t('Cancel')}>
<button
class="text-gray-800 dark:text-gray-100"
on:click={() => {
cancelUpdateModelHandler(updateModelId);
}}
>
<svg
class="w-4 h-4 text-gray-800 dark:text-white"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18 17.94 6M18 18 6.06 6"
/>
</svg>
</button>
</Tooltip>
</div>
{/if}
@@ -754,25 +821,28 @@
{/each}
</select>
</div>
<button
class="px-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
on:click={() => {
showModelDeleteConfirm = true;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
<Tooltip content={$i18n.t('Delete Model')} placement="top">
<button
class="px-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
on:click={() => {
showModelDeleteConfirm = true;
}}
disabled={deleteModelTag === ''}
>
<path
fill-rule="evenodd"
d="M5 3.25V4H2.75a.75.75 0 0 0 0 1.5h.3l.815 8.15A1.5 1.5 0 0 0 5.357 15h5.285a1.5 1.5 0 0 0 1.493-1.35l.815-8.15h.3a.75.75 0 0 0 0-1.5H11v-.75A2.25 2.25 0 0 0 8.75 1h-1.5A2.25 2.25 0 0 0 5 3.25Zm2.25-.75a.75.75 0 0 0-.75.75V4h3v-.75a.75.75 0 0 0-.75-.75h-1.5ZM6.05 6a.75.75 0 0 1 .787.713l.275 5.5a.75.75 0 0 1-1.498.075l-.275-5.5A.75.75 0 0 1 6.05 6Zm3.9 0a.75.75 0 0 1 .712.787l-.275 5.5a.75.75 0 0 1-1.498-.075l.275-5.5a.75.75 0 0 1 .786-.711Z"
clip-rule="evenodd"
/>
</svg>
</button>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M5 3.25V4H2.75a.75.75 0 0 0 0 1.5h.3l.815 8.15A1.5 1.5 0 0 0 5.357 15h5.285a1.5 1.5 0 0 0 1.493-1.35l.815-8.15h.3a.75.75 0 0 0 0-1.5H11v-.75A2.25 2.25 0 0 0 8.75 1h-1.5A2.25 2.25 0 0 0 5 3.25Zm2.25-.75a.75.75 0 0 0-.75.75V4h3v-.75a.75.75 0 0 0-.75-.75h-1.5ZM6.05 6a.75.75 0 0 1 .787.713l.275 5.5a.75.75 0 0 1-1.498.075l-.275-5.5A.75.75 0 0 1 6.05 6Zm3.9 0a.75.75 0 0 1 .712.787l-.275 5.5a.75.75 0 0 1-1.498-.075l.275-5.5a.75.75 0 0 1 .786-.711Z"
clip-rule="evenodd"
/>
</svg>
</button>
</Tooltip>
</div>
</div>
@@ -799,27 +869,31 @@
</div>
<div class="flex self-start">
<button
class="px-2.5 py-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition disabled:cursor-not-allowed"
on:click={() => {
createModelHandler();
}}
disabled={createModelLoading}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="size-4"
<Tooltip content={$i18n.t('Create Model')} placement="top">
<button
class="px-2.5 py-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition disabled:cursor-not-allowed"
on:click={() => {
createModelHandler();
}}
disabled={createModelLoading ||
createModelName.trim() === '' ||
createModelObject.trim() === ''}
>
<path
d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z"
/>
<path
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
/>
</svg>
</button>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="size-4"
>
<path
d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z"
/>
<path
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
/>
</svg>
</button>
</Tooltip>
</div>
</div>
@@ -936,57 +1010,59 @@
</div>
{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
<button
class="px-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg disabled:cursor-not-allowed transition"
type="submit"
disabled={modelTransferring}
>
{#if modelTransferring}
<div class="self-center">
<svg
class=" w-4 h-4"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
<Tooltip content={$i18n.t('Upload Model')} placement="top">
<button
class="px-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg disabled:cursor-not-allowed transition"
type="submit"
disabled={modelLoading}
>
{#if modelLoading}
<div class="self-center">
<svg
class=" w-4 h-4"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
}
</style>
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</style>
<path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/>
<path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/>
</svg>
</div>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z"
/>
<path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
/>
</svg>
</div>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z"
/>
<path
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
/>
</svg>
{/if}
</button>
{/if}
</button>
</Tooltip>
{/if}
</div>
@@ -59,7 +59,7 @@
{#if servers !== null}
<div class="">
<div class="mb-3">
<div class=" mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
@@ -83,7 +83,7 @@
</Tooltip>
</div>
<div class="flex flex-col gap-1.5">
<div class="flex flex-col gap-1">
{#each servers as server, idx}
<Connection
bind:connection={server}
@@ -95,7 +95,7 @@
{#if webConfig}
<div class="">
<div class="mb-3">
<div class=" mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
@@ -724,7 +724,7 @@
</div>
<div class="mb-3">
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Loader')}</div>
<div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Loader')}</div>
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
+2 -8
View File
@@ -56,14 +56,7 @@
let showDefaultPermissionsModal = false;
const setGroups = async () => {
const allGroups = await getGroups(localStorage.token);
const userGroup = allGroups.find((g) => g.name.toLowerCase() === 'user');
if (userGroup) {
defaultPermissions = userGroup.permissions;
}
groups = allGroups.filter((g) => g.name.toLowerCase() !== 'user');
groups = await getGroups(localStorage.token);
};
const addGroupHandler = async (group) => {
@@ -110,6 +103,7 @@
total = res.total;
}
defaultPermissions = await getUserDefaultPermissions(localStorage.token);
await setGroups();
loaded = true;
});
+9 -11
View File
@@ -112,17 +112,6 @@
}}
/>
{#key selectedUser}
<EditUserModal
bind:show={showEditUserModal}
{selectedUser}
sessionUser={$user}
on:save={async () => {
getUserList();
}}
/>
{/key}
<AddUserModal
bind:show={showAddUserModal}
on:save={async () => {
@@ -130,6 +119,15 @@
}}
/>
<EditUserModal
bind:show={showEditUserModal}
{selectedUser}
sessionUser={$user}
on:save={async () => {
getUserList();
}}
/>
{#if selectedUser}
<UserChatsModal bind:show={showUserChatsModal} user={selectedUser} />
{/if}
@@ -22,6 +22,18 @@
export let selectedUser;
export let sessionUser;
$: if (show) {
init();
}
const init = () => {
if (selectedUser) {
_user = selectedUser;
_user.password = '';
loadUserGroups();
}
};
let _user = {
profile_image_url: '',
role: 'pending',
@@ -52,14 +64,6 @@
return null;
});
};
onMount(() => {
if (selectedUser) {
_user = selectedUser;
_user.password = '';
loadUserGroups();
}
});
</script>
<Modal size="sm" bind:show>
@@ -87,7 +91,11 @@
<div class=" px-5 pt-3 pb-5 w-full">
<div class="flex self-center w-full">
<div class=" self-start h-full mr-6">
<UserProfileImage bind:profileImageUrl={_user.profile_image_url} user={_user} />
<UserProfileImage
imageClassName="size-14"
bind:profileImageUrl={_user.profile_image_url}
user={_user}
/>
</div>
<div class=" flex-1">
+3 -3
View File
@@ -160,7 +160,7 @@
};
const onChange = async () => {
$socket?.emit('channel-events', {
$socket?.emit('events:channel', {
channel_id: id,
message_id: null,
data: {
@@ -180,7 +180,7 @@
chatId.set('');
}
$socket?.on('channel-events', channelEventHandler);
$socket?.on('events:channel', channelEventHandler);
mediaQuery = window.matchMedia('(min-width: 1024px)');
@@ -197,7 +197,7 @@
});
onDestroy(() => {
$socket?.off('channel-events', channelEventHandler);
$socket?.off('events:channel', channelEventHandler);
});
</script>
@@ -876,12 +876,12 @@
richText={$settings?.richTextInput ?? true}
showFormattingToolbar={$settings?.showFormattingToolbar ?? false}
shiftEnter={!($settings?.ctrlEnterToSend ?? false) &&
(!$mobile ||
!(
'ontouchstart' in window ||
navigator.maxTouchPoints > 0 ||
navigator.msMaxTouchPoints > 0
))}
!$mobile &&
!(
'ontouchstart' in window ||
navigator.maxTouchPoints > 0 ||
navigator.msMaxTouchPoints > 0
)}
largeTextAsFile={$settings?.largeTextAsFile ?? false}
floatingMenuPlacement={'top-start'}
{suggestions}
+1
View File
@@ -19,6 +19,7 @@
<nav class="sticky top-0 z-30 w-full px-1.5 py-1.5 -mb-8 flex items-center drag-region">
<div
id="navbar-bg-gradient-to-b"
class=" bg-linear-to-b via-50% from-white via-white to-transparent dark:from-gray-900 dark:via-gray-900 dark:to-transparent pointer-events-none absolute inset-0 -bottom-7 z-[-1]"
></div>
+3 -3
View File
@@ -143,7 +143,7 @@
};
const onChange = async () => {
$socket?.emit('channel-events', {
$socket?.emit('events:channel', {
channel_id: channel.id,
message_id: threadId,
data: {
@@ -156,11 +156,11 @@
};
onMount(() => {
$socket?.on('channel-events', channelEventHandler);
$socket?.on('events:channel', channelEventHandler);
});
onDestroy(() => {
$socket?.off('channel-events', channelEventHandler);
$socket?.off('events:channel', channelEventHandler);
});
</script>
+20 -113
View File
@@ -4,7 +4,14 @@
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
import { artifactCode, chatId, settings, showArtifacts, showControls } from '$lib/stores';
import {
artifactCode,
chatId,
settings,
showArtifacts,
showControls,
artifactContents
} from '$lib/stores';
import { copyToClipboard, createMessagesList } from '$lib/utils';
import XMark from '../icons/XMark.svelte';
@@ -15,8 +22,6 @@
import Download from '../icons/Download.svelte';
export let overlay = false;
export let history;
let messages = [];
let contents: Array<{ type: string; content: string }> = [];
let selectedContentIdx = 0;
@@ -24,121 +29,11 @@
let copied = false;
let iframeElement: HTMLIFrameElement;
$: if (history) {
messages = createMessagesList(history, history.currentId);
getContents();
} else {
messages = [];
getContents();
}
const getContents = () => {
contents = [];
messages.forEach((message) => {
if (message?.role !== 'user' && message?.content) {
const codeBlockContents = message.content.match(/```[\s\S]*?```/g);
let codeBlocks = [];
if (codeBlockContents) {
codeBlockContents.forEach((block) => {
const lang = block.split('\n')[0].replace('```', '').trim().toLowerCase();
const code = block.replace(/```[\s\S]*?\n/, '').replace(/```$/, '');
codeBlocks.push({ lang, code });
});
}
let htmlContent = '';
let cssContent = '';
let jsContent = '';
codeBlocks.forEach((block) => {
const { lang, code } = block;
if (lang === 'html') {
htmlContent += code + '\n';
} else if (lang === 'css') {
cssContent += code + '\n';
} else if (lang === 'javascript' || lang === 'js') {
jsContent += code + '\n';
}
});
const inlineHtml = message.content.match(/<html>[\s\S]*?<\/html>/gi);
const inlineCss = message.content.match(/<style>[\s\S]*?<\/style>/gi);
const inlineJs = message.content.match(/<script>[\s\S]*?<\/script>/gi);
if (inlineHtml) {
inlineHtml.forEach((block) => {
const content = block.replace(/<\/?html>/gi, ''); // Remove <html> tags
htmlContent += content + '\n';
});
}
if (inlineCss) {
inlineCss.forEach((block) => {
const content = block.replace(/<\/?style>/gi, ''); // Remove <style> tags
cssContent += content + '\n';
});
}
if (inlineJs) {
inlineJs.forEach((block) => {
const content = block.replace(/<\/?script>/gi, ''); // Remove <script> tags
jsContent += content + '\n';
});
}
if (htmlContent || cssContent || jsContent) {
const renderedContent = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<${''}style>
body {
background-color: white; /* Ensure the iframe has a white background */
}
${cssContent}
</${''}style>
</head>
<body>
${htmlContent}
<${''}script>
${jsContent}
</${''}script>
</body>
</html>
`;
contents = [...contents, { type: 'iframe', content: renderedContent }];
} else {
// Check for SVG content
for (const block of codeBlocks) {
if (block.lang === 'svg' || (block.lang === 'xml' && block.code.includes('<svg'))) {
contents = [...contents, { type: 'svg', content: block.code }];
}
}
}
}
});
if (contents.length === 0) {
showControls.set(false);
showArtifacts.set(false);
}
selectedContentIdx = contents ? contents.length - 1 : 0;
};
function navigateContent(direction: 'prev' | 'next') {
console.log(selectedContentIdx);
selectedContentIdx =
direction === 'prev'
? Math.max(selectedContentIdx - 1, 0)
: Math.min(selectedContentIdx + 1, contents.length - 1);
console.log(selectedContentIdx);
}
const iframeLoadHandler = () => {
@@ -201,6 +96,18 @@
selectedContentIdx = codeIdx !== -1 ? codeIdx : 0;
}
});
artifactContents.subscribe((value) => {
contents = value;
console.log('Artifact contents updated:', contents);
if (contents.length === 0) {
showControls.set(false);
showArtifacts.set(false);
}
selectedContentIdx = contents ? contents.length - 1 : 0;
});
});
</script>
+167 -37
View File
@@ -4,6 +4,7 @@
import { PaneGroup, Pane, PaneResizer } from 'paneforge';
import { getContext, onDestroy, onMount, tick } from 'svelte';
import { fade } from 'svelte/transition';
const i18n: Writable<i18nType> = getContext('i18n');
import { goto } from '$app/navigation';
@@ -26,6 +27,7 @@
banners,
user,
socket,
audioQueue,
showControls,
showCallOverlay,
currentChatPage,
@@ -34,6 +36,7 @@
showOverview,
chatTitle,
showArtifacts,
artifactContents,
tools,
toolServers,
functions,
@@ -41,6 +44,7 @@
pinnedChats,
showEmbeds
} from '$lib/stores';
import {
convertMessagesToHistory,
copyToClipboard,
@@ -48,8 +52,10 @@
createMessagesList,
getPromptVariables,
processDetails,
removeAllDetails
removeAllDetails,
getCodeBlockContents
} from '$lib/utils';
import { AudioQueue } from '$lib/utils/audio';
import {
createNewChat,
@@ -75,8 +81,8 @@
import { getTools } from '$lib/apis/tools';
import { uploadFile } from '$lib/apis/files';
import { createOpenAITextStream } from '$lib/apis/streaming';
import { fade } from 'svelte/transition';
import { getFunctions } from '$lib/apis/functions';
import { updateFolderById } from '$lib/apis/folders';
import Banner from '../common/Banner.svelte';
import MessageInput from '$lib/components/chat/MessageInput.svelte';
@@ -89,7 +95,6 @@
import Spinner from '../common/Spinner.svelte';
import Tooltip from '../common/Tooltip.svelte';
import Sidebar from '../icons/Sidebar.svelte';
import { getFunctions } from '$lib/apis/functions';
import Image from '../common/Image.svelte';
export let chatIdProp = '';
@@ -191,6 +196,8 @@
codeInterpreterEnabled = input.codeInterpreterEnabled;
}
} catch (e) {}
} else {
await setDefaults();
}
const chatInput = document.getElementById('chat-input');
@@ -219,10 +226,15 @@
}
const saveSessionSelectedModels = () => {
if (selectedModels.length === 0 || (selectedModels.length === 1 && selectedModels[0] === '')) {
const selectedModelsString = JSON.stringify(selectedModels);
if (
selectedModels.length === 0 ||
(selectedModels.length === 1 && selectedModels[0] === '') ||
sessionStorage.selectedModels === selectedModelsString
) {
return;
}
sessionStorage.selectedModels = JSON.stringify(selectedModels);
sessionStorage.selectedModels = selectedModelsString;
console.log('saveSessionSelectedModels', selectedModels, sessionStorage.selectedModels);
};
@@ -294,7 +306,7 @@
}
};
const showMessage = async (message) => {
const showMessage = async (message, ignoreSettings = false) => {
await tick();
const _chatId = JSON.parse(JSON.stringify($chatId));
@@ -320,7 +332,7 @@
await tick();
await tick();
if ($settings?.scrollOnBranchChange ?? true) {
if (($settings?.scrollOnBranchChange ?? true) || ignoreSettings) {
const messageElement = document.getElementById(`message-${message.id}`);
if (messageElement) {
messageElement.scrollIntoView({ behavior: 'smooth' });
@@ -499,18 +511,50 @@
}
};
const savedModelIds = async () => {
if (
$selectedFolder &&
selectedModels.filter((modelId) => modelId !== '').length > 0 &&
JSON.stringify($selectedFolder?.data?.model_ids) !== JSON.stringify(selectedModels)
) {
const res = await updateFolderById(localStorage.token, $selectedFolder.id, {
data: {
model_ids: selectedModels
}
});
}
};
$: if (selectedModels !== null) {
savedModelIds();
}
let pageSubscribe = null;
let showControlsSubscribe = null;
let selectedFolderSubscribe = null;
const stopAudio = () => {
try {
speechSynthesis.cancel();
$audioQueue.stop();
} catch {}
};
onMount(async () => {
loading = true;
console.log('mounted');
window.addEventListener('message', onMessageHandler);
$socket?.on('chat-events', chatEventHandler);
$socket?.on('events', chatEventHandler);
audioQueue.set(new AudioQueue(document.getElementById('audioElement')));
pageSubscribe = page.subscribe(async (p) => {
if (p.url.pathname === '/') {
await tick();
initNewChat();
}
stopAudio();
});
const storageChatInput = sessionStorage.getItem(
@@ -548,7 +592,7 @@
} catch (e) {}
}
showControls.subscribe(async (value) => {
showControlsSubscribe = showControls.subscribe(async (value) => {
if (controlPane && !$mobile) {
try {
if (value) {
@@ -569,17 +613,33 @@
}
});
selectedFolderSubscribe = selectedFolder.subscribe(async (folder) => {
if (
folder?.data?.model_ids &&
JSON.stringify(selectedModels) !== JSON.stringify(folder.data.model_ids)
) {
selectedModels = folder.data.model_ids;
console.log('Set selectedModels from folder data:', selectedModels);
}
});
const chatInput = document.getElementById('chat-input');
chatInput?.focus();
chats.subscribe(() => {});
});
onDestroy(() => {
pageSubscribe();
chatIdUnsubscriber?.();
window.removeEventListener('message', onMessageHandler);
$socket?.off('chat-events', chatEventHandler);
try {
pageSubscribe();
showControlsSubscribe();
selectedFolderSubscribe();
chatIdUnsubscriber?.();
window.removeEventListener('message', onMessageHandler);
$socket?.off('events', chatEventHandler);
$audioQueue?.destroy();
} catch (e) {
console.error(e);
}
});
// File upload functions
@@ -775,11 +835,69 @@
}
};
$: if (history) {
getContents();
} else {
artifactContents.set([]);
}
const getContents = () => {
const messages = history ? createMessagesList(history, history.currentId) : [];
let contents = [];
messages.forEach((message) => {
if (message?.role !== 'user' && message?.content) {
const {
codeBlocks: codeBlocks,
html: htmlContent,
css: cssContent,
js: jsContent
} = getCodeBlockContents(message.content);
if (htmlContent || cssContent || jsContent) {
const renderedContent = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<${''}style>
body {
background-color: white; /* Ensure the iframe has a white background */
}
${cssContent}
</${''}style>
</head>
<body>
${htmlContent}
<${''}script>
${jsContent}
</${''}script>
</body>
</html>
`;
contents = [...contents, { type: 'iframe', content: renderedContent }];
} else {
// Check for SVG content
for (const block of codeBlocks) {
if (block.lang === 'svg' || (block.lang === 'xml' && block.code.includes('<svg'))) {
contents = [...contents, { type: 'svg', content: block.code }];
}
}
}
}
});
artifactContents.set(contents);
};
//////////////////////////
// Web functions
//////////////////////////
const initNewChat = async () => {
console.log('initNewChat');
if ($user?.role !== 'admin' && $user?.permissions?.chat?.temporary_enforced) {
await temporaryChatEnabled.set(true);
}
@@ -830,17 +948,22 @@
$models.map((m) => m.id).includes(modelId)
);
} else {
if (sessionStorage.selectedModels) {
selectedModels = JSON.parse(sessionStorage.selectedModels);
sessionStorage.removeItem('selectedModels');
if ($selectedFolder?.data?.model_ids) {
selectedModels = $selectedFolder?.data?.model_ids;
} else {
if ($settings?.models) {
selectedModels = $settings?.models;
} else if ($config?.default_models) {
console.log($config?.default_models.split(',') ?? '');
selectedModels = $config?.default_models.split(',');
if (sessionStorage.selectedModels) {
selectedModels = JSON.parse(sessionStorage.selectedModels);
sessionStorage.removeItem('selectedModels');
} else {
if ($settings?.models) {
selectedModels = $settings?.models;
} else if ($config?.default_models) {
console.log($config?.default_models.split(',') ?? '');
selectedModels = $config?.default_models.split(',');
}
}
}
selectedModels = selectedModels.filter((modelId) => availableModels.includes(modelId));
}
@@ -1032,7 +1155,7 @@
});
}
};
const chatCompletedHandler = async (chatId, modelId, responseMessageId, messages) => {
const chatCompletedHandler = async (_chatId, modelId, responseMessageId, messages) => {
const res = await chatCompleted(localStorage.token, {
model: modelId,
messages: messages.map((m) => ({
@@ -1046,7 +1169,7 @@
})),
filter_ids: selectedFilterIds.length > 0 ? selectedFilterIds : undefined,
model_item: $models.find((m) => m.id === modelId),
chat_id: chatId,
chat_id: _chatId,
session_id: $socket?.id,
id: responseMessageId
}).catch((error) => {
@@ -1074,9 +1197,9 @@
await tick();
if ($chatId == chatId) {
if ($chatId == _chatId) {
if (!$temporaryChatEnabled) {
chat = await updateChatById(localStorage.token, chatId, {
chat = await updateChatById(localStorage.token, _chatId, {
models: selectedModels,
messages: messages,
history: history,
@@ -1092,7 +1215,7 @@
taskIds = null;
};
const chatActionHandler = async (chatId, actionId, modelId, responseMessageId, event = null) => {
const chatActionHandler = async (_chatId, actionId, modelId, responseMessageId, event = null) => {
const messages = createMessagesList(history, responseMessageId);
const res = await chatAction(localStorage.token, actionId, {
@@ -1107,7 +1230,7 @@
})),
...(event ? { event: event } : {}),
model_item: $models.find((m) => m.id === modelId),
chat_id: chatId,
chat_id: _chatId,
session_id: $socket?.id,
id: responseMessageId
}).catch((error) => {
@@ -1129,9 +1252,9 @@
}
}
if ($chatId == chatId) {
if ($chatId == _chatId) {
if (!$temporaryChatEnabled) {
chat = await updateChatById(localStorage.token, chatId, {
chat = await updateChatById(localStorage.token, _chatId, {
models: selectedModels,
messages: messages,
history: history,
@@ -1499,7 +1622,7 @@
chatFiles.push(
..._files.filter((item) =>
['doc', 'text', 'file', 'note', 'chat', 'collection'].includes(item.type)
['doc', 'text', 'file', 'note', 'chat', 'folder', 'collection'].includes(item.type)
)
);
chatFiles = chatFiles.filter(
@@ -2159,8 +2282,8 @@
selectedFolder.set(null);
} else {
_chatId = 'local';
await chatId.set('local');
_chatId = `local:${$socket?.id}`; // Use socket id for temporary chat
await chatId.set(_chatId);
}
await tick();
@@ -2240,7 +2363,7 @@
</title>
</svelte:head>
<audio id="audioElement" src="" style="display: none;" />
<audio id="audioElement" src="" style="display: none;"></audio>
<EventConfirmDialog
bind:show={showEventConfirmation}
@@ -2392,7 +2515,7 @@
</div>
</div>
<div class=" pb-2">
<div class=" pb-2 z-10">
<MessageInput
bind:this={messageInput}
{history}
@@ -2522,3 +2645,10 @@
</div>
{/if}
</div>
<style>
::-webkit-scrollbar {
height: 0.5rem;
width: 0.5rem;
}
</style>
+126 -125
View File
@@ -13,12 +13,9 @@
showEmbeds
} from '$lib/stores';
import Modal from '../common/Modal.svelte';
import Controls from './Controls/Controls.svelte';
import CallOverlay from './MessageInput/CallOverlay.svelte';
import Drawer from '../common/Drawer.svelte';
import Overview from './Overview.svelte';
import EllipsisVertical from '../icons/EllipsisVertical.svelte';
import Artifacts from './Artifacts.svelte';
import Embeds from './ChatControls/Embeds.svelte';
@@ -154,24 +151,113 @@
}
</script>
<SvelteFlowProvider>
{#if !largeScreen}
{#if $showControls}
<Drawer
show={$showControls}
onClose={() => {
showControls.set(false);
}}
{#if !largeScreen}
{#if $showControls}
<Drawer
show={$showControls}
onClose={() => {
showControls.set(false);
}}
>
<div
class=" {$showCallOverlay || $showOverview || $showArtifacts || $showEmbeds
? ' h-screen w-full'
: 'px-4 py-3'} h-full"
>
{#if $showCallOverlay}
<div
class=" h-full max-h-[100dvh] bg-white text-gray-700 dark:bg-black dark:text-gray-300 flex justify-center"
>
<CallOverlay
bind:files
{submitPrompt}
{stopResponse}
{modelId}
{chatId}
{eventTarget}
on:close={() => {
showControls.set(false);
}}
/>
</div>
{:else if $showEmbeds}
<Embeds />
{:else if $showArtifacts}
<Artifacts {history} />
{:else if $showOverview}
{#await import('./Overview.svelte') then { default: Overview }}
<Overview
{history}
onNodeClick={(e) => {
const node = e.node;
showMessage(node.data.message, true);
}}
onClose={() => {
showControls.set(false);
}}
/>
{/await}
{:else}
<Controls
on:close={() => {
showControls.set(false);
}}
{models}
bind:chatFiles
bind:params
/>
{/if}
</div>
</Drawer>
{/if}
{:else}
<!-- if $showControls -->
{#if $showControls}
<PaneResizer
class="relative flex items-center justify-center group border-l border-gray-50 dark:border-gray-850 hover:border-gray-200 dark:hover:border-gray-800 transition z-20"
id="controls-resizer"
>
<div
class=" absolute -left-1.5 -right-1.5 -top-0 -bottom-0 z-20 cursor-col-resize bg-transparent"
/>
</PaneResizer>
{/if}
<Pane
bind:pane
defaultSize={0}
onResize={(size) => {
if ($showControls && pane.isExpanded()) {
if (size < minSize) {
pane.resize(minSize);
}
if (size < minSize) {
localStorage.chatControlsSize = 0;
} else {
// save the size in pixels to localStorage
const container = document.getElementById('chat-container');
localStorage.chatControlsSize = Math.floor((size / 100) * container.clientWidth);
}
}
}}
onCollapse={() => {
showControls.set(false);
}}
collapsible={true}
class=" z-10 bg-white dark:bg-gray-850"
>
{#if $showControls}
<div class="flex max-h-full min-h-full">
<div
class=" {$showCallOverlay || $showOverview || $showArtifacts || $showEmbeds
? ' h-screen w-full'
: 'px-4 py-3'} h-full"
class="w-full {($showOverview || $showArtifacts || $showEmbeds) && !$showCallOverlay
? ' '
: 'px-4 py-3 bg-white dark:shadow-lg dark:bg-gray-850 '} z-40 pointer-events-auto overflow-y-auto scrollbar-hidden"
id="controls-container"
>
{#if $showCallOverlay}
<div
class=" h-full max-h-[100dvh] bg-white text-gray-700 dark:bg-black dark:text-gray-300 flex justify-center"
>
<div class="w-full h-full flex justify-center">
<CallOverlay
bind:files
{submitPrompt}
@@ -185,19 +271,28 @@
/>
</div>
{:else if $showEmbeds}
<Embeds />
<Embeds overlay={dragged} />
{:else if $showArtifacts}
<Artifacts {history} />
<Artifacts {history} overlay={dragged} />
{:else if $showOverview}
<Overview
{history}
on:nodeclick={(e) => {
showMessage(e.detail.node.data.message);
}}
on:close={() => {
showControls.set(false);
}}
/>
{#await import('./Overview.svelte') then { default: Overview }}
<Overview
{history}
onNodeClick={(e) => {
const node = e.node;
if (node?.data?.message?.favorite) {
history.messages[node.data.message.id].favorite = true;
} else {
history.messages[node.data.message.id].favorite = null;
}
showMessage(node.data.message, true);
}}
onClose={() => {
showControls.set(false);
}}
/>
{/await}
{:else}
<Controls
on:close={() => {
@@ -209,101 +304,7 @@
/>
{/if}
</div>
</Drawer>
</div>
{/if}
{:else}
<!-- if $showControls -->
{#if $showControls}
<PaneResizer
class="relative flex items-center justify-center group border-l border-gray-50 dark:border-gray-850 hover:border-gray-200 dark:hover:border-gray-800 transition z-20"
id="controls-resizer"
>
<div
class=" absolute -left-1.5 -right-1.5 -top-0 -bottom-0 z-20 cursor-col-resize bg-transparent"
/>
</PaneResizer>
{/if}
<Pane
bind:pane
defaultSize={0}
onResize={(size) => {
if ($showControls && pane.isExpanded()) {
if (size < minSize) {
pane.resize(minSize);
}
if (size < minSize) {
localStorage.chatControlsSize = 0;
} else {
// save the size in pixels to localStorage
const container = document.getElementById('chat-container');
localStorage.chatControlsSize = Math.floor((size / 100) * container.clientWidth);
}
}
}}
onCollapse={() => {
showControls.set(false);
}}
collapsible={true}
class=" z-10 bg-white dark:bg-gray-850"
>
{#if $showControls}
<div class="flex max-h-full min-h-full">
<div
class="w-full {($showOverview || $showArtifacts || $showEmbeds) && !$showCallOverlay
? ' '
: 'px-4 py-3 bg-white dark:shadow-lg dark:bg-gray-850 '} z-40 pointer-events-auto overflow-y-auto scrollbar-hidden"
id="controls-container"
>
{#if $showCallOverlay}
<div class="w-full h-full flex justify-center">
<CallOverlay
bind:files
{submitPrompt}
{stopResponse}
{modelId}
{chatId}
{eventTarget}
on:close={() => {
showControls.set(false);
}}
/>
</div>
{:else if $showEmbeds}
<Embeds overlay={dragged} />
{:else if $showArtifacts}
<Artifacts {history} overlay={dragged} />
{:else if $showOverview}
<Overview
{history}
on:nodeclick={(e) => {
if (e.detail.node.data.message.favorite) {
history.messages[e.detail.node.data.message.id].favorite = true;
} else {
history.messages[e.detail.node.data.message.id].favorite = null;
}
showMessage(e.detail.node.data.message);
}}
on:close={() => {
showControls.set(false);
}}
/>
{:else}
<Controls
on:close={() => {
showControls.set(false);
}}
{models}
bind:chatFiles
bind:params
/>
{/if}
</div>
</div>
{/if}
</Pane>
{/if}
</SvelteFlowProvider>
</Pane>
{/if}
@@ -13,9 +13,14 @@
class="pointer-events-auto z-20 flex justify-between items-center py-3 px-2 font-primar text-gray-900 dark:text-white"
>
<div class="flex-1 flex items-center justify-between pl-2">
<div class="flex items-center space-x-2">
<a
class="flex items-center space-x-2 hover:underline"
href={$embed?.url}
target="_blank"
rel="noopener noreferrer"
>
{$embed?.title ?? 'Embedded Content'}
</div>
</a>
</div>
<button
@@ -13,6 +13,7 @@
import LightBulb from '$lib/components/icons/LightBulb.svelte';
import Markdown from '../Messages/Markdown.svelte';
import Skeleton from '../Messages/Skeleton.svelte';
import { chatId, models, socket } from '$lib/stores';
export let id = '';
export let messageId = '';
@@ -118,6 +119,9 @@
let res;
[res, controller] = await chatCompletion(localStorage.token, {
model: model,
model_item: $models.find((m) => m.id === model),
session_id: $socket?.id,
chat_id: $chatId,
messages: [
...messages,
{
@@ -246,11 +250,11 @@
{#if responseContent === null}
{#if !floatingInput}
<div
class="flex flex-row gap-0.5 shrink-0 p-1 bg-white dark:bg-gray-850 dark:text-gray-100 text-medium rounded-lg shadow-xl"
class="flex flex-row shrink-0 p-0.5 bg-white dark:bg-gray-850 dark:text-gray-100 text-medium rounded-xl shadow-xl border border-gray-100 dark:border-gray-800"
>
{#each actions as action}
<button
class="px-1 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-sm flex items-center gap-1 min-w-fit"
class="px-1.5 py-[1px] hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl flex items-center gap-1 min-w-fit transition"
on:click={async () => {
selectedText = window.getSelection().toString();
selectedAction = action;
@@ -280,7 +284,7 @@
</div>
{:else}
<div
class="py-1 flex dark:text-gray-100 bg-gray-50 dark:bg-gray-800 border border-gray-100 dark:border-gray-850 w-72 rounded-full shadow-xl"
class="py-1 flex dark:text-gray-100 bg-white dark:bg-gray-850 border border-gray-100 dark:border-gray-800 w-72 rounded-full shadow-xl"
>
<input
type="text"
@@ -295,7 +299,7 @@
}}
/>
<div class="ml-1 mr-2">
<div class="ml-1 mr-1">
<button
class="{floatingInputValue !== ''
? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
@@ -321,19 +325,22 @@
</div>
{/if}
{:else}
<div class="bg-white dark:bg-gray-850 dark:text-gray-100 rounded-xl shadow-xl w-80 max-w-full">
<div
class="bg-white dark:bg-gray-850 dark:text-gray-100 rounded-3xl shadow-xl w-80 max-w-full border border-gray-100 dark:border-gray-800"
>
<div
class="bg-gray-50/50 dark:bg-gray-800 dark:text-gray-100 text-medium rounded-xl px-3.5 py-3 w-full"
class="bg-white dark:bg-gray-850 dark:text-gray-100 text-medium rounded-3xl px-3.5 pt-3 w-full"
>
<div class="font-medium">
<Markdown id={`${id}-float-prompt`} {content} />
</div>
</div>
<div
class="bg-white dark:bg-gray-850 dark:text-gray-100 text-medium rounded-xl px-3.5 py-3 w-full"
>
<div class=" max-h-80 overflow-y-auto w-full markdown-prose-xs" id="response-container">
<div class="bg-white dark:bg-gray-850 dark:text-gray-100 text-medium rounded-4xl w-full">
<div
class=" max-h-80 overflow-y-auto w-full markdown-prose-xs px-3.5 py-3"
id="response-container"
>
{#if !responseContent || responseContent?.trim() === ''}
<Skeleton size="sm" />
{:else}
File diff suppressed because it is too large Load Diff
@@ -13,6 +13,8 @@
import Database from '$lib/components/icons/Database.svelte';
import GlobeAlt from '$lib/components/icons/GlobeAlt.svelte';
import Youtube from '$lib/components/icons/Youtube.svelte';
import { folders } from '$lib/stores';
import Folder from '$lib/components/icons/Folder.svelte';
const i18n = getContext('i18n');
@@ -144,14 +146,25 @@
]
: [];
items = [...collections, ...collection_files, ...legacy_collections, ...legacy_documents].map(
(item) => {
return {
...item,
...(item?.legacy || item?.meta?.legacy || item?.meta?.document ? { legacy: true } : {})
};
}
);
let folder_items = $folders.map((folder) => ({
...folder,
type: 'folder',
description: $i18n.t('Folder'),
title: folder.name
}));
items = [
...folder_items,
...collections,
...collection_files,
...legacy_collections,
...legacy_documents
].map((item) => {
return {
...item,
...(item?.legacy || item?.meta?.legacy || item?.meta?.document ? { legacy: true } : {})
};
});
fuse = new Fuse(items, {
keys: ['name', 'description']
@@ -213,6 +226,8 @@
>
{#if item?.type === 'collection'}
<Database className="size-4" />
{:else if item?.type === 'folder'}
<Folder className="size-4" />
{:else}
<DocumentPage className="size-4" />
{/if}
@@ -56,6 +56,10 @@
fileUploadCapableModels.length === selectedModels.length &&
($user?.role === 'admin' || $user?.permissions?.chat?.file_upload);
$: if (!fileUploadEnabled && files.length > 0) {
files = [];
}
const detectMobile = () => {
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
return /android|iphone|ipad|ipod|windows phone/i.test(userAgent);
@@ -199,7 +203,9 @@
className="w-full"
>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl {!fileUploadEnabled
? 'opacity-50'
: ''}"
on:click={() => {
if (fileUploadEnabled) {
showAttachWebpageModal = true;
@@ -31,7 +31,7 @@
const getItemsPage = async () => {
itemsLoading = true;
let res = await getChatList(localStorage.token, page, true).catch(() => {
let res = await getChatList(localStorage.token, page, true, true).catch(() => {
return [];
});
@@ -42,6 +42,7 @@
export let onShowValves: Function;
export let onClose: Function;
export let closeOnOutsideClick = true;
let show = false;
let tab = '';
@@ -330,7 +331,7 @@
{#each Object.keys(tools) as toolId}
<button
class="relative flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800/50"
on:click={(e) => {
on:click={async (e) => {
if (!(tools[toolId]?.authenticated ?? true)) {
e.preventDefault();
@@ -338,9 +339,18 @@
let serverId = parts?.at(-1) ?? toolId;
const authUrl = getOAuthClientAuthorizationUrl(serverId, 'mcp');
window.open(authUrl, '_blank', 'noopener');
window.open(authUrl, '_self', 'noopener');
} else {
tools[toolId].enabled = !tools[toolId].enabled;
const state = tools[toolId].enabled;
await tick();
if (state) {
selectedToolIds = [...selectedToolIds, toolId];
} else {
selectedToolIds = selectedToolIds.filter((id) => id !== toolId);
}
}
}}
>
@@ -383,18 +393,7 @@
{/if}
<div class=" shrink-0">
<Switch
state={tools[toolId].enabled}
on:change={async (e) => {
const state = e.detail;
await tick();
if (state) {
selectedToolIds = [...selectedToolIds, toolId];
} else {
selectedToolIds = selectedToolIds.filter((id) => id !== toolId);
}
}}
/>
<Switch state={tools[toolId].enabled} />
</div>
</button>
{/each}

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