Compare commits

...

329 Commits

Author SHA1 Message Date
SamiAhmed7777 c89703531c feat: Add theme customizer plugin 2025-03-10 01:20:20 -07:00
SamiAhmed7777 77269fe4b4 Moved branding files to new folder
Deploy to HuggingFace Spaces / deploy (push) Blocked by required conditions
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
Release / release (push) Failing after 1m18s
Deploy to HuggingFace Spaces / check-secret (push) Successful in 11s
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Failing after 3m51s
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Failing after 1m11s
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Failing after 1m5s
Python CI / Format Backend (3.11) (push) Failing after 1m22s
Frontend Build / Format & Build Frontend (push) Failing after 1m41s
Frontend Build / Frontend Unit Tests (push) Failing after 52s
Release to PyPI / release (push) Failing after 1m28s
2025-03-05 21:02:07 -08:00
SamiAhmed7777 89afded87e Moved branding files to OpenWebUI's new static assets location 2025-03-05 19:54:55 -08:00
Timothy Jaeryang Baek 3b70cd64d7 Merge pull request #11257 from open-webui/dev
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
0.5.20
2025-03-05 19:44:45 -08:00
Timothy Jaeryang Baek 1173459eee doc: changelog 2025-03-05 19:42:41 -08:00
Timothy Jaeryang Baek c0ecff9d70 fix: pinyin keyboard 2025-03-05 19:36:30 -08:00
Timothy Jaeryang Baek d4fca9dabf chore: format 2025-03-05 19:17:41 -08:00
Timothy Jaeryang Baek aaaebfabbe chore: format 2025-03-05 19:16:59 -08:00
Timothy Jaeryang Baek 6eccf8668e fix: non-root container issue 2025-03-05 19:14:43 -08:00
Timothy Jaeryang Baek 5e78a5c510 enh: enable_code_execution toggle
Co-Authored-By: recrudesce <6450799+recrudesce@users.noreply.github.com>
2025-03-05 19:06:28 -08:00
Timothy Jaeryang Baek dce24225e0 Merge pull request #11249 from TiancongLx/dev
i18n: update zh-TW
2025-03-05 18:52:34 -08:00
Timothy Jaeryang Baek f9b44c6bf2 refac 2025-03-05 18:39:32 -08:00
Timothy Jaeryang Baek f573a90a56 Merge pull request #11254 from open-webui/main
dev
2025-03-05 18:11:18 -08:00
Timothy Jaeryang Baek 23bb0d927f chore: format 2025-03-05 18:10:24 -08:00
Tiancong Li 6b0af942db i18n: update zh-TW 2025-03-06 07:23:55 +08:00
Timothy Jaeryang Baek f95a482c24 Merge pull request #11209 from aleixdorca/dev
Update catalan translation.json
2025-03-05 14:56:37 -08:00
Timothy Jaeryang Baek edf94d732b refac 2025-03-05 14:54:48 -08:00
Timothy Jaeryang Baek 73a77abd20 Merge pull request #11245 from tupe2009/kleqon.fix-es-bugs
fix: ElasticSearch indexes delete and upsert bugs(Post 0.5.19 release)
2025-03-05 13:36:55 -08:00
ofek a8f205213c fixed es bugs 2025-03-05 23:19:56 +02:00
Timothy Jaeryang Baek 4770285c04 Merge pull request #11211 from open-webui/dev
fix: site.webmanifest
2025-03-05 00:45:21 -08:00
Timothy Jaeryang Baek 1639fbb544 fix: site.webmanifest 2025-03-05 00:45:04 -08:00
Aleix Dorca 2dc5f3c30c Update catalan translation.json 2025-03-05 08:42:16 +01:00
Timothy Jaeryang Baek 1a51584fe0 Merge pull request #10939 from open-webui/dev
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
0.5.19
2025-03-04 22:22:20 -08:00
Timothy Jaeryang Baek 976c94ba5a chore: pyproject 2025-03-04 22:13:51 -08:00
Timothy Jaeryang Baek 4df74690e1 doc: changelog 2025-03-04 22:10:51 -08:00
Timothy Jaeryang Baek e2d60bd92e chore: format 2025-03-04 22:10:47 -08:00
Timothy Jaeryang Baek c2eada36f1 Merge pull request #10898 from hurxxxx/fix/update-msal-browser-version
fix: update msal browser version
2025-03-04 21:56:22 -08:00
Timothy Jaeryang Baek 0ce6975ec8 chore: bump 2025-03-04 21:49:42 -08:00
Timothy Jaeryang Baek 1ddc56fcf8 Merge pull request #11201 from open-webui/main
dev
2025-03-04 21:43:33 -08:00
Timothy Jaeryang Baek 7f7365db4f Merge pull request #11132 from dongs0104/dev
fix: email claim constant
2025-03-04 21:39:28 -08:00
Timothy Jaeryang Baek c9e2ba56c6 Merge pull request #11198 from xinhai-ai/refa_filters
chore: move the filter_type check earlier to avoid unnecessary valve reads.
2025-03-04 21:27:36 -08:00
Timothy Jaeryang Baek b01c9184c2 chore: format 2025-03-04 21:24:17 -08:00
星海 3ee1ae8c95 refac: filters 2025-03-05 13:05:14 +08:00
星海 8ce6c8cf78 refac: filters 2025-03-05 13:00:45 +08:00
Timothy Jaeryang Baek fbf4f5eb30 refac 2025-03-04 18:19:05 -08:00
Timothy Jaeryang Baek 6c77e6fdb3 refac 2025-03-04 18:08:43 -08:00
Timothy Jaeryang Baek 3b74431ea3 refac: filters 2025-03-04 18:04:55 -08:00
Timothy Jaeryang Baek 53fe7c460b Merge pull request #11178 from silentoplayz/fix
chore: Update ToolkitEditor.svelte
2025-03-04 17:53:47 -08:00
Timothy Jaeryang Baek f39556b933 Merge pull request #11163 from tidely/pathfix
chore: Use platform specific path separators
2025-03-04 17:53:21 -08:00
Timothy Jaeryang Baek cffd02defe Merge pull request #11153 from panda44312/patch-4
i18n: update ZH-CN
2025-03-04 17:42:05 -08:00
Timothy Jaeryang Baek bdc6088159 Merge pull request #11151 from TiancongLx/dev
i18n: update zh-TW
2025-03-04 17:41:57 -08:00
Timothy Jaeryang Baek 2a75a85556 refac 2025-03-04 17:40:32 -08:00
Silentoplayz 94c025c6d7 Update ToolkitEditor.svelte
fix
2025-03-04 17:46:24 -05:00
Timothy Jaeryang Baek 8961bc2453 Merge pull request #11173 from TFWol/patch-2
chore: Revamp Feature Request template
2025-03-04 13:49:07 -08:00
TFWol cb82c7062a Revamp Feature Request template 2025-03-04 11:18:50 -08:00
tidely b15814c42f chore: remove unnecessary Path conversions
Remove unnecessary `pathlib.Path` conversions. (CACHE_DIR and DATA_DIR)

Use `/` Path joining shorthand to ensure using platform specific Path separators (Windows: \\, Unix: /)
2025-03-04 19:53:52 +02:00
Panda 023c8bb5b4 i18n: zh-cn Update 2025-03-04 16:08:06 +01:00
Tiancong Li 500440ebec i18n: update zh-TW 2025-03-04 22:51:24 +08:00
Timothy Jaeryang Baek b94de4f002 refac 2025-03-04 02:45:05 -08:00
Timothy Jaeryang Baek 039a1e1f14 refac: assets 2025-03-04 01:47:17 -08:00
Timothy Jaeryang Baek 14a6c1f496 refac: favicon 2025-03-04 01:30:22 -08:00
Timothy Jaeryang Baek 0a295ff56d refac: icons 2025-03-04 01:25:47 -08:00
Timothy Jaeryang Baek 36ffa9824b refac 2025-03-04 01:16:25 -08:00
Timothy Jaeryang Baek e3769c1073 refac 2025-03-04 01:16:08 -08:00
Timothy Jaeryang Baek d78fe4decc Merge pull request #11073 from vincent-thevenin/dev
fix: fix issue with call voice recording starting before voice detection
2025-03-04 00:55:03 -08:00
Timothy Jaeryang Baek 7b442e4be0 Merge pull request #11141 from Youggls/dev
fix: correct parameter name for MilvusClient instantiation
2025-03-04 00:54:49 -08:00
Timothy Jaeryang Baek d17ac95f95 Merge pull request #11143 from panda44312/patch-3
i18n: zh-cn
2025-03-04 00:53:31 -08:00
Panda 59c3203990 IUpdate translation.json 2025-03-04 09:48:03 +01:00
Timothy Jaeryang Baek a4747c88e0 refac 2025-03-04 00:33:19 -08:00
Timothy Jaeryang Baek 39ea59edc8 chore: format 2025-03-04 00:32:27 -08:00
Perry Li 67ed61d022 fixbug: correct parameter name for MilvusClient instantiation
Replace incorrect parameter 'database=MILVUS_DB' with valid 'db_name=MILVUS_DB'
2025-03-04 16:02:19 +08:00
Dong Shin b8f3abda5a fix: email claim constant 2025-03-04 15:50:04 +09:00
Timothy Jaeryang Baek 8697f72068 refac: styling 2025-03-03 22:44:03 -08:00
Timothy Jaeryang Baek 3e7077bb73 refac 2025-03-03 22:24:19 -08:00
Timothy Jaeryang Baek 5f89c1b137 enh: model selector tag filter 2025-03-03 21:47:27 -08:00
Timothy Jaeryang Baek 811e1f92b1 refac 2025-03-03 21:27:48 -08:00
Timothy Jaeryang Baek 15cbccc8f5 enh: code block collapse 2025-03-03 21:10:15 -08:00
Timothy Jaeryang Baek 1cfa491179 Merge pull request #10868 from tidely/loggingnamemap
chore: use logging.getLevelNamesMapping() for validating log level
2025-03-03 20:55:22 -08:00
Timothy Jaeryang Baek 35e7e07821 refac: cuda error gc 2025-03-03 20:55:07 -08:00
Timothy Jaeryang Baek 9ff54288ef refac 2025-03-03 20:53:14 -08:00
Timothy Jaeryang Baek baca6db680 Merge pull request #10987 from alpha-pet/feat-submit-on-ctrl-enter
feat: add alternative input configuration where Ctrl-Enter is required to send chat messages
2025-03-03 20:39:53 -08:00
Timothy Jaeryang Baek 6bd2bc5683 refac 2025-03-03 20:34:45 -08:00
Timothy Jaeryang Baek 029625ec4e refac 2025-03-03 20:34:17 -08:00
Timothy Jaeryang Baek 7e65f7fa4d fix: chat event handler not being registered on sign in 2025-03-03 20:31:35 -08:00
Timothy Jaeryang Baek 80f5573cf3 refac: AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST 2025-03-03 20:24:10 -08:00
Timothy Jaeryang Baek 1236157800 fix: timestamp missing in edited user message 2025-03-03 20:21:39 -08:00
Timothy Jaeryang Baek e628bfe6ff fix 2025-03-03 20:16:40 -08:00
Timothy Jaeryang Baek bb2bd7d721 Merge pull request #11120 from OrenZhang/fix_jupyter
fix(jupyter): fix kernel_id not set and optimize code
2025-03-03 20:15:09 -08:00
Timothy Jaeryang Baek 9c107f9d2d refac: images "url" fallback 2025-03-03 20:07:59 -08:00
orenzhang fafd847f18 refactor(jupyter): change log message for execution failed 2025-03-04 12:01:08 +08:00
orenzhang 3a7e5a5f31 feat(jupyter): check response status for destroy kernel 2025-03-04 12:00:04 +08:00
orenzhang 613d278afc refactor(jupyter): change log level 2025-03-04 11:58:32 +08:00
orenzhang 96e0c4d7b8 refactor(jupyter): replace httpx with aiohttp 2025-03-04 11:56:11 +08:00
Timothy Jaeryang Baek 46cea1f904 fix: ldap email case sensitive 2025-03-03 19:52:27 -08:00
Timothy Jaeryang Baek d844fc7edb refac: inline citation handling 2025-03-03 19:48:00 -08:00
Timothy Jaeryang Baek 3468dda556 refac: rm unused api endpoint 2025-03-03 19:33:55 -08:00
Timothy Jaeryang Baek e6ff4169e1 Merge pull request #11066 from NibbinNone/dev
feat: add support for reasoning_content
2025-03-03 16:37:28 -08:00
Timothy Jaeryang Baek 6f8c1a8f0d Merge pull request #11089 from tupe2009/kleqon.feat-add-elasticsearch-support
feat: Elasticsearch as a vector store support
2025-03-03 16:34:39 -08:00
Timothy Jaeryang Baek d529be86fe Merge pull request #11109 from panda44312/patch-2
i18n: Update zh-cn
2025-03-03 16:34:21 -08:00
Panda 3ad4b10470 i18n: Update zh-cn 2025-03-04 00:17:02 +01:00
Timothy Jaeryang Baek 376fce273e Merge pull request #11079 from MeteorSkyOne/dev
fix: allow error returns None
2025-03-03 14:57:54 -08:00
Timothy Jaeryang Baek c19960002b Merge pull request #11086 from aindriu80/main
(i18n)Updated Irish translations
2025-03-03 14:57:01 -08:00
Timothy Jaeryang Baek dc2b8fd327 Merge branch 'dev' into main 2025-03-03 14:56:48 -08:00
ofek 737dfd2763 added elasticsearch support 2025-03-03 23:39:42 +02:00
Aindriú Mac Giolla Eoin 3fa323d1a1 Updated Irish translations 2025-03-03 18:41:11 +00:00
MeteorSky a01ef6cfa0 fix: allow error returns None 2025-03-04 00:08:49 +08:00
Timothy Jaeryang Baek c7d4d2e41f Merge pull request #11067 from TiancongLx/dev
i18n: update zh-TW (fix typo)
2025-03-03 06:49:27 -08:00
orenzhang 744ffbb1fb fix(jupyter): fix kernel_id not set and optimize code 2025-03-03 22:18:02 +08:00
vincent-thevenin 4d6132b9de fix: fix issue with call voice recording starting before voice detection 2025-03-03 13:59:28 +01:00
Tiancong Li 7a31ff6a2f i18n: update zh-TW (fix typo) 2025-03-03 16:55:37 +08:00
CorbinChen 516f1cf67b feat: add support for reasoning_content 2025-03-03 16:47:35 +08:00
Timothy Jaeryang Baek 3d6e48b05e Merge pull request #11043 from TiancongLx/dev
i18n: update zh-TW
2025-03-02 12:16:56 -08:00
Tiancong Li 3dd492627e i18n: update zh-TW 2025-03-02 17:04:58 +08:00
Timothy Jaeryang Baek 66210b95fc Merge pull request #11038 from satlus/pwa-fix-linkrel
fixed manifest in app.html restoring PWA
2025-03-01 19:55:08 -08:00
satlus 6e58d99920 fixed manifest link rel, restoring PWA 2025-03-01 19:43:40 -05:00
Timothy Jaeryang Baek 6471f12668 Merge pull request #11033 from dtaivpp/main
fix: Changed to use collection_name and fixed bulk indexing missing index.
2025-03-01 16:00:13 -08:00
David Tippett f3c4c2b8e3 Changed to use colleciton name and fixed bulk indexing missing index. 2025-03-01 13:26:19 -05:00
Timothy Jaeryang Baek a73b6448c5 Merge pull request #11032 from cheahjs/chore/build-arm-on-arm
ci: Update GitHub Actions runner selection for multi-platform builds
2025-03-01 08:23:54 -08:00
Jun Siang Cheah 3238981202 ci: Update GitHub Actions runner selection for multi-platform builds 2025-03-01 16:04:26 +00:00
Timothy Jaeryang Baek 59ddce1ea3 chore: bump 2025-03-01 08:00:40 -08:00
Timothy Jaeryang Baek 2b025b267a chore: bump 2025-03-01 07:57:08 -08:00
Timothy Jaeryang Baek efe8c4ca69 chore: format 2025-03-01 07:28:00 -08:00
Timothy Jaeryang Baek d7088efe73 fix: "stream" hook not working 2025-03-01 06:56:24 -08:00
Timothy Jaeryang Baek 05c5e73304 Merge pull request #10373 from dannyl1u/logit_bias
feat: logit bias
2025-03-01 06:13:19 -08:00
SamiAhmed7777 08a5ff700e Updated graphics 2025-03-01 01:28:55 -08:00
SamiAhmed7777 edf57900dc Updated graphics 2025-03-01 00:22:21 -08:00
Timothy Jaeryang Baek f00f518abe Merge pull request #11011 from open-webui/dependabot/pip/backend/dev/sqlalchemy-2.0.38
chore(deps): bump sqlalchemy from 2.0.32 to 2.0.38 in /backend
2025-02-28 18:44:20 -08:00
Timothy Jaeryang Baek 639f4a44e0 Merge pull request #11013 from open-webui/dependabot/pip/backend/dev/black-25.1.0
chore(deps): bump black from 24.8.0 to 25.1.0 in /backend
2025-02-28 18:44:14 -08:00
Timothy Jaeryang Baek 92e1c654ce Merge pull request #11014 from open-webui/dependabot/pip/backend/dev/peewee-3.17.9
chore(deps): bump peewee from 3.17.8 to 3.17.9 in /backend
2025-02-28 18:44:07 -08:00
Timothy Jaeryang Baek 81d2df4df9 Merge pull request #11015 from open-webui/dependabot/pip/backend/dev/bcrypt-4.3.0
chore(deps): bump bcrypt from 4.2.0 to 4.3.0 in /backend
2025-02-28 18:43:57 -08:00
Timothy Jaeryang Baek 65949dbcbc Merge pull request #11012 from open-webui/dependabot/pip/backend/dev/uvicorn-standard--0.34.0
chore(deps): bump uvicorn[standard] from 0.30.6 to 0.34.0 in /backend
2025-02-28 18:43:47 -08:00
dependabot[bot] eb97e30b0a chore(deps): bump bcrypt from 4.2.0 to 4.3.0 in /backend
Bumps [bcrypt](https://github.com/pyca/bcrypt) from 4.2.0 to 4.3.0.
- [Changelog](https://github.com/pyca/bcrypt/blob/main/release.py)
- [Commits](https://github.com/pyca/bcrypt/compare/4.2.0...4.3.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-01 02:37:41 +00:00
dependabot[bot] 84518490c5 chore(deps): bump peewee from 3.17.8 to 3.17.9 in /backend
Bumps [peewee](https://github.com/coleifer/peewee) from 3.17.8 to 3.17.9.
- [Release notes](https://github.com/coleifer/peewee/releases)
- [Changelog](https://github.com/coleifer/peewee/blob/master/CHANGELOG.md)
- [Commits](https://github.com/coleifer/peewee/compare/3.17.8...3.17.9)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-01 02:37:36 +00:00
dependabot[bot] dd73ecbdc9 chore(deps): bump black from 24.8.0 to 25.1.0 in /backend
Bumps [black](https://github.com/psf/black) from 24.8.0 to 25.1.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/24.8.0...25.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-01 02:37:32 +00:00
dependabot[bot] 588a179b7f chore(deps): bump uvicorn[standard] from 0.30.6 to 0.34.0 in /backend
Bumps [uvicorn[standard]](https://github.com/encode/uvicorn) from 0.30.6 to 0.34.0.
- [Release notes](https://github.com/encode/uvicorn/releases)
- [Changelog](https://github.com/encode/uvicorn/blob/master/docs/release-notes.md)
- [Commits](https://github.com/encode/uvicorn/compare/0.30.6...0.34.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-01 02:37:27 +00:00
dependabot[bot] 244915cd1e chore(deps): bump sqlalchemy from 2.0.32 to 2.0.38 in /backend
Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 2.0.32 to 2.0.38.
- [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases)
- [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/main/CHANGES.rst)
- [Commits](https://github.com/sqlalchemy/sqlalchemy/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-01 02:37:24 +00:00
Timothy Jaeryang Baek 094d777e55 Merge pull request #11009 from amuwall/fix-typing-error
fix: typing error in replace_messages_variable func args
2025-02-28 18:34:24 -08:00
amuwall b430deab15 fix: fix typing error in args 2025-03-01 10:20:29 +08:00
Timothy Jaeryang Baek fe44e4d344 i18n(html): sync html language with i18n language 2025-02-28 16:06:28 -08:00
Timothy Jaeryang Baek 88c02d5a14 refac 2025-02-28 16:02:15 -08:00
Timothy Jaeryang Baek b75b891de1 Merge pull request #10983 from NovoNordisk-OpenSource/yvedeng/fix-redis-timeout-config
fix: add redis lock timeout config
2025-02-28 16:01:59 -08:00
Timothy Jaeryang Baek f1ce653830 Merge pull request #10992 from softue/i18n-floating-buttons
i18n floating buttons (en-US and pt-BR)
2025-02-28 15:59:49 -08:00
Rômulo Mendes Figueiredo c06484297b i18n: ask and explain floating buttons 2025-02-28 12:39:57 -03:00
Timothy Jaeryang Baek 331caad5b8 fix 2025-02-28 07:36:56 -08:00
Thomas Rehn 8b3fcfea13 feat: add alternative input configuration where Ctrl-Enter is required to send chat messages 2025-02-28 15:23:03 +01:00
Timothy Jaeryang Baek 05599e1bb5 Merge pull request #10967 from OrenZhang/i18n_zh_cn
i18n(zh-cn): add chinese translation
2025-02-28 03:21:51 -08:00
orenzhang bcdbc4fb8a i18n(zh-cn): add chinese translation 2025-02-28 17:47:27 +08:00
Yifang Deng 41a2b86e70 fix: add redis lock timeout config 2025-02-28 10:12:51 +01:00
Danny Liu f9292ab4d2 Merge branch 'dev' into logit_bias 2025-02-27 23:51:13 -08:00
dannyl1u f4bd094128 Merge remote-tracking branch 'origin' into logit_bias 2025-02-27 23:48:22 -08:00
dannyl1u 90aa29528c logit_bias: handle comma seperated values 2025-02-27 23:13:30 -08:00
dannyl1u 34e3cb6881 logit bias: update tooltip message 2025-02-27 23:13:09 -08:00
Timothy Jaeryang Baek d0ddb0637e enh: web embed bypass embedding and retrieval support 2025-02-27 16:34:05 -08:00
hurxxxx 2405809f92 Merge branch 'main' into fix/update-msal-browser-version 2025-02-28 08:44:06 +09:00
hurxxxx 77579f5509 chore: cleanup 2025-02-28 08:42:02 +09:00
Timothy Jaeryang Baek 5e33955566 Merge pull request #10940 from TheMasterFX/update-de-DE-translation
i18n: Update de-DE translation
2025-02-27 15:17:59 -08:00
Timothy Jaeryang Baek 73c0262345 Merge branch 'dev' into update-de-DE-translation 2025-02-27 15:17:51 -08:00
Timothy Jaeryang Baek a3ea924f0a Merge pull request #10941 from TiancongLx/dev
i18n: update zh-TW
2025-02-27 15:17:03 -08:00
Tiancong Li 01435997b9 i18n: update zh-TW 2025-02-28 06:29:53 +08:00
Timmo 90fe437e29 i18n: Update de-DE translation - fix typos 2025-02-27 23:23:05 +01:00
Timmo 43077b82b7 i18n: Update de-DE translation 2025-02-27 23:09:02 +01:00
Timothy Jaeryang Baek 1b56a8f3cb Merge pull request #10864 from kurtdami/perplexity_integration
feat: add perplexity integration to web search
2025-02-27 13:51:03 -08:00
Timothy Jaeryang Baek 1d27402dd0 Merge pull request #10910 from saraangelmurphy/awsworkloadidentity
feat: add AWS workload identity support
2025-02-27 13:50:36 -08:00
Timothy Jaeryang Baek 6f7161acf8 chore: format 2025-02-27 13:04:39 -08:00
Timothy Jaeryang Baek 54e3ca8e6c refac: rm confusing defaults 2025-02-27 13:03:28 -08:00
Timothy Jaeryang Baek 95cadaca72 Merge pull request #10933 from open-webui/dev
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
0.5.18
2025-02-27 12:00:52 -08:00
Timothy Jaeryang Baek e26c443815 0.5.18 2025-02-27 12:00:23 -08:00
Timothy Jaeryang Baek 046f1fb30a Merge pull request #10927 from open-webui/dev
0.5.18
2025-02-27 11:50:08 -08:00
Timothy Jaeryang Baek 63d825692e doc: changelog 2025-02-27 11:49:59 -08:00
Timothy Jaeryang Baek 70667d5cc8 refac 2025-02-27 11:45:25 -08:00
Timothy Jaeryang Baek 2ac0eae99e Merge pull request #10925 from SleepyYui/fix-10905
fix: Fix OpenAI connection not actually deleting
2025-02-27 11:44:20 -08:00
Timothy Jaeryang Baek 1dd442f38b Merge pull request #10919 from df-cgdm/main
**fix** Resolve a bug in /api/models/base which was not returning openai models when ENABLE_FORWARD_USER_INFO_HEADERS is true
2025-02-27 11:42:51 -08:00
Yui 94c19f73b0 add config update when deleting openai model 2025-02-27 20:41:30 +01:00
Timothy Jaeryang Baek ed0b9be4e3 fix 2025-02-27 11:39:59 -08:00
Timothy Jaeryang Baek 866d02bbae refac 2025-02-27 11:39:00 -08:00
Timothy Jaeryang Baek f7c5cd486b Merge pull request #10908 from barnabehvrd/main
i18n: Updated translations for fr-FR
2025-02-27 11:37:58 -08:00
Timothy Jaeryang Baek 660a44c918 fix: insecure connection over LAN 2025-02-27 11:37:44 -08:00
Didier FOURNOUT b6873153c1 resolve a bug in /api/models/base not returning openai models when ENABLE_FORWARD_USER_INFO_HEADERS is true 2025-02-27 19:32:15 +00:00
df-cgdm 24a2e84692 Merge branch 'open-webui:main' into main 2025-02-27 19:13:00 +00:00
Sara Angel-Murphy 5e873bc643 feat: add AWS workload identity support 2025-02-27 13:12:54 -05:00
v0ltis f643182eb0 Updated french translation 2025-02-27 19:04:53 +01:00
hurxxxx c5cfcc1229 chore: cleanup 2025-02-27 23:16:52 +09:00
hurxxxx 203dfdedf1 Update deprecated msal-browser version to the latest 2025-02-27 23:12:09 +09:00
hurxxxx febc9ab885 Merge remote-tracking branch 'upstream/main' 2025-02-27 21:08:20 +09:00
Timothy Jaeryang Baek 38fb9d5fd8 chore: jspdf depedency 2025-02-27 03:54:16 -08:00
Timothy Jaeryang Baek 15485e7c5d Merge pull request #10469 from open-webui/dev
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
0.5.17
2025-02-27 03:46:08 -08:00
Timothy Jaeryang Baek 8241fa2e12 doc: changelog 2025-02-27 03:45:33 -08:00
Timothy Jaeryang Baek 37a7dd6d33 chore: format 2025-02-27 02:18:44 -08:00
Timothy Jaeryang Baek 85b7ec9387 Merge pull request #10873 from TiancongLx/dev
i18n: update zh-TW
2025-02-27 02:17:33 -08:00
Tiancong Li b838a1df99 i18n: update zh-TW 2025-02-27 17:57:10 +08:00
tidely d7b18b662b chore: use logging.getLevelNamesMapping()
Use `logging.getLevelNamesMapping()` for getting all valid logging levels. This also allows adding new ones using `logging.addLevel()`. This feature was [added](https://docs.python.org/3.11/library/logging.html#logging.getLevelNamesMapping) in Python 3.11 which is above the minimum supported Python version for open-webui.
2025-02-27 11:19:27 +02:00
kurtdami b061775932 feat: add perplexity integration to web search 2025-02-27 00:30:48 -08:00
Timothy Jaeryang Baek 8c4d967ef4 refac: evaluations settings ui 2025-02-26 23:58:43 -08:00
Timothy Jaeryang Baek ce7cf62a55 refac: dedup 2025-02-26 23:51:39 -08:00
Timothy Jaeryang Baek 0a4dbf7cf0 refac 2025-02-26 23:41:09 -08:00
Timothy Jaeryang Baek fcbdfbd744 refac 2025-02-26 23:35:09 -08:00
Timothy Jaeryang Baek 1ae702b8a6 Merge pull request #10860 from open-webui/audit-log-dev
feat: add audit logging feature
2025-02-26 23:19:37 -08:00
Timothy Jaeryang Baek 2d379cb35f chore: pyproject 2025-02-26 23:19:19 -08:00
Timothy Jaeryang Baek 76e90d9f3f Merge pull request #10436 from victorstevansuse/feat/audits
feat: add audit logging feature
2025-02-26 23:15:10 -08:00
Timothy Jaeryang Baek da8c42ee49 feat: Sort models by name in ascending/descending order
Co-Authored-By: silentoplayz <50341825+silentoplayz@users.noreply.github.com>
2025-02-26 23:12:46 -08:00
Timothy Jaeryang Baek ddb30589e3 chore: format
HIDE MODELS
2025-02-26 22:18:18 -08:00
Timothy Jaeryang Baek ef734833a5 fix 2025-02-26 21:22:51 -08:00
Timothy Jaeryang Baek 40ddd172e0 refac: web search settings ui 2025-02-26 15:56:45 -08:00
Timothy Jaeryang Baek 57010901e6 enh: bypass embedding and retrieval 2025-02-26 15:42:19 -08:00
Timothy Jaeryang Baek 1c2e36f1b7 refac 2025-02-26 13:59:08 -08:00
Timothy Jaeryang Baek 34aeaaf020 refac 2025-02-26 13:54:26 -08:00
Timothy Jaeryang Baek fa91d83ac3 refac: documents settings ui 2025-02-26 13:48:56 -08:00
Timothy Jaeryang Baek 78a8ef8e66 refac: audio file handling 2025-02-26 13:09:52 -08:00
Timothy Jaeryang Baek 46ac6f2b29 fix 2025-02-26 12:53:07 -08:00
Timothy Jaeryang Baek 33d3558ca9 Merge pull request #10817 from NovoNordisk-OpenSource/ivaroli/adding-json-as-supported-file-type
fix: Using the TextLoader instead of Tika for JSON files
2025-02-26 12:49:29 -08:00
Timothy Jaeryang Baek c1c92b0c03 Merge pull request #10820 from oyve/dev
i18n: Update translation.json for nb-NO
2025-02-26 12:49:02 -08:00
Øyvind 618307f35b Update translation.json
Fill all empty translation
2025-02-26 16:29:47 +01:00
Ívar Óli Sigurðsson c5a09cdd21 adding a comma 2025-02-26 15:27:03 +01:00
Ívar Óli Sigurðsson 661711164a Adding json as a known source for Tika 2025-02-26 15:11:21 +01:00
Timothy Jaeryang Baek 80efaaffdd Merge pull request #10810 from panda44312/patch-1
i18n: Update zh-cn translation
2025-02-26 04:29:41 -08:00
Panda 46f2dc0d11 Update zh-cn 2025-02-26 12:24:52 +01:00
Timothy Jaeryang Baek 674d6e08fc Merge pull request #10809 from TobiasGoerke/feat/update_timestamp_asynchronously
feat: update get_current_user to refresh last active timestamp asynchronously
2025-02-26 02:38:06 -08:00
Tobias Goerke 76891f4760 feat: update get_current_user to refresh last active timestamp asynchronously 2025-02-26 10:53:47 +01:00
Timothy Jaeryang Baek 20f0b02b38 Merge pull request #10755 from softue/check-if-enable-ldap-chroma
Conditional import of ldap3 and chromadb
2025-02-25 16:02:40 -08:00
Timothy Jaeryang Baek bef7a532cc refac 2025-02-25 16:01:29 -08:00
Timothy Jaeryang Baek ced508c912 Merge pull request #10783 from silentoplayz/share-fix
fix: community sharing for prompts and tools
2025-02-25 15:35:11 -08:00
Timothy Jaeryang Baek c883368842 Merge pull request #10786 from silentoplayz/confirm-clear-for-memories
feat: Confirmation prompt on 'Clear' for memories
2025-02-25 15:34:14 -08:00
Silentoplayz 8097b6a3ff Update ManageModal.svelte
no confirmation when no memories exist
2025-02-25 18:00:59 -05:00
Silentoplayz 7cfbf6489a Update ManageModal.svelte
feat: Add ConfirmDialog to 'Clear Memory' button
2025-02-25 17:53:13 -05:00
Silentoplayz 11967a780d Share fix
Fix
2025-02-25 17:05:05 -05:00
Timothy Jaeryang Baek 6cf0ceedba refac 2025-02-25 11:11:36 -08:00
Timothy Jaeryang Baek ec72dcba75 Merge pull request #10742 from hjer/improve-swedish-translation
i18n: Add and improve Swedish translation
2025-02-25 10:54:19 -08:00
Timothy Jaeryang Baek 54d7bf8f28 Merge branch 'dev' into improve-swedish-translation 2025-02-25 10:54:08 -08:00
Timothy Jaeryang Baek 3be5e3129b Merge pull request #10752 from NovoNordisk-OpenSource/yvedeng/standardize-logging
refactor: replace print statements with logging
2025-02-25 10:53:02 -08:00
Rômulo Mendes Figueiredo 2298bfbd7e refac: conditional import of ldap3 2025-02-25 13:07:53 -03:00
Yifang Deng 0e5d5ecb81 refactor: replace print statements with logging for better error tracking 2025-02-25 15:53:55 +01:00
Rômulo Mendes Figueiredo b52a7e3543 refac: conditional import of chromadb 2025-02-25 11:35:36 -03:00
hjer df1238229c add and improve swedish translation 2025-02-25 13:58:09 +01:00
Timothy Jaeryang Baek eafefcf838 chore: bump 2025-02-25 01:46:57 -08:00
Timothy Jaeryang Baek 9f27d7710b chore: format 2025-02-25 01:46:08 -08:00
Timothy Jaeryang Baek 7c68a424f8 refac 2025-02-25 01:44:39 -08:00
Timothy Jaeryang Baek 5d3249848d refac: allow image only message via enter 2025-02-25 01:40:17 -08:00
Timothy Jaeryang Baek 7e2b2ec761 fix: openai usage display issue 2025-02-25 01:35:59 -08:00
Timothy Jaeryang Baek b964b175d4 refac 2025-02-25 01:03:15 -08:00
Timothy Jaeryang Baek 46c4da4864 enh: "stream" hook 2025-02-25 01:00:29 -08:00
df-cgdm 126045318f Merge branch 'open-webui:main' into main 2025-02-25 07:37:57 +00:00
Timothy Jaeryang Baek 205013da2f Merge pull request #10683 from grandtele/fix/openai-o1-legacy-roles
fix: restore compatibility for older o1 models (o1-mini, o1-preview)
2025-02-24 23:35:18 -08:00
Timothy Jaeryang Baek abf36c0645 Merge pull request #10711 from jasonpnnl/send-usage-to-pipeline-outlet
fix: Include usage in chatCompleted handler for pipeline outlet functions
2025-02-24 23:34:18 -08:00
Timothy Jaeryang Baek e06111a362 refac 2025-02-24 23:21:03 -08:00
Timothy Jaeryang Baek cd3904046d refac 2025-02-24 20:19:32 -08:00
Jason E. Jensen 89aaf64209 add optional usage to chatCompleted messages 2025-02-24 22:32:08 +00:00
Timothy Jaeryang Baek 95ae2aeef3 Merge pull request #10704 from TiancongLx/dev
i18n: update zh-TW
2025-02-24 12:11:27 -08:00
Tiancong Li cefe274f74 i18n: update zh-TW 2025-02-25 04:03:16 +08:00
Timothy Jaeryang Baek d8bc3098db Merge pull request #9918 from df-cgdm/main
feat: Add  X-OpenWebUI when forwarding to ollama servers
2025-02-24 11:55:04 -08:00
Timothy Jaeryang Baek 628b89009c Merge pull request #10690 from hurxxxx/feat/onedrive-file-attach
feat: OneDrive file picker integration
2025-02-24 11:52:37 -08:00
Timothy Jaeryang Baek 23ff2988bf Merge branch 'dev' into feat/onedrive-file-attach 2025-02-24 11:51:59 -08:00
Timothy Jaeryang Baek e0c199255f Merge pull request #10666 from EkaterinePapava/main
i18n: Update Georgian translation
2025-02-24 11:37:18 -08:00
hurxxxx b2422b3c8f i18n: onedrive related 2025-02-25 01:56:33 +09:00
hurxxxx 4cc3102758 feat: onedrive file picker integration 2025-02-25 01:47:07 +09:00
grand 8c020488dd * fix: restore compatibility for older o1 models (o1-mini, o1-preview) 2025-02-24 12:04:06 +01:00
Gunwoo Hur 0335d479f9 feat: add onedrive file picker 2025-02-24 17:27:37 +09:00
Ekaterine Papava dcaab9d6b5 Update Georgian translation 2025-02-24 09:07:45 +01:00
Timothy Jaeryang Baek 0ba45ad82b Merge pull request #10617 from grandtele/fix/openai-o1-o3-role
fix: replace "system" with "developer" for o1 and o3 models
2025-02-23 22:33:59 -08:00
Timothy Jaeryang Baek 802f8809dd fix: codeblock 2025-02-23 21:39:34 -08:00
Timothy Jaeryang Baek 7a63947b94 fix: tailwind v4 ios 16.4 compatibility issue 2025-02-23 18:41:33 -08:00
Timothy Jaeryang Baek 737b1723c7 refac 2025-02-23 14:12:41 -08:00
Timothy Jaeryang Baek e90e254575 Merge pull request #10605 from silentoplayz/shortcuts-modal-update
feat: Add more keyboard shortcuts
2025-02-23 13:48:55 -08:00
Timothy Jaeryang Baek f8ad74fc42 refac 2025-02-23 13:32:19 -08:00
Timothy Jaeryang Baek 6c862b8fbf Merge pull request #10619 from OrenZhang/feat_s3_virtual_path
feat(s3): support for s3 virtual path style
2025-02-23 13:27:22 -08:00
Timothy Jaeryang Baek 4b908ea626 Merge pull request #10607 from TiancongLx/dev
i18n: update zh-TW
2025-02-23 13:24:04 -08:00
Timothy Jaeryang Baek b77937dfad Merge pull request #10634 from sebdanielsson/handle-no-location-access
fix: Handle no location permission
2025-02-23 13:23:47 -08:00
Sebastian b00e14eb3f Handle no location permission 2025-02-23 20:27:55 +01:00
orenzhang 9748a030f7 Merge remote-tracking branch 'oui/dev' into feat_s3_virtual_path 2025-02-23 20:54:04 +08:00
orenzhang 1060d62add refactor(s3): code lint 2025-02-23 20:52:22 +08:00
orenzhang 4c3dd84b32 feat(s3): support for s3 virtual path style 2025-02-23 20:31:08 +08:00
grand 82657acdce * fix: adjust role mapping for o1 and o3 models in openai_o1_o3_handler 2025-02-23 13:20:45 +01:00
Tiancong Li c96cc831d5 i18n: update zh-TW 2025-02-23 14:29:51 +08:00
Silentoplayz e41a5278f7 Update ShortcutsModal.svelte
fix
2025-02-22 23:00:06 -05:00
Silentoplayz b5aa562039 Update ShortcutsModal.svelte
Added more shortcuts
2025-02-22 22:50:37 -05:00
Silentoplayz 5281aeb07b Merge branch 'dev' of https://github.com/silentoplayz/open-webui into dev 2025-02-22 21:17:56 -05:00
Timothy Jaeryang Baek 39717812ee Merge pull request #10572 from Kylapaallikko/dev
i18n: Update Finnish Translation
2025-02-22 14:31:24 -08:00
Timothy Jaeryang Baek 9f6e182400 Merge pull request #10573 from dom133/dev
i18n: Update Polish Translation
2025-02-22 14:30:27 -08:00
Timothy Jaeryang Baek e4ba8fea8a Merge pull request #10583 from hurxxxx/feat/show_at_model_suggestions_to_placeholder
feat: Show atSelectedModel suggestions in Placeholder
2025-02-22 14:29:52 -08:00
hurxxxx f80aea60c7 fix: Fix incorrect object property usage 2025-02-23 00:19:51 +09:00
hurxxxx a407558375 feat: Show suggestions when selecting the atModel. 2025-02-22 23:27:13 +09:00
dom133 65aa41ab81 i18n: Update Polish Translation 2025-02-22 13:10:30 +01:00
Kylapaallikko 6331963e66 Update translation.json 2025-02-22 13:34:35 +02:00
Timothy Jaeryang Baek 32fa8d5143 Merge pull request #10566 from oloflarsson/feat/async-pipes
feat: add optional support for async pipes
2025-02-22 02:47:08 -08:00
Timothy Jaeryang Baek 8f3fcdcf5c refac 2025-02-22 02:00:06 -08:00
Timothy Jaeryang Baek 794919e91d refac 2025-02-22 01:22:17 -08:00
Timothy Jaeryang Baek 667d26ca12 refac 2025-02-22 01:16:58 -08:00
Timothy Jaeryang Baek 50dec12072 refac 2025-02-21 22:15:22 -08:00
Timothy Jaeryang Baek 8fb6b03ddc Merge pull request #10555 from pwnless/dev
fix: ollama backend native tool calling
2025-02-21 21:09:51 -08:00
hopeless 7bfa29fa81 Update payload.py
Fixes ollama native tool calling because native tool calling content will be str '', and tool call processing will be completely ignored.
2025-02-22 12:13:14 +08:00
Timothy Jaeryang Baek 9bada6421e refac: code block image styling 2025-02-21 16:39:56 -08:00
Timothy Jaeryang Baek b14e75dd6c feat: added Trust Proxy Environment switch in Web Search admin settings tab.
Co-Authored-By: harry zhou <67385896+harryzhou2000@users.noreply.github.com>
2025-02-21 13:40:11 -08:00
Timothy Jaeryang Baek 6623583004 Merge pull request #10549 from jeannotdamoiseaux/fix/oauth-groups-default-typeerror
Fix: Ensure `user_oauth_groups` defaults to an empty list to prevent TypeError
2025-02-21 13:37:35 -08:00
Jeannot Damoiseaux d50098b622 Fix: Ensure user_oauth_groups defaults to an empty list to prevent TypeError
When the OAuth groups claim does not yield a list, `user_oauth_groups` was previously
set to None, causing a TypeError during membership checks. Changed this default to
an empty list (`[]`) to ensure the variable is always iterable, preventing errors
for non-admin users while logging in.

This fix ensures stability in the `update_user_groups` function.
2025-02-21 22:25:22 +01:00
Timothy Jaeryang Baek 642dcd4b70 fix: model import 2025-02-21 13:19:11 -08:00
Timothy Jaeryang Baek a7d8ed0c6d refac 2025-02-21 12:11:21 -08:00
Timothy Jaeryang Baek ab1b910d80 Merge pull request #10486 from Micca/feature/document_intelligence_support
Feat: Adding Support for Azure AI Document Intelligence for Content Extraction (Revised)
2025-02-21 10:56:18 -08:00
Timothy Jaeryang Baek a3c2ff61ed Merge pull request #10483 from KarlLee830/translate
i18n: Update Chinese Translation
2025-02-21 10:55:55 -08:00
Timothy Jaeryang Baek b111434fa9 Merge pull request #10488 from glimpse-0201/dev
fix:Quick selection tool lost
2025-02-21 10:55:44 -08:00
Timothy Jaeryang Baek 613a087387 refac 2025-02-21 10:55:03 -08:00
Timothy Jaeryang Baek b0a19a9801 Merge pull request #10501 from Synergyst/feature/openai-tts-custom-url-fix-dev
fix: Custom OpenAI-TTS URL to fetch actual voices and models
2025-02-21 10:52:41 -08:00
Timothy Jaeryang Baek 086a78f863 Merge pull request #10521 from Seniorsimo/add-stop-parameter
**fix** Added support for stop parameter in OpenAI to Ollama conversion
2025-02-21 10:51:49 -08:00
Timothy Jaeryang Baek b4c64a21b3 Merge pull request #10513 from mcelrath/dev
Support thinking/solution tags used by Openthinker
2025-02-21 10:49:45 -08:00
Simone fb3886cf04 Added support for stop parameter 2025-02-21 18:46:10 +01:00
Bob McElrath c9079d4b4d Merge branch 'open-webui:dev' into dev 2025-02-21 09:13:53 -05:00
Bob McElrath c4b441de65 Support thinking tags used by Openthinker 2025-02-21 09:12:34 -05:00
Synergyst f789ad59a9 Update audio.py
Removed original code that was commented out
2025-02-21 04:47:46 -06:00
Coleton M cdf620e6ee Update audio.py to fetch custom URL voices and models 2025-02-21 04:41:45 -06:00
huanght 2993332b38 fix:Quick selection tool lost 2025-02-21 16:10:11 +08:00
Olof Larsson ab1f2ae914 feat/async-pipes 2025-02-21 08:24:48 +01:00
Karl Lee 68e33b5386 i18n: Update Chinese Translation 2025-02-21 14:58:40 +08:00
Timothy Jaeryang Baek e31f680788 refac 2025-02-20 20:46:00 -08:00
Timothy Jaeryang Baek 1bf16ec23d Merge pull request #10462 from Seniorsimo/fix-ollama-stream-conversion
**fix** ollama to openai conversion on one shot streamed response
2025-02-20 13:30:29 -08:00
Timothy Jaeryang Baek 2697a4d7f8 Merge pull request #10460 from aleixdorca/dev
Update catalan translation.json
2025-02-20 13:30:00 -08:00
Timothy Jaeryang Baek 8629f6b0f3 Merge pull request #10466 from HuangruiChu/main
i18n: Update zh-Chinese translation
2025-02-20 13:29:49 -08:00
Richard (Huangrui) Chu 1332a0d381 Update zh-CN translation
1. remove the typo ”‘’"  for -1 表示无限制,正整数表示具体限制”
2. Token should be kept rather than translated as "标记"
3. Max Tokens (num_predict) should be "最大Token数量 (num_predict)"
4. "Enter Jupyter Token" here “Token" could be translated as ”令牌“ just as 	"JWT Token": "JWT 令牌", (line 582)
5. "TLS" which means "Transport Layer Security" should be translated to "传输层安全协议"
6. "Tokens To Keep On Context Refresh (num_keep)" "在语境刷新时需保留的 Token 数量"
7. change token to "Token" in the Chinese translation.
2025-02-20 16:07:32 -05:00
Simone a8859a8145 Fix on ollama to openai conversion - stream can return a single message with content 2025-02-20 21:25:32 +01:00
Timothy Jaeryang Baek 6fedd72e39 Merge pull request #10459 from open-webui/dev
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
0.5.16
2025-02-20 11:26:36 -08:00
Timothy Jaeryang Baek 472a291c24 doc: changelog 2025-02-20 11:25:17 -08:00
Aleix Dorca 08df5f6d7c Update catalan translation.json 2025-02-20 20:24:58 +01:00
Timothy Jaeryang Baek b124c15018 Merge pull request #10431 from EntropyYue/dev
i18n: Update Chinese translation
2025-02-20 11:19:18 -08:00
Timothy Jaeryang Baek 0a7a265ff5 Merge pull request #10445 from PkmX/pr-fix-web-search-proxy
fix: correctly interpret RAG_WEB_SEARCH_TRUST_ENV as bool
2025-02-20 11:16:47 -08:00
Timothy Jaeryang Baek 93d486d50e revert: faulty dedup code 2025-02-20 11:02:45 -08:00
PkmX af10aba805 fix: correctly interpret RAG_WEB_SEARCH_TRUST_ENV as bool 2025-02-21 01:46:17 +08:00
Victor Ribeiro 39c6ecb50d feat: add audit logger middleware and custom logger using loguru 2025-02-20 11:15:38 -03:00
hurxxxx f865a23a18 Merge remote-tracking branch 'upstream/main' 2025-02-20 23:08:47 +09:00
EntropyYue 89b4627e9a Update translation.json 2025-02-20 21:47:21 +08:00
EntropyYue e920f96b17 Update translation.json 2025-02-20 21:12:04 +08:00
Timothy Jaeryang Baek cf4bbcde14 Merge pull request #10421 from open-webui/dev
fix: workflow
2025-02-20 02:10:02 -08:00
Timothy Jaeryang Baek c882aacc23 fix: workflow 2025-02-20 02:09:44 -08:00
Timothy Jaeryang Baek 624f2ebcff Merge pull request #10420 from open-webui/dependabot/pip/pip-e8a89e233e
build(deps): bump the pip group across 2 directories with 1 update
2025-02-20 02:03:07 -08:00
dependabot[bot] eb218f6674 build(deps): bump the pip group across 2 directories with 1 update
Bumps the pip group with 1 update in the / directory: [python-jose](https://github.com/mpdavis/python-jose).
Bumps the pip group with 1 update in the /backend directory: [python-jose](https://github.com/mpdavis/python-jose).


Updates `python-jose` from 3.3.0 to 3.4.0
- [Release notes](https://github.com/mpdavis/python-jose/releases)
- [Changelog](https://github.com/mpdavis/python-jose/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mpdavis/python-jose/compare/3.3.0...3.4.0)

Updates `python-jose` from 3.3.0 to 3.4.0
- [Release notes](https://github.com/mpdavis/python-jose/releases)
- [Changelog](https://github.com/mpdavis/python-jose/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mpdavis/python-jose/compare/3.3.0...3.4.0)

---
updated-dependencies:
- dependency-name: python-jose
  dependency-type: direct:production
  dependency-group: pip
- dependency-name: python-jose
  dependency-type: direct:production
  dependency-group: pip
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-20 10:02:35 +00:00
dannyl1u 8aa6415500 include logit_bias in form_data 2025-02-19 16:23:58 -08:00
dannyl1u 4a2a12fd21 feat: scaffolding for logit_bias 2025-02-19 10:33:49 -08:00
hurxxxx a381ffc585 revert 2025-02-18 23:17:06 +09:00
hurxxxx cdc13e9960 chore: Fix unnecessary whitespace issues 2025-02-18 23:15:57 +09:00
silentoplayz 1a2d81bc01 Update main.py
Change ASCII font for "Open WebUI" from "Standard" to "ANSI Shadow"
2025-02-14 12:48:33 -05:00
Didier FOURNOUT 06062568c7 black formatting 2025-02-13 16:12:46 +00:00
Didier FOURNOUT 6d62e71c34 Add x-Open-Webui headers for ollama + more for openai 2025-02-13 15:29:26 +00:00
Mazurek Michal 35f3824932 feat: Implement Document Intelligence as Content Extraction Engine 2025-02-07 13:44:47 +01:00
209 changed files with 9529 additions and 4744 deletions
-80
View File
@@ -1,80 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
# Bug Report
## Important Notes
- **Before submitting a bug report**: Please check the Issues or Discussions section to see if a similar issue or feature request has already been posted. It's likely we're already tracking it! If youre unsure, start a discussion post first. 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. Were here to help if youre open to learning and communicating positively. Remember, Open WebUI is a volunteer-driven project managed by a single maintainer and supported by contributors who also have full-time jobs. 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.
- **Bug reproducibility**: If a bug cannot be reproduced with a `:main` or `:dev` Docker setup, or a pip install with Python 3.11, it may require additional help from the community. In such cases, we will move it to the "issues" Discussions section due to our limited resources. We encourage the community to assist with these issues. Remember, its not that the issue doesnt exist; we need your help!
Note: Please remove the notes above when submitting your post. Thank you for your understanding and support!
---
## Installation Method
[Describe the method you used to install the project, e.g., git clone, Docker, pip, etc.]
## Environment
- **Open WebUI Version:** [e.g., v0.3.11]
- **Ollama (if applicable):** [e.g., v0.2.0, v0.1.32-rc1]
- **Operating System:** [e.g., Windows 10, macOS Big Sur, Ubuntu 20.04]
- **Browser (if applicable):** [e.g., Chrome 100.0, Firefox 98.0]
**Confirmation:**
- [ ] I have read and followed all the instructions provided in the README.md.
- [ ] I am on the latest version of both Open WebUI and Ollama.
- [ ] I have included the browser console logs.
- [ ] I have included the Docker container logs.
- [ ] I have provided the exact steps to reproduce the bug in the "Steps to Reproduce" section below.
## Expected Behavior:
[Describe what you expected to happen.]
## Actual Behavior:
[Describe what actually happened.]
## Description
**Bug Summary:**
[Provide a brief but clear summary of the bug]
## Reproduction Details
**Steps to Reproduce:**
[Outline the steps to reproduce the bug. Be as detailed as possible.]
## Logs and Screenshots
**Browser Console Logs:**
[Include relevant browser console logs, if applicable]
**Docker Container Logs:**
[Include relevant Docker container logs, if applicable]
**Screenshots/Screen Recordings (if applicable):**
[Attach any relevant screenshots to help illustrate the issue]
## Additional Information
[Include any additional details that may help in understanding and reproducing the issue. This could include specific configurations, error messages, or anything else relevant to the bug.]
## Note
If the bug report is incomplete or does not follow the provided instructions, it may not be addressed. Please ensure that you have followed the steps outlined in the README.md and troubleshooting.md documents, and provide all necessary information for us to reproduce and address the issue. Thank you!
+144
View File
@@ -0,0 +1,144 @@
name: Bug Report
description: Create a detailed bug report to help us improve Open WebUI.
title: 'issue: '
labels: ['bug', 'triage']
assignees: []
body:
- type: markdown
attributes:
value: |
# Bug Report
## Important Notes
- **Before submitting a bug report**: Please check the [Issues](https://github.com/open-webui/open-webui/issues) or [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.
- **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.
- **Contributing**: If you encounter an issue, consider submitting a pull request or forking the project. We prioritize preventing contributor burnout to maintain Open WebUI's quality.
- **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!
- type: checkboxes
id: issue-check
attributes:
label: Check Existing Issues
description: Confirm that youve checked for existing reports before submitting a new one.
options:
- label: I have searched the existing issues and discussions.
required: true
- type: dropdown
id: installation-method
attributes:
label: Installation Method
description: How did you install Open WebUI?
options:
- Git Clone
- Pip Install
- Docker
- Other
validations:
required: true
- type: input
id: open-webui-version
attributes:
label: Open WebUI Version
description: Specify the version (e.g., v0.3.11)
validations:
required: true
- type: input
id: ollama-version
attributes:
label: Ollama Version (if applicable)
description: Specify the version (e.g., v0.2.0, or v0.1.32-rc1)
validations:
required: false
- type: input
id: operating-system
attributes:
label: Operating System
description: Specify the OS (e.g., Windows 10, macOS Sonoma, Ubuntu 22.04)
validations:
required: true
- type: input
id: browser
attributes:
label: Browser (if applicable)
description: Specify the browser/version (e.g., Chrome 100.0, Firefox 98.0)
validations:
required: false
- type: checkboxes
id: confirmation
attributes:
label: Confirmation
description: Ensure the following prerequisites have been met.
options:
- label: I have read and followed all instructions in `README.md`.
required: true
- label: I am using the latest version of **both** Open WebUI and Ollama.
required: true
- label: I have checked the browser console logs.
required: true
- label: I have checked the Docker container logs.
required: true
- label: I have listed steps to reproduce the bug in detail.
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected Behavior
description: Describe what should have happened.
validations:
required: true
- type: textarea
id: actual-behavior
attributes:
label: Actual Behavior
description: Describe what actually happened.
validations:
required: true
- type: textarea
id: reproduction-steps
attributes:
label: Steps to Reproduce
description: Provide step-by-step instructions to reproduce the issue.
placeholder: |
1. Go to '...'
2. Click on '...'
3. Scroll down to '...'
4. See the error message '...'
validations:
required: true
- type: textarea
id: logs-screenshots
attributes:
label: Logs & Screenshots
description: Include relevant logs, errors, or screenshots to help diagnose the issue.
placeholder: 'Attach logs from the browser console, Docker logs, or error messages.'
validations:
required: true
- type: textarea
id: additional-info
attributes:
label: Additional Information
description: Provide any extra details that may assist in understanding the issue.
validations:
required: false
- type: markdown
attributes:
value: |
## Note
If the bug report is incomplete or does not follow instructions, it may not be addressed. Ensure that you've followed all the **README.md** and **troubleshooting.md** guidelines, and provide all necessary information for us to reproduce the issue.
Thank you for contributing to Open WebUI!
-35
View File
@@ -1,35 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
# Feature Request
## Important Notes
- **Before submitting a report**: Please check the Issues or Discussions section to see if a similar issue or feature request has already been posted. It's likely we're already tracking it! If youre unsure, start a discussion post first. 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. Were here to help if youre open to learning and communicating positively. Remember, Open WebUI is a volunteer-driven project managed by a single maintainer and supported by contributors who also have full-time jobs. 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.
- **Bug reproducibility**: If a bug cannot be reproduced with a `:main` or `:dev` Docker setup, or a pip install with Python 3.11, it may require additional help from the community. In such cases, we will move it to the "issues" Discussions section due to our limited resources. We encourage the community to assist with these issues. Remember, its not that the issue doesnt exist; we need your help!
Note: Please remove the notes above when submitting your post. Thank you for your understanding and support!
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
@@ -0,0 +1,64 @@
name: Feature Request
description: Suggest an idea for this project
title: 'feat: '
labels: ['triage']
body:
- type: markdown
attributes:
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.
It's likely we're already tracking it! If youre unsure, start a discussion post first.
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**.
Remember:
- Open WebUI is a **volunteer-driven project**
- It's managed by a **single maintainer**
- It's supported by contributors who also have **full-time jobs**
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.
### Bug reproducibility
If a bug cannot be reproduced with a `:main` or `:dev` Docker setup, or a `pip install` with Python 3.11, it may require additional help from the community. In such cases, we will move it to the "[issues](https://github.com/open-webui/open-webui/discussions/categories/issues)" Discussions section due to our limited resources. We encourage the community to assist with these issues. Remember, its not that the issue doesnt exist; we need your help!
- type: checkboxes
id: existing-issue
attributes:
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.
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..."
validations:
required: true
- type: textarea
id: solution-description
attributes:
label: Desired Solution you'd like
description: Clearly describe what you want to happen.
validations:
required: true
- type: textarea
id: alternatives-considered
attributes:
label: Alternatives Considered
description: A clear and concise description of any alternative solutions or features you've considered.
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: Add any other context or screenshots about the feature request here.
+3 -3
View File
@@ -14,7 +14,7 @@ env:
jobs:
build-main-image:
runs-on: ubuntu-latest
runs-on: ${{ matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }}
permissions:
contents: read
packages: write
@@ -111,7 +111,7 @@ jobs:
retention-days: 1
build-cuda-image:
runs-on: ubuntu-latest
runs-on: ${{ matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }}
permissions:
contents: read
packages: write
@@ -211,7 +211,7 @@ jobs:
retention-days: 1
build-ollama-image:
runs-on: ubuntu-latest
runs-on: ${{ matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }}
permissions:
contents: read
packages: write
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
node-version: 22
- uses: actions/setup-python@v5
with:
python-version: 3.11
+65
View File
@@ -5,6 +5,71 @@ 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.5.20] - 2025-03-05
### Added
- **⚡ Toggle Code Execution On/Off**: You can now enable or disable code execution, providing more control over security, ensuring a safer and more customizable experience.
### Fixed
- **📜 Pinyin Keyboard Enter Key Now Works Properly**: Resolved an issue where the Enter key for Pinyin keyboards was not functioning as expected, ensuring seamless input for Chinese users.
- **🖼️ Web Manifest Loading Issue Fixed**: Addressed inconsistencies with 'site.webmanifest', guaranteeing proper loading and representation of the app across different browsers and devices.
- **📦 Non-Root Container Issue Resolved**: Fixed a critical issue where the UI failed to load correctly in non-root containers, ensuring reliable deployment in various environments.
## [0.5.19] - 2025-03-04
### Added
- **📊 Logit Bias Parameter Support**: Fine-tune conversation dynamics by adjusting the Logit Bias parameter directly in chat settings, giving you more control over model responses.
- **⌨️ Customizable Enter Behavior**: You can now configure Enter to send messages only when combined with Ctrl (Ctrl+Enter) via Settings > Interface, preventing accidental message sends.
- **📝 Collapsible Code Blocks**: Easily collapse long code blocks to declutter your chat, making it easier to focus on important details.
- **🏷️ Tag Selector in Model Selector**: Quickly find and categorize models with the new tag filtering system in the Model Selector, streamlining model discovery.
- **📈 Experimental Elasticsearch Vector DB Support**: Now supports Elasticsearch as a vector database, offering more flexibility for data retrieval in Retrieval-Augmented Generation (RAG) workflows.
- **⚙️ General Reliability Enhancements**: Various stability improvements across the WebUI, ensuring a smoother, more consistent experience.
- **🌍 Updated Translations**: Refined multilingual support for better localization and accuracy across various languages.
### Fixed
- **🔄 "Stream" Hook Activation**: Fixed an issue where the "Stream" hook only worked when globally enabled, ensuring reliable real-time filtering.
- **📧 LDAP Email Case Sensitivity**: Resolved an issue where LDAP login failed due to email case sensitivity mismatches, improving authentication reliability.
- **💬 WebSocket Chat Event Registration**: Fixed a bug preventing chat event listeners from being registered upon sign-in, ensuring real-time updates work properly.
## [0.5.18] - 2025-02-27
### Fixed
- **🌐 Open WebUI Now Works Over LAN in Insecure Context**: Resolved an issue preventing Open WebUI from functioning when accessed over a local network in an insecure context, ensuring seamless connectivity.
- **🔄 UI Now Reflects Deleted Connections Instantly**: Fixed an issue where deleting a connection did not update the UI in real time, ensuring accurate system state visibility.
- **🛠️ Models Now Display Correctly with ENABLE_FORWARD_USER_INFO_HEADERS**: Addressed a bug where models were not visible when ENABLE_FORWARD_USER_INFO_HEADERS was set, restoring proper model listing.
## [0.5.17] - 2025-02-27
### Added
- **🚀 Instant Document Upload with Bypass Embedding & Retrieval**: Admins can now enable "Bypass Embedding & Retrieval" in Admin Settings > Documents, significantly speeding up document uploads and ensuring full document context is retained without chunking.
- **🔎 "Stream" Hook for Real-Time Filtering**: The new "stream" hook allows dynamic real-time message filtering. Learn more in our documentation (https://docs.openwebui.com/features/plugin/functions/filter).
- **☁️ OneDrive Integration**: Early support for OneDrive storage integration has been introduced, expanding file import options.
- **📈 Enhanced Logging with Loguru**: Backend logging has been improved with Loguru, making debugging and issue tracking far more efficient.
- **⚙️ General Stability Enhancements**: Backend and frontend refactoring improves performance, ensuring a smoother and more reliable user experience.
- **🌍 Updated Translations**: Refined multilingual support for better localization and accuracy across various languages.
### Fixed
- **🔄 Reliable Model Imports from the Community Platform**: Resolved import failures, allowing seamless integration of community-shared models without errors.
- **📊 OpenAI Usage Statistics Restored**: Fixed an issue where OpenAI usage metrics were not displaying correctly, ensuring accurate tracking of usage data.
- **🗂️ Deduplication for Retrieved Documents**: Documents retrieved during searches are now intelligently deduplicated, meaning no more redundant results—helping to keep information concise and relevant.
### Changed
- **📝 "Full Context Mode" Renamed for Clarity**: The "Full Context Mode" toggle in Web Search settings is now labeled "Bypass Embedding & Retrieval" for consistency across the UI.
## [0.5.16] - 2025-02-20
### Fixed
- **🔍 Web Search Retrieval Restored**: Resolved a critical issue that broke web search retrieval by reverting deduplication changes, ensuring complete and accurate search results once again.
## [0.5.15] - 2025-02-20
### Added
+104 -34
View File
@@ -9,7 +9,6 @@ from pathlib import Path
from typing import Generic, Optional, TypeVar
from urllib.parse import urlparse
import chromadb
import requests
from pydantic import BaseModel
from sqlalchemy import JSON, Column, DateTime, Integer, func
@@ -44,7 +43,7 @@ logging.getLogger("uvicorn.access").addFilter(EndpointFilter())
# Function to run the alembic migrations
def run_migrations():
print("Running migrations")
log.info("Running migrations")
try:
from alembic import command
from alembic.config import Config
@@ -57,7 +56,7 @@ def run_migrations():
command.upgrade(alembic_cfg, "head")
except Exception as e:
print(f"Error: {e}")
log.exception(f"Error running migrations: {e}")
run_migrations()
@@ -588,6 +587,17 @@ load_oauth_providers()
STATIC_DIR = Path(os.getenv("STATIC_DIR", OPEN_WEBUI_DIR / "static")).resolve()
for file_path in (FRONTEND_BUILD_DIR / "static").glob("**/*"):
if file_path.is_file():
target_path = STATIC_DIR / file_path.relative_to(
(FRONTEND_BUILD_DIR / "static")
)
target_path.parent.mkdir(parents=True, exist_ok=True)
try:
shutil.copyfile(file_path, target_path)
except Exception as e:
logging.error(f"An error occurred: {e}")
frontend_favicon = FRONTEND_BUILD_DIR / "static" / "favicon.png"
if frontend_favicon.exists():
@@ -660,11 +670,7 @@ if CUSTOM_NAME:
# LICENSE_KEY
####################################
LICENSE_KEY = PersistentConfig(
"LICENSE_KEY",
"license.key",
os.environ.get("LICENSE_KEY", ""),
)
LICENSE_KEY = os.environ.get("LICENSE_KEY", "")
####################################
# STORAGE PROVIDER
@@ -678,6 +684,10 @@ S3_REGION_NAME = os.environ.get("S3_REGION_NAME", None)
S3_BUCKET_NAME = os.environ.get("S3_BUCKET_NAME", None)
S3_KEY_PREFIX = os.environ.get("S3_KEY_PREFIX", None)
S3_ENDPOINT_URL = os.environ.get("S3_ENDPOINT_URL", None)
S3_USE_ACCELERATE_ENDPOINT = (
os.environ.get("S3_USE_ACCELERATE_ENDPOINT", "False").lower() == "true"
)
S3_ADDRESSING_STYLE = os.environ.get("S3_ADDRESSING_STYLE", None)
GCS_BUCKET_NAME = os.environ.get("GCS_BUCKET_NAME", None)
GOOGLE_APPLICATION_CREDENTIALS_JSON = os.environ.get(
@@ -692,16 +702,16 @@ AZURE_STORAGE_KEY = os.environ.get("AZURE_STORAGE_KEY", None)
# File Upload DIR
####################################
UPLOAD_DIR = f"{DATA_DIR}/uploads"
Path(UPLOAD_DIR).mkdir(parents=True, exist_ok=True)
UPLOAD_DIR = DATA_DIR / "uploads"
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
####################################
# Cache DIR
####################################
CACHE_DIR = f"{DATA_DIR}/cache"
Path(CACHE_DIR).mkdir(parents=True, exist_ok=True)
CACHE_DIR = DATA_DIR / "cache"
CACHE_DIR.mkdir(parents=True, exist_ok=True)
####################################
@@ -1094,7 +1104,7 @@ try:
banners = json.loads(os.environ.get("WEBUI_BANNERS", "[]"))
banners = [BannerModel(**banner) for banner in banners]
except Exception as e:
print(f"Error loading WEBUI_BANNERS: {e}")
log.exception(f"Error loading WEBUI_BANNERS: {e}")
banners = []
WEBUI_BANNERS = PersistentConfig("WEBUI_BANNERS", "ui.banners", banners)
@@ -1370,6 +1380,11 @@ Responses from models: {{responses}}"""
# Code Interpreter
####################################
ENABLE_CODE_EXECUTION = PersistentConfig(
"ENABLE_CODE_EXECUTION",
"code_execution.enable",
os.environ.get("ENABLE_CODE_EXECUTION", "True").lower() == "true",
)
CODE_EXECUTION_ENGINE = PersistentConfig(
"CODE_EXECUTION_ENGINE",
@@ -1498,21 +1513,27 @@ VECTOR_DB = os.environ.get("VECTOR_DB", "chroma")
# Chroma
CHROMA_DATA_PATH = f"{DATA_DIR}/vector_db"
CHROMA_TENANT = os.environ.get("CHROMA_TENANT", chromadb.DEFAULT_TENANT)
CHROMA_DATABASE = os.environ.get("CHROMA_DATABASE", chromadb.DEFAULT_DATABASE)
CHROMA_HTTP_HOST = os.environ.get("CHROMA_HTTP_HOST", "")
CHROMA_HTTP_PORT = int(os.environ.get("CHROMA_HTTP_PORT", "8000"))
CHROMA_CLIENT_AUTH_PROVIDER = os.environ.get("CHROMA_CLIENT_AUTH_PROVIDER", "")
CHROMA_CLIENT_AUTH_CREDENTIALS = os.environ.get("CHROMA_CLIENT_AUTH_CREDENTIALS", "")
# Comma-separated list of header=value pairs
CHROMA_HTTP_HEADERS = os.environ.get("CHROMA_HTTP_HEADERS", "")
if CHROMA_HTTP_HEADERS:
CHROMA_HTTP_HEADERS = dict(
[pair.split("=") for pair in CHROMA_HTTP_HEADERS.split(",")]
if VECTOR_DB == "chroma":
import chromadb
CHROMA_TENANT = os.environ.get("CHROMA_TENANT", chromadb.DEFAULT_TENANT)
CHROMA_DATABASE = os.environ.get("CHROMA_DATABASE", chromadb.DEFAULT_DATABASE)
CHROMA_HTTP_HOST = os.environ.get("CHROMA_HTTP_HOST", "")
CHROMA_HTTP_PORT = int(os.environ.get("CHROMA_HTTP_PORT", "8000"))
CHROMA_CLIENT_AUTH_PROVIDER = os.environ.get("CHROMA_CLIENT_AUTH_PROVIDER", "")
CHROMA_CLIENT_AUTH_CREDENTIALS = os.environ.get(
"CHROMA_CLIENT_AUTH_CREDENTIALS", ""
)
else:
CHROMA_HTTP_HEADERS = None
CHROMA_HTTP_SSL = os.environ.get("CHROMA_HTTP_SSL", "false").lower() == "true"
# Comma-separated list of header=value pairs
CHROMA_HTTP_HEADERS = os.environ.get("CHROMA_HTTP_HEADERS", "")
if CHROMA_HTTP_HEADERS:
CHROMA_HTTP_HEADERS = dict(
[pair.split("=") for pair in CHROMA_HTTP_HEADERS.split(",")]
)
else:
CHROMA_HTTP_HEADERS = None
CHROMA_HTTP_SSL = os.environ.get("CHROMA_HTTP_SSL", "false").lower() == "true"
# this uses the model defined in the Dockerfile ENV variable. If you dont use docker or docker based deployments such as k8s, the default embedding model will be used (sentence-transformers/all-MiniLM-L6-v2)
# Milvus
@@ -1532,6 +1553,17 @@ OPENSEARCH_CERT_VERIFY = os.environ.get("OPENSEARCH_CERT_VERIFY", False)
OPENSEARCH_USERNAME = os.environ.get("OPENSEARCH_USERNAME", None)
OPENSEARCH_PASSWORD = os.environ.get("OPENSEARCH_PASSWORD", None)
# ElasticSearch
ELASTICSEARCH_URL = os.environ.get("ELASTICSEARCH_URL", "https://localhost:9200")
ELASTICSEARCH_CA_CERTS = os.environ.get("ELASTICSEARCH_CA_CERTS", None)
ELASTICSEARCH_API_KEY = os.environ.get("ELASTICSEARCH_API_KEY", None)
ELASTICSEARCH_USERNAME = os.environ.get("ELASTICSEARCH_USERNAME", None)
ELASTICSEARCH_PASSWORD = os.environ.get("ELASTICSEARCH_PASSWORD", None)
ELASTICSEARCH_CLOUD_ID = os.environ.get("ELASTICSEARCH_CLOUD_ID", None)
SSL_ASSERT_FINGERPRINT = os.environ.get("SSL_ASSERT_FINGERPRINT", None)
ELASTICSEARCH_INDEX_PREFIX = os.environ.get(
"ELASTICSEARCH_INDEX_PREFIX", "open_webui_collections"
)
# Pgvector
PGVECTOR_DB_URL = os.environ.get("PGVECTOR_DB_URL", DATABASE_URL)
if VECTOR_DB == "pgvector" and not PGVECTOR_DB_URL.startswith("postgres"):
@@ -1566,6 +1598,18 @@ GOOGLE_DRIVE_API_KEY = PersistentConfig(
os.environ.get("GOOGLE_DRIVE_API_KEY", ""),
)
ENABLE_ONEDRIVE_INTEGRATION = PersistentConfig(
"ENABLE_ONEDRIVE_INTEGRATION",
"onedrive.enable",
os.getenv("ENABLE_ONEDRIVE_INTEGRATION", "False").lower() == "true",
)
ONEDRIVE_CLIENT_ID = PersistentConfig(
"ONEDRIVE_CLIENT_ID",
"onedrive.client_id",
os.environ.get("ONEDRIVE_CLIENT_ID", ""),
)
# RAG Content Extraction
CONTENT_EXTRACTION_ENGINE = PersistentConfig(
"CONTENT_EXTRACTION_ENGINE",
@@ -1579,6 +1623,26 @@ TIKA_SERVER_URL = PersistentConfig(
os.getenv("TIKA_SERVER_URL", "http://tika:9998"), # Default for sidecar deployment
)
DOCUMENT_INTELLIGENCE_ENDPOINT = PersistentConfig(
"DOCUMENT_INTELLIGENCE_ENDPOINT",
"rag.document_intelligence_endpoint",
os.getenv("DOCUMENT_INTELLIGENCE_ENDPOINT", ""),
)
DOCUMENT_INTELLIGENCE_KEY = PersistentConfig(
"DOCUMENT_INTELLIGENCE_KEY",
"rag.document_intelligence_key",
os.getenv("DOCUMENT_INTELLIGENCE_KEY", ""),
)
BYPASS_EMBEDDING_AND_RETRIEVAL = PersistentConfig(
"BYPASS_EMBEDDING_AND_RETRIEVAL",
"rag.bypass_embedding_and_retrieval",
os.environ.get("BYPASS_EMBEDDING_AND_RETRIEVAL", "False").lower() == "true",
)
RAG_TOP_K = PersistentConfig(
"RAG_TOP_K", "rag.top_k", int(os.environ.get("RAG_TOP_K", "3"))
)
@@ -1714,7 +1778,7 @@ Respond to the user query using the provided context, incorporating inline citat
- Respond in the same language as the user's query.
- If the context is unreadable or of poor quality, inform the user and provide the best possible answer.
- If the answer isn't present in the context but you possess the knowledge, explain this to the user and provide the answer using your own understanding.
- **Only include inline citations using [source_id] when a <source_id> tag is explicitly provided in the context.**
- **Only include inline citations using [source_id] (e.g., [1], [2]) when a `<source_id>` tag is explicitly provided in the context.**
- Do not cite if the <source_id> tag is not provided in the context.
- Do not use XML tags in your response.
- Ensure citations are concise and directly related to the information provided.
@@ -1795,10 +1859,10 @@ RAG_WEB_SEARCH_ENGINE = PersistentConfig(
os.getenv("RAG_WEB_SEARCH_ENGINE", ""),
)
RAG_WEB_SEARCH_FULL_CONTEXT = PersistentConfig(
"RAG_WEB_SEARCH_FULL_CONTEXT",
"rag.web.search.full_context",
os.getenv("RAG_WEB_SEARCH_FULL_CONTEXT", "False").lower() == "true",
BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = PersistentConfig(
"BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL",
"rag.web.search.bypass_embedding_and_retrieval",
os.getenv("BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL", "False").lower() == "true",
)
# You can provide a list of your own websites to filter after performing a web search.
@@ -1936,6 +2000,12 @@ EXA_API_KEY = PersistentConfig(
os.getenv("EXA_API_KEY", ""),
)
PERPLEXITY_API_KEY = PersistentConfig(
"PERPLEXITY_API_KEY",
"rag.web.search.perplexity_api_key",
os.getenv("PERPLEXITY_API_KEY", ""),
)
RAG_WEB_SEARCH_RESULT_COUNT = PersistentConfig(
"RAG_WEB_SEARCH_RESULT_COUNT",
"rag.web.search.result_count",
@@ -1957,7 +2027,7 @@ RAG_WEB_LOADER_ENGINE = PersistentConfig(
RAG_WEB_SEARCH_TRUST_ENV = PersistentConfig(
"RAG_WEB_SEARCH_TRUST_ENV",
"rag.web.search.trust_env",
os.getenv("RAG_WEB_SEARCH_TRUST_ENV", False),
os.getenv("RAG_WEB_SEARCH_TRUST_ENV", "False").lower() == "true",
)
PLAYWRIGHT_WS_URI = PersistentConfig(
@@ -2375,7 +2445,7 @@ LDAP_SEARCH_BASE = PersistentConfig(
LDAP_SEARCH_FILTERS = PersistentConfig(
"LDAP_SEARCH_FILTER",
"ldap.server.search_filter",
os.environ.get("LDAP_SEARCH_FILTER", ""),
os.environ.get("LDAP_SEARCH_FILTER", os.environ.get("LDAP_SEARCH_FILTERS", "")),
)
LDAP_USE_TLS = PersistentConfig(
+35 -12
View File
@@ -65,10 +65,8 @@ except Exception:
# LOGGING
####################################
log_levels = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"]
GLOBAL_LOG_LEVEL = os.environ.get("GLOBAL_LOG_LEVEL", "").upper()
if GLOBAL_LOG_LEVEL in log_levels:
if GLOBAL_LOG_LEVEL in logging.getLevelNamesMapping():
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL, force=True)
else:
GLOBAL_LOG_LEVEL = "INFO"
@@ -78,6 +76,7 @@ log.info(f"GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}")
if "cuda_error" in locals():
log.exception(cuda_error)
del cuda_error
log_sources = [
"AUDIO",
@@ -100,7 +99,7 @@ SRC_LOG_LEVELS = {}
for source in log_sources:
log_env_var = source + "_LOG_LEVEL"
SRC_LOG_LEVELS[source] = os.environ.get(log_env_var, "").upper()
if SRC_LOG_LEVELS[source] not in log_levels:
if SRC_LOG_LEVELS[source] not in logging.getLevelNamesMapping():
SRC_LOG_LEVELS[source] = GLOBAL_LOG_LEVEL
log.info(f"{log_env_var}: {SRC_LOG_LEVELS[source]}")
@@ -386,6 +385,7 @@ ENABLE_WEBSOCKET_SUPPORT = (
WEBSOCKET_MANAGER = os.environ.get("WEBSOCKET_MANAGER", "")
WEBSOCKET_REDIS_URL = os.environ.get("WEBSOCKET_REDIS_URL", REDIS_URL)
WEBSOCKET_REDIS_LOCK_TIMEOUT = os.environ.get("WEBSOCKET_REDIS_LOCK_TIMEOUT", 60)
AIOHTTP_CLIENT_TIMEOUT = os.environ.get("AIOHTTP_CLIENT_TIMEOUT", "")
@@ -397,19 +397,20 @@ else:
except Exception:
AIOHTTP_CLIENT_TIMEOUT = 300
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST = os.environ.get(
"AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST", ""
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = os.environ.get(
"AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST",
os.environ.get("AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST", ""),
)
if AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST == "":
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST = None
if AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST == "":
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = None
else:
try:
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST = int(
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST
)
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = int(AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
except Exception:
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST = 5
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = 5
####################################
# OFFLINE_MODE
@@ -419,3 +420,25 @@ OFFLINE_MODE = os.environ.get("OFFLINE_MODE", "false").lower() == "true"
if OFFLINE_MODE:
os.environ["HF_HUB_OFFLINE"] = "1"
####################################
# AUDIT LOGGING
####################################
ENABLE_AUDIT_LOGS = os.getenv("ENABLE_AUDIT_LOGS", "false").lower() == "true"
# Where to store log file
AUDIT_LOGS_FILE_PATH = f"{DATA_DIR}/audit.log"
# Maximum size of a file before rotating into a new log file
AUDIT_LOG_FILE_ROTATION_SIZE = os.getenv("AUDIT_LOG_FILE_ROTATION_SIZE", "10MB")
# METADATA | REQUEST | REQUEST_RESPONSE
AUDIT_LOG_LEVEL = os.getenv("AUDIT_LOG_LEVEL", "REQUEST_RESPONSE").upper()
try:
MAX_BODY_LOG_SIZE = int(os.environ.get("MAX_BODY_LOG_SIZE") or 2048)
except ValueError:
MAX_BODY_LOG_SIZE = 2048
# Comma separated list for urls to exclude from audit
AUDIT_EXCLUDED_PATHS = os.getenv("AUDIT_EXCLUDED_PATHS", "/chats,/chat,/folders").split(
","
)
AUDIT_EXCLUDED_PATHS = [path.strip() for path in AUDIT_EXCLUDED_PATHS]
AUDIT_EXCLUDED_PATHS = [path.lstrip("/") for path in AUDIT_EXCLUDED_PATHS]
+6 -3
View File
@@ -2,6 +2,7 @@ import logging
import sys
import inspect
import json
import asyncio
from pydantic import BaseModel
from typing import AsyncGenerator, Generator, Iterator
@@ -76,11 +77,13 @@ async def get_function_models(request):
if hasattr(function_module, "pipes"):
sub_pipes = []
# Check if pipes is a function or a list
# Handle pipes being a list, sync function, or async function
try:
if callable(function_module.pipes):
sub_pipes = function_module.pipes()
if asyncio.iscoroutinefunction(function_module.pipes):
sub_pipes = await function_module.pipes()
else:
sub_pipes = function_module.pipes()
else:
sub_pipes = function_module.pipes
except Exception as e:
+61 -12
View File
@@ -45,6 +45,9 @@ from starlette.middleware.sessions import SessionMiddleware
from starlette.responses import Response, StreamingResponse
from open_webui.utils import logger
from open_webui.utils.audit import AuditLevel, AuditLoggingMiddleware
from open_webui.utils.logger import start_logger
from open_webui.socket.main import (
app as socket_app,
periodic_usage_pool_cleanup,
@@ -95,12 +98,14 @@ from open_webui.config import (
OLLAMA_API_CONFIGS,
# OpenAI
ENABLE_OPENAI_API,
ONEDRIVE_CLIENT_ID,
OPENAI_API_BASE_URLS,
OPENAI_API_KEYS,
OPENAI_API_CONFIGS,
# Direct Connections
ENABLE_DIRECT_CONNECTIONS,
# Code Execution
ENABLE_CODE_EXECUTION,
CODE_EXECUTION_ENGINE,
CODE_EXECUTION_JUPYTER_URL,
CODE_EXECUTION_JUPYTER_AUTH,
@@ -161,6 +166,7 @@ from open_webui.config import (
RAG_TEMPLATE,
DEFAULT_RAG_TEMPLATE,
RAG_FULL_CONTEXT,
BYPASS_EMBEDDING_AND_RETRIEVAL,
RAG_EMBEDDING_MODEL,
RAG_EMBEDDING_MODEL_AUTO_UPDATE,
RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
@@ -180,6 +186,8 @@ from open_webui.config import (
CHUNK_SIZE,
CONTENT_EXTRACTION_ENGINE,
TIKA_SERVER_URL,
DOCUMENT_INTELLIGENCE_ENDPOINT,
DOCUMENT_INTELLIGENCE_KEY,
RAG_TOP_K,
RAG_TEXT_SPLITTER,
TIKTOKEN_ENCODING_NAME,
@@ -188,7 +196,7 @@ from open_webui.config import (
YOUTUBE_LOADER_PROXY_URL,
# Retrieval (Web Search)
RAG_WEB_SEARCH_ENGINE,
RAG_WEB_SEARCH_FULL_CONTEXT,
BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL,
RAG_WEB_SEARCH_RESULT_COUNT,
RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
RAG_WEB_SEARCH_TRUST_ENV,
@@ -208,6 +216,7 @@ from open_webui.config import (
BING_SEARCH_V7_SUBSCRIPTION_KEY,
BRAVE_SEARCH_API_KEY,
EXA_API_KEY,
PERPLEXITY_API_KEY,
KAGI_SEARCH_API_KEY,
MOJEEK_SEARCH_API_KEY,
BOCHA_SEARCH_API_KEY,
@@ -215,11 +224,13 @@ from open_webui.config import (
GOOGLE_PSE_ENGINE_ID,
GOOGLE_DRIVE_CLIENT_ID,
GOOGLE_DRIVE_API_KEY,
ONEDRIVE_CLIENT_ID,
ENABLE_RAG_HYBRID_SEARCH,
ENABLE_RAG_LOCAL_WEB_FETCH,
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
ENABLE_RAG_WEB_SEARCH,
ENABLE_GOOGLE_DRIVE_INTEGRATION,
ENABLE_ONEDRIVE_INTEGRATION,
UPLOAD_DIR,
# WebUI
WEBUI_AUTH,
@@ -298,8 +309,11 @@ from open_webui.config import (
reset_config,
)
from open_webui.env import (
AUDIT_EXCLUDED_PATHS,
AUDIT_LOG_LEVEL,
CHANGELOG,
GLOBAL_LOG_LEVEL,
MAX_BODY_LOG_SIZE,
SAFE_MODE,
SRC_LOG_LEVELS,
VERSION,
@@ -384,11 +398,12 @@ https://github.com/open-webui/open-webui
@asynccontextmanager
async def lifespan(app: FastAPI):
start_logger()
if RESET_CONFIG_ON_START:
reset_config()
if app.state.config.LICENSE_KEY:
get_license_data(app, app.state.config.LICENSE_KEY)
if LICENSE_KEY:
get_license_data(app, LICENSE_KEY)
asyncio.create_task(periodic_usage_pool_cleanup())
yield
@@ -406,7 +421,7 @@ oauth_manager = OAuthManager(app)
app.state.config = AppConfig()
app.state.WEBUI_NAME = WEBUI_NAME
app.state.config.LICENSE_KEY = LICENSE_KEY
app.state.LICENSE_METADATA = None
########################################
#
@@ -526,6 +541,7 @@ app.state.config.FILE_MAX_COUNT = RAG_FILE_MAX_COUNT
app.state.config.RAG_FULL_CONTEXT = RAG_FULL_CONTEXT
app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL = BYPASS_EMBEDDING_AND_RETRIEVAL
app.state.config.ENABLE_RAG_HYBRID_SEARCH = ENABLE_RAG_HYBRID_SEARCH
app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
@@ -533,6 +549,8 @@ app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
app.state.config.CONTENT_EXTRACTION_ENGINE = CONTENT_EXTRACTION_ENGINE
app.state.config.TIKA_SERVER_URL = TIKA_SERVER_URL
app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT = DOCUMENT_INTELLIGENCE_ENDPOINT
app.state.config.DOCUMENT_INTELLIGENCE_KEY = DOCUMENT_INTELLIGENCE_KEY
app.state.config.TEXT_SPLITTER = RAG_TEXT_SPLITTER
app.state.config.TIKTOKEN_ENCODING_NAME = TIKTOKEN_ENCODING_NAME
@@ -560,10 +578,13 @@ app.state.config.YOUTUBE_LOADER_PROXY_URL = YOUTUBE_LOADER_PROXY_URL
app.state.config.ENABLE_RAG_WEB_SEARCH = ENABLE_RAG_WEB_SEARCH
app.state.config.RAG_WEB_SEARCH_ENGINE = RAG_WEB_SEARCH_ENGINE
app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT = RAG_WEB_SEARCH_FULL_CONTEXT
app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = (
BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL
)
app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = RAG_WEB_SEARCH_DOMAIN_FILTER_LIST
app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = ENABLE_GOOGLE_DRIVE_INTEGRATION
app.state.config.ENABLE_ONEDRIVE_INTEGRATION = ENABLE_ONEDRIVE_INTEGRATION
app.state.config.SEARXNG_QUERY_URL = SEARXNG_QUERY_URL
app.state.config.GOOGLE_PSE_API_KEY = GOOGLE_PSE_API_KEY
app.state.config.GOOGLE_PSE_ENGINE_ID = GOOGLE_PSE_ENGINE_ID
@@ -584,6 +605,7 @@ app.state.config.JINA_API_KEY = JINA_API_KEY
app.state.config.BING_SEARCH_V7_ENDPOINT = BING_SEARCH_V7_ENDPOINT
app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY = BING_SEARCH_V7_SUBSCRIPTION_KEY
app.state.config.EXA_API_KEY = EXA_API_KEY
app.state.config.PERPLEXITY_API_KEY = PERPLEXITY_API_KEY
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = RAG_WEB_SEARCH_RESULT_COUNT
app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = RAG_WEB_SEARCH_CONCURRENT_REQUESTS
@@ -639,6 +661,7 @@ app.state.EMBEDDING_FUNCTION = get_embedding_function(
#
########################################
app.state.config.ENABLE_CODE_EXECUTION = ENABLE_CODE_EXECUTION
app.state.config.CODE_EXECUTION_ENGINE = CODE_EXECUTION_ENGINE
app.state.config.CODE_EXECUTION_JUPYTER_URL = CODE_EXECUTION_JUPYTER_URL
app.state.config.CODE_EXECUTION_JUPYTER_AUTH = CODE_EXECUTION_JUPYTER_AUTH
@@ -879,6 +902,19 @@ app.include_router(
app.include_router(utils.router, prefix="/api/v1/utils", tags=["utils"])
try:
audit_level = AuditLevel(AUDIT_LOG_LEVEL)
except ValueError as e:
logger.error(f"Invalid audit level: {AUDIT_LOG_LEVEL}. Error: {e}")
audit_level = AuditLevel.NONE
if audit_level != AuditLevel.NONE:
app.add_middleware(
AuditLoggingMiddleware,
audit_level=audit_level,
excluded_paths=AUDIT_EXCLUDED_PATHS,
max_body_size=MAX_BODY_LOG_SIZE,
)
##################################
#
# Chat Endpoints
@@ -911,7 +947,7 @@ async def get_models(request: Request, user=Depends(get_verified_user)):
return filtered_models
models = await get_all_models(request)
models = await get_all_models(request, user=user)
# Filter out filter pipelines
models = [
@@ -940,7 +976,7 @@ async def get_models(request: Request, user=Depends(get_verified_user)):
@app.get("/api/models/base")
async def get_base_models(request: Request, user=Depends(get_admin_user)):
models = await get_all_base_models(request)
models = await get_all_base_models(request, user=user)
return {"data": models}
@@ -951,7 +987,7 @@ async def chat_completion(
user=Depends(get_verified_user),
):
if not request.app.state.MODELS:
await get_all_models(request)
await get_all_models(request, user=user)
model_item = form_data.pop("model_item", {})
tasks = form_data.pop("background_tasks", None)
@@ -987,7 +1023,7 @@ async def chat_completion(
"files": form_data.get("files", None),
"features": form_data.get("features", None),
"variables": form_data.get("variables", None),
"model": model_info.model_dump() if model_info else model,
"model": model,
"direct": model_item.get("direct", False),
**(
{"function_calling": "native"}
@@ -1005,7 +1041,7 @@ async def chat_completion(
form_data["metadata"] = metadata
form_data, metadata, events = await process_chat_payload(
request, form_data, metadata, user, model
request, form_data, user, metadata, model
)
except Exception as e:
@@ -1019,7 +1055,7 @@ async def chat_completion(
response = await chat_completion_handler(request, form_data, user)
return await process_chat_response(
request, response, form_data, user, events, metadata, tasks
request, response, form_data, user, metadata, model, events, tasks
)
except Exception as e:
raise HTTPException(
@@ -1108,9 +1144,10 @@ async def get_app_config(request: Request):
if data is not None and "id" in data:
user = Users.get_user_by_id(data["id"])
user_count = Users.get_num_users()
onboarding = False
if user is None:
user_count = Users.get_num_users()
onboarding = user_count == 0
return {
@@ -1138,6 +1175,7 @@ async def get_app_config(request: Request):
"enable_direct_connections": app.state.config.ENABLE_DIRECT_CONNECTIONS,
"enable_channels": app.state.config.ENABLE_CHANNELS,
"enable_web_search": app.state.config.ENABLE_RAG_WEB_SEARCH,
"enable_code_execution": app.state.config.ENABLE_CODE_EXECUTION,
"enable_code_interpreter": app.state.config.ENABLE_CODE_INTERPRETER,
"enable_image_generation": app.state.config.ENABLE_IMAGE_GENERATION,
"enable_autocomplete_generation": app.state.config.ENABLE_AUTOCOMPLETE_GENERATION,
@@ -1146,6 +1184,7 @@ async def get_app_config(request: Request):
"enable_admin_export": ENABLE_ADMIN_EXPORT,
"enable_admin_chat_access": ENABLE_ADMIN_CHAT_ACCESS,
"enable_google_drive_integration": app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
"enable_onedrive_integration": app.state.config.ENABLE_ONEDRIVE_INTEGRATION,
}
if user is not None
else {}
@@ -1155,6 +1194,7 @@ async def get_app_config(request: Request):
{
"default_models": app.state.config.DEFAULT_MODELS,
"default_prompt_suggestions": app.state.config.DEFAULT_PROMPT_SUGGESTIONS,
"user_count": user_count,
"code": {
"engine": app.state.config.CODE_EXECUTION_ENGINE,
},
@@ -1177,6 +1217,15 @@ async def get_app_config(request: Request):
"client_id": GOOGLE_DRIVE_CLIENT_ID.value,
"api_key": GOOGLE_DRIVE_API_KEY.value,
},
"onedrive": {"client_id": ONEDRIVE_CLIENT_ID.value},
"license_metadata": app.state.LICENSE_METADATA,
**(
{
"active_entries": app.state.USER_COUNT,
}
if user.role == "admin"
else {}
),
}
if user is not None
else {}
+9 -5
View File
@@ -1,3 +1,4 @@
import logging
import json
import time
import uuid
@@ -5,7 +6,7 @@ from typing import Optional
from open_webui.internal.db import Base, get_db
from open_webui.models.tags import TagModel, Tag, Tags
from open_webui.env import SRC_LOG_LEVELS
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON
@@ -16,6 +17,9 @@ from sqlalchemy.sql import exists
# Chat DB Schema
####################
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
class Chat(Base):
__tablename__ = "chat"
@@ -670,7 +674,7 @@ class ChatTable:
# Perform pagination at the SQL level
all_chats = query.offset(skip).limit(limit).all()
print(len(all_chats))
log.info(f"The number of chats: {len(all_chats)}")
# Validate and return chats
return [ChatModel.model_validate(chat) for chat in all_chats]
@@ -731,7 +735,7 @@ class ChatTable:
query = db.query(Chat).filter_by(user_id=user_id)
tag_id = tag_name.replace(" ", "_").lower()
print(db.bind.dialect.name)
log.info(f"DB dialect name: {db.bind.dialect.name}")
if db.bind.dialect.name == "sqlite":
# SQLite JSON1 querying for tags within the meta JSON field
query = query.filter(
@@ -752,7 +756,7 @@ class ChatTable:
)
all_chats = query.all()
print("all_chats", all_chats)
log.debug(f"all_chats: {all_chats}")
return [ChatModel.model_validate(chat) for chat in all_chats]
def add_chat_tag_by_id_and_user_id_and_tag_name(
@@ -810,7 +814,7 @@ class ChatTable:
count = query.count()
# Debugging output for inspection
print(f"Count of chats for tag '{tag_name}':", count)
log.info(f"Count of chats for tag '{tag_name}': {count}")
return count
+1 -1
View File
@@ -118,7 +118,7 @@ class FeedbackTable:
else:
return None
except Exception as e:
print(e)
log.exception(f"Error creating a new feedback: {e}")
return None
def get_feedback_by_id(self, id: str) -> Optional[FeedbackModel]:
+1 -1
View File
@@ -119,7 +119,7 @@ class FilesTable:
else:
return None
except Exception as e:
print(f"Error creating tool: {e}")
log.exception(f"Error inserting a new file: {e}")
return None
def get_file_by_id(self, id: str) -> Optional[FileModel]:
+1 -1
View File
@@ -82,7 +82,7 @@ class FolderTable:
else:
return None
except Exception as e:
print(e)
log.exception(f"Error inserting a new folder: {e}")
return None
def get_folder_by_id_and_user_id(
+8 -4
View File
@@ -105,7 +105,7 @@ class FunctionsTable:
else:
return None
except Exception as e:
print(f"Error creating tool: {e}")
log.exception(f"Error creating a new function: {e}")
return None
def get_function_by_id(self, id: str) -> Optional[FunctionModel]:
@@ -170,7 +170,7 @@ class FunctionsTable:
function = db.get(Function, id)
return function.valves if function.valves else {}
except Exception as e:
print(f"An error occurred: {e}")
log.exception(f"Error getting function valves by id {id}: {e}")
return None
def update_function_valves_by_id(
@@ -202,7 +202,9 @@ class FunctionsTable:
return user_settings["functions"]["valves"].get(id, {})
except Exception as e:
print(f"An error occurred: {e}")
log.exception(
f"Error getting user values by id {id} and user id {user_id}: {e}"
)
return None
def update_user_valves_by_id_and_user_id(
@@ -225,7 +227,9 @@ class FunctionsTable:
return user_settings["functions"]["valves"][id]
except Exception as e:
print(f"An error occurred: {e}")
log.exception(
f"Error updating user valves by id {id} and user_id {user_id}: {e}"
)
return None
def update_function_by_id(self, id: str, updated: dict) -> Optional[FunctionModel]:
+2 -3
View File
@@ -166,7 +166,7 @@ class ModelsTable:
else:
return None
except Exception as e:
print(e)
log.exception(f"Failed to insert a new model: {e}")
return None
def get_all_models(self) -> list[ModelModel]:
@@ -246,8 +246,7 @@ class ModelsTable:
db.refresh(model)
return ModelModel.model_validate(model)
except Exception as e:
print(e)
log.exception(f"Failed to update the model by id {id}: {e}")
return None
def delete_model_by_id(self, id: str) -> bool:
+1 -1
View File
@@ -61,7 +61,7 @@ class TagTable:
else:
return None
except Exception as e:
print(e)
log.exception(f"Error inserting a new tag: {e}")
return None
def get_tag_by_name_and_user_id(
+8 -4
View File
@@ -131,7 +131,7 @@ class ToolsTable:
else:
return None
except Exception as e:
print(f"Error creating tool: {e}")
log.exception(f"Error creating a new tool: {e}")
return None
def get_tool_by_id(self, id: str) -> Optional[ToolModel]:
@@ -175,7 +175,7 @@ class ToolsTable:
tool = db.get(Tool, id)
return tool.valves if tool.valves else {}
except Exception as e:
print(f"An error occurred: {e}")
log.exception(f"Error getting tool valves by id {id}: {e}")
return None
def update_tool_valves_by_id(self, id: str, valves: dict) -> Optional[ToolValves]:
@@ -204,7 +204,9 @@ class ToolsTable:
return user_settings["tools"]["valves"].get(id, {})
except Exception as e:
print(f"An error occurred: {e}")
log.exception(
f"Error getting user values by id {id} and user_id {user_id}: {e}"
)
return None
def update_user_valves_by_id_and_user_id(
@@ -227,7 +229,9 @@ class ToolsTable:
return user_settings["tools"]["valves"][id]
except Exception as e:
print(f"An error occurred: {e}")
log.exception(
f"Error updating user valves by id {id} and user_id {user_id}: {e}"
)
return None
def update_tool_by_id(self, id: str, updated: dict) -> Optional[ToolModel]:
@@ -0,0 +1,34 @@
from fastapi import FastAPI
class ThemeCustomizerPlugin:
def __init__(self):
self.name = "theme_customizer"
self.description = "Theme customization plugin for OpenWebUI"
self.version = "1.0.0"
self.author = "SamiAhmed7777"
def setup(self) -> None:
"""Setup any initial plugin state."""
pass
def register(self, app: FastAPI) -> None:
"""Register the theme customizer plugin with OpenWebUI."""
# Register frontend components
if not hasattr(app.state, "plugin_components"):
app.state.plugin_components = {}
# Theme Customization Component
app.state.plugin_components["theme_customizer"] = {
"name": "ThemeCustomizer",
"path": "/plugins/theme_customizer/components/ThemeCustomizer.svelte",
"settings": {
"section": "general", # This will make it appear under General
"title": "Theme Customization",
"icon": "palette",
"description": "Customize colors, fonts, and appearance",
"order": 300 # This will place it after the general settings
}
}
# Create plugin instance
plugin = ThemeCustomizerPlugin()
@@ -0,0 +1,109 @@
<script lang="ts">
import { onMount } from 'svelte';
import Button from '$lib/components/ui/button/Button.svelte';
import Input from '$lib/components/ui/input/Input.svelte';
import Label from '$lib/components/ui/label/Label.svelte';
import Card from '$lib/components/ui/card/Card.svelte';
import Select from '$lib/components/ui/select/Select.svelte';
import { toast } from '$lib/components/ui/toast';
let primaryColor = '#007AFF';
let secondaryColor = '#6B7280';
let backgroundColor = '#FFFFFF';
let textColor = '#000000';
let fontSize = '16px';
let currentFont = 'Inter';
let availableFonts = [
'Inter',
'Roboto',
'Open Sans',
'Montserrat',
'Lato'
];
async function updateTheme() {
try {
document.documentElement.style.setProperty('--primary-color', primaryColor);
document.documentElement.style.setProperty('--secondary-color', secondaryColor);
document.documentElement.style.setProperty('--background-color', backgroundColor);
document.documentElement.style.setProperty('--text-color', textColor);
document.documentElement.style.setProperty('--font-size', fontSize);
document.documentElement.style.setProperty('--font-family', currentFont);
toast({
title: 'Success',
description: 'Theme updated successfully'
});
} catch (error) {
toast({
title: 'Error',
description: 'Failed to update theme',
variant: 'destructive'
});
}
}
onMount(() => {
// Load current theme values from CSS variables
primaryColor = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || primaryColor;
secondaryColor = getComputedStyle(document.documentElement).getPropertyValue('--secondary-color').trim() || secondaryColor;
backgroundColor = getComputedStyle(document.documentElement).getPropertyValue('--background-color').trim() || backgroundColor;
textColor = getComputedStyle(document.documentElement).getPropertyValue('--text-color').trim() || textColor;
fontSize = getComputedStyle(document.documentElement).getPropertyValue('--font-size').trim() || fontSize;
currentFont = getComputedStyle(document.documentElement).getPropertyValue('--font-family').trim() || currentFont;
});
</script>
<Card class="p-6">
<h2 class="text-2xl font-bold mb-4">Theme Customization</h2>
<div class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div class="flex flex-col gap-2">
<Label>Primary Color</Label>
<Input type="color" bind:value={primaryColor} />
</div>
<div class="flex flex-col gap-2">
<Label>Secondary Color</Label>
<Input type="color" bind:value={secondaryColor} />
</div>
<div class="flex flex-col gap-2">
<Label>Background Color</Label>
<Input type="color" bind:value={backgroundColor} />
</div>
<div class="flex flex-col gap-2">
<Label>Text Color</Label>
<Input type="color" bind:value={textColor} />
</div>
</div>
<div class="flex flex-col gap-2">
<Label>Font Size</Label>
<Input
type="text"
bind:value={fontSize}
placeholder="e.g., 16px, 1.2rem"
/>
</div>
<div class="flex flex-col gap-2">
<Label>Font Family</Label>
<Select
bind:value={currentFont}
options={availableFonts}
/>
</div>
<Button
variant="primary"
class="w-full mt-4"
on:click={updateTheme}
>
Apply Theme
</Button>
</div>
</Card>
@@ -4,6 +4,7 @@ import ftfy
import sys
from langchain_community.document_loaders import (
AzureAIDocumentIntelligenceLoader,
BSHTMLLoader,
CSVLoader,
Docx2txtLoader,
@@ -76,6 +77,7 @@ known_source_ext = [
"jsx",
"hs",
"lhs",
"json",
]
@@ -147,6 +149,27 @@ class Loader:
file_path=file_path,
mime_type=file_content_type,
)
elif (
self.engine == "document_intelligence"
and self.kwargs.get("DOCUMENT_INTELLIGENCE_ENDPOINT") != ""
and self.kwargs.get("DOCUMENT_INTELLIGENCE_KEY") != ""
and (
file_ext in ["pdf", "xls", "xlsx", "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",
]
)
):
loader = AzureAIDocumentIntelligenceLoader(
file_path=file_path,
api_endpoint=self.kwargs.get("DOCUMENT_INTELLIGENCE_ENDPOINT"),
api_key=self.kwargs.get("DOCUMENT_INTELLIGENCE_KEY"),
)
else:
if file_ext == "pdf":
loader = PyPDFLoader(
@@ -1,13 +1,19 @@
import os
import logging
import torch
import numpy as np
from colbert.infra import ColBERTConfig
from colbert.modeling.checkpoint import Checkpoint
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class ColBERT:
def __init__(self, name, **kwargs) -> None:
print("ColBERT: Loading model", name)
log.info("ColBERT: Loading model", name)
self.device = "cuda" if torch.cuda.is_available() else "cpu"
DOCKER = kwargs.get("env") == "docker"
+88 -50
View File
@@ -5,6 +5,7 @@ from typing import Optional, Union
import asyncio
import requests
import hashlib
from huggingface_hub import snapshot_download
from langchain.retrievers import ContextualCompressionRetriever, EnsembleRetriever
@@ -14,8 +15,10 @@ from langchain_core.documents import Document
from open_webui.config import VECTOR_DB
from open_webui.retrieval.vector.connector import VECTOR_DB_CLIENT
from open_webui.utils.misc import get_last_user_message
from open_webui.utils.misc import get_last_user_message, calculate_sha256_string
from open_webui.models.users import UserModel
from open_webui.models.files import Files
from open_webui.env import (
SRC_LOG_LEVELS,
@@ -80,7 +83,7 @@ def query_doc(
return result
except Exception as e:
print(e)
log.exception(f"Error querying doc {collection_name} with limit {k}: {e}")
raise e
@@ -93,7 +96,7 @@ def get_doc(collection_name: str, user: UserModel = None):
return result
except Exception as e:
print(e)
log.exception(f"Error getting doc {collection_name}: {e}")
raise e
@@ -173,60 +176,41 @@ def merge_get_results(get_results: list[dict]) -> dict:
def merge_and_sort_query_results(
query_results: list[dict], k: int, reverse: bool = False
) -> list[dict]:
) -> dict:
# Initialize lists to store combined data
combined_distances = []
combined_documents = []
combined_metadatas = []
combined_ids = []
combined = []
seen_hashes = set() # To store unique document hashes
for data in query_results:
combined_distances.extend(data["distances"][0])
combined_documents.extend(data["documents"][0])
combined_metadatas.extend(data["metadatas"][0])
# DISTINCT(chunk_id,file_id) - in case if id (chunk_ids) become ordinals
combined_ids.extend(
[
f"{id}-{meta['file_id']}"
for id, meta in zip(data["ids"][0], data["metadatas"][0])
]
)
distances = data["distances"][0]
documents = data["documents"][0]
metadatas = data["metadatas"][0]
# Create a list of tuples (distance, document, metadata, ids)
combined = list(
zip(combined_distances, combined_documents, combined_metadatas, combined_ids)
)
for distance, document, metadata in zip(distances, documents, metadatas):
if isinstance(document, str):
doc_hash = hashlib.md5(
document.encode()
).hexdigest() # Compute a hash for uniqueness
if doc_hash not in seen_hashes:
seen_hashes.add(doc_hash)
combined.append((distance, document, metadata))
# Sort the list based on distances
combined.sort(key=lambda x: x[0], reverse=reverse)
sorted_distances = []
sorted_documents = []
sorted_metadatas = []
# Otherwise we don't have anything :-(
if combined:
# Unzip the sorted list
all_distances, all_documents, all_metadatas, all_ids = zip(*combined)
seen_ids = set()
# Slicing the lists to include only k elements
for index, id in enumerate(all_ids):
if id not in seen_ids:
sorted_distances.append(all_distances[index])
sorted_documents.append(all_documents[index])
sorted_metadatas.append(all_metadatas[index])
seen_ids.add(id)
if len(sorted_distances) >= k:
break
# Slice to keep only the top k elements
sorted_distances, sorted_documents, sorted_metadatas = (
zip(*combined[:k]) if combined else ([], [], [])
)
# Create the output dictionary
result = {
"distances": [sorted_distances],
"documents": [sorted_documents],
"metadatas": [sorted_metadatas],
# Create and return the output dictionary
return {
"distances": [list(sorted_distances)],
"documents": [list(sorted_documents)],
"metadatas": [list(sorted_metadatas)],
}
return result
def get_all_items_from_collections(collection_names: list[str]) -> dict:
results = []
@@ -355,6 +339,7 @@ def get_embedding_function(
def get_sources_from_files(
request,
files,
queries,
embedding_function,
@@ -372,19 +357,71 @@ def get_sources_from_files(
relevant_contexts = []
for file in files:
context = None
if file.get("docs"):
# BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL
context = {
"documents": [[doc.get("content") for doc in file.get("docs")]],
"metadatas": [[doc.get("metadata") for doc in file.get("docs")]],
}
elif file.get("context") == "full":
# Manual Full Mode Toggle
context = {
"documents": [[file.get("file").get("data", {}).get("content")]],
"metadatas": [[{"file_id": file.get("id"), "name": file.get("name")}]],
}
else:
context = None
elif (
file.get("type") != "web_search"
and request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL
):
# BYPASS_EMBEDDING_AND_RETRIEVAL
if file.get("type") == "collection":
file_ids = file.get("data", {}).get("file_ids", [])
documents = []
metadatas = []
for file_id in file_ids:
file_object = Files.get_file_by_id(file_id)
if file_object:
documents.append(file_object.data.get("content", ""))
metadatas.append(
{
"file_id": file_id,
"name": file_object.filename,
"source": file_object.filename,
}
)
context = {
"documents": [documents],
"metadatas": [metadatas],
}
elif file.get("id"):
file_object = Files.get_file_by_id(file.get("id"))
if file_object:
context = {
"documents": [[file_object.data.get("content", "")]],
"metadatas": [
[
{
"file_id": file.get("id"),
"name": file_object.filename,
"source": file_object.filename,
}
]
],
}
elif file.get("file").get("data"):
context = {
"documents": [[file.get("file").get("data", {}).get("content")]],
"metadatas": [
[file.get("file").get("data", {}).get("metadata", {})]
],
}
else:
collection_names = []
if file.get("type") == "collection":
if file.get("legacy"):
@@ -447,6 +484,7 @@ def get_sources_from_files(
if context:
if "data" in file:
del file["data"]
relevant_contexts.append({**context, "file": file})
sources = []
@@ -543,7 +581,7 @@ def generate_openai_batch_embeddings(
else:
raise "Something went wrong :/"
except Exception as e:
print(e)
log.exception(f"Error generating openai batch embeddings: {e}")
return None
@@ -577,7 +615,7 @@ def generate_ollama_batch_embeddings(
else:
raise "Something went wrong :/"
except Exception as e:
print(e)
log.exception(f"Error generating ollama batch embeddings: {e}")
return None
@@ -16,6 +16,10 @@ elif VECTOR_DB == "pgvector":
from open_webui.retrieval.vector.dbs.pgvector import PgvectorClient
VECTOR_DB_CLIENT = PgvectorClient()
elif VECTOR_DB == "elasticsearch":
from open_webui.retrieval.vector.dbs.elasticsearch import ElasticsearchClient
VECTOR_DB_CLIENT = ElasticsearchClient()
else:
from open_webui.retrieval.vector.dbs.chroma import ChromaClient
+6 -2
View File
@@ -1,4 +1,5 @@
import chromadb
import logging
from chromadb import Settings
from chromadb.utils.batch_utils import create_batches
@@ -16,6 +17,10 @@ from open_webui.config import (
CHROMA_CLIENT_AUTH_PROVIDER,
CHROMA_CLIENT_AUTH_CREDENTIALS,
)
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class ChromaClient:
@@ -102,8 +107,7 @@ class ChromaClient:
}
)
return None
except Exception as e:
print(e)
except:
return None
def get(self, collection_name: str) -> Optional[GetResult]:
@@ -0,0 +1,295 @@
from elasticsearch import Elasticsearch, BadRequestError
from typing import Optional
import ssl
from elasticsearch.helpers import bulk, scan
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
from open_webui.config import (
ELASTICSEARCH_URL,
ELASTICSEARCH_CA_CERTS,
ELASTICSEARCH_API_KEY,
ELASTICSEARCH_USERNAME,
ELASTICSEARCH_PASSWORD,
ELASTICSEARCH_CLOUD_ID,
ELASTICSEARCH_INDEX_PREFIX,
SSL_ASSERT_FINGERPRINT,
)
class ElasticsearchClient:
"""
Important:
in order to reduce the number of indexes and since the embedding vector length is fixed, we avoid creating
an index for each file but store it as a text field, while seperating to different index
baesd on the embedding length.
"""
def __init__(self):
self.index_prefix = ELASTICSEARCH_INDEX_PREFIX
self.client = Elasticsearch(
hosts=[ELASTICSEARCH_URL],
ca_certs=ELASTICSEARCH_CA_CERTS,
api_key=ELASTICSEARCH_API_KEY,
cloud_id=ELASTICSEARCH_CLOUD_ID,
basic_auth=(
(ELASTICSEARCH_USERNAME, ELASTICSEARCH_PASSWORD)
if ELASTICSEARCH_USERNAME and ELASTICSEARCH_PASSWORD
else None
),
ssl_assert_fingerprint=SSL_ASSERT_FINGERPRINT,
)
# Status: works
def _get_index_name(self, dimension: int) -> str:
return f"{self.index_prefix}_d{str(dimension)}"
# Status: works
def _scan_result_to_get_result(self, result) -> GetResult:
if not result:
return None
ids = []
documents = []
metadatas = []
for hit in result:
ids.append(hit["_id"])
documents.append(hit["_source"].get("text"))
metadatas.append(hit["_source"].get("metadata"))
return GetResult(ids=[ids], documents=[documents], metadatas=[metadatas])
# Status: works
def _result_to_get_result(self, result) -> GetResult:
if not result["hits"]["hits"]:
return None
ids = []
documents = []
metadatas = []
for hit in result["hits"]["hits"]:
ids.append(hit["_id"])
documents.append(hit["_source"].get("text"))
metadatas.append(hit["_source"].get("metadata"))
return GetResult(ids=[ids], documents=[documents], metadatas=[metadatas])
# Status: works
def _result_to_search_result(self, result) -> SearchResult:
ids = []
distances = []
documents = []
metadatas = []
for hit in result["hits"]["hits"]:
ids.append(hit["_id"])
distances.append(hit["_score"])
documents.append(hit["_source"].get("text"))
metadatas.append(hit["_source"].get("metadata"))
return SearchResult(
ids=[ids],
distances=[distances],
documents=[documents],
metadatas=[metadatas],
)
# Status: works
def _create_index(self, dimension: int):
body = {
"mappings": {
"dynamic_templates": [
{
"strings": {
"match_mapping_type": "string",
"mapping": {"type": "keyword"},
}
}
],
"properties": {
"collection": {"type": "keyword"},
"id": {"type": "keyword"},
"vector": {
"type": "dense_vector",
"dims": dimension, # Adjust based on your vector dimensions
"index": True,
"similarity": "cosine",
},
"text": {"type": "text"},
"metadata": {"type": "object"},
},
}
}
self.client.indices.create(index=self._get_index_name(dimension), body=body)
# Status: works
def _create_batches(self, items: list[VectorItem], batch_size=100):
for i in range(0, len(items), batch_size):
yield items[i : min(i + batch_size, len(items))]
# Status: works
def has_collection(self, collection_name) -> bool:
query_body = {"query": {"bool": {"filter": []}}}
query_body["query"]["bool"]["filter"].append(
{"term": {"collection": collection_name}}
)
try:
result = self.client.count(index=f"{self.index_prefix}*", body=query_body)
return result.body["count"] > 0
except Exception as e:
return None
def delete_collection(self, collection_name: str):
query = {"query": {"term": {"collection": collection_name}}}
self.client.delete_by_query(index=f"{self.index_prefix}*", body=query)
# Status: works
def search(
self, collection_name: str, vectors: list[list[float]], limit: int
) -> Optional[SearchResult]:
query = {
"size": limit,
"_source": ["text", "metadata"],
"query": {
"script_score": {
"query": {
"bool": {"filter": [{"term": {"collection": collection_name}}]}
},
"script": {
"source": "cosineSimilarity(params.vector, 'vector') + 1.0",
"params": {
"vector": vectors[0]
}, # Assuming single query vector
},
}
},
}
result = self.client.search(
index=self._get_index_name(len(vectors[0])), body=query
)
return self._result_to_search_result(result)
# Status: only tested halfwat
def query(
self, collection_name: str, filter: dict, limit: Optional[int] = None
) -> Optional[GetResult]:
if not self.has_collection(collection_name):
return None
query_body = {
"query": {"bool": {"filter": []}},
"_source": ["text", "metadata"],
}
for field, value in filter.items():
query_body["query"]["bool"]["filter"].append({"term": {field: value}})
query_body["query"]["bool"]["filter"].append(
{"term": {"collection": collection_name}}
)
size = limit if limit else 10
try:
result = self.client.search(
index=f"{self.index_prefix}*",
body=query_body,
size=size,
)
return self._result_to_get_result(result)
except Exception as e:
return None
# Status: works
def _has_index(self, dimension: int):
return self.client.indices.exists(
index=self._get_index_name(dimension=dimension)
)
def get_or_create_index(self, dimension: int):
if not self._has_index(dimension=dimension):
self._create_index(dimension=dimension)
# Status: works
def get(self, collection_name: str) -> Optional[GetResult]:
# Get all the items in the collection.
query = {
"query": {"bool": {"filter": [{"term": {"collection": collection_name}}]}},
"_source": ["text", "metadata"],
}
results = list(scan(self.client, index=f"{self.index_prefix}*", query=query))
return self._scan_result_to_get_result(results)
# Status: works
def insert(self, collection_name: str, items: list[VectorItem]):
if not self._has_index(dimension=len(items[0]["vector"])):
self._create_index(dimension=len(items[0]["vector"]))
for batch in self._create_batches(items):
actions = [
{
"_index": self._get_index_name(dimension=len(items[0]["vector"])),
"_id": item["id"],
"_source": {
"collection": collection_name,
"vector": item["vector"],
"text": item["text"],
"metadata": item["metadata"],
},
}
for item in batch
]
bulk(self.client, actions)
# Upsert documents using the update API with doc_as_upsert=True.
def upsert(self, collection_name: str, items: list[VectorItem]):
if not self._has_index(dimension=len(items[0]["vector"])):
self._create_index(dimension=len(items[0]["vector"]))
for batch in self._create_batches(items):
actions = [
{
"_op_type": "update",
"_index": self._get_index_name(dimension=len(item["vector"])),
"_id": item["id"],
"doc": {
"collection": collection_name,
"vector": item["vector"],
"text": item["text"],
"metadata": item["metadata"],
},
"doc_as_upsert": True,
}
for item in batch
]
bulk(self.client, actions)
# Delete specific documents from a collection by filtering on both collection and document IDs.
def delete(
self,
collection_name: str,
ids: Optional[list[str]] = None,
filter: Optional[dict] = None,
):
query = {
"query": {"bool": {"filter": [{"term": {"collection": collection_name}}]}}
}
# logic based on chromaDB
if ids:
query["query"]["bool"]["filter"].append({"terms": {"_id": ids}})
elif filter:
for field, value in filter.items():
query["query"]["bool"]["filter"].append(
{"term": {f"metadata.{field}": value}}
)
self.client.delete_by_query(index=f"{self.index_prefix}*", body=query)
def reset(self):
indices = self.client.indices.get(index=f"{self.index_prefix}*")
for index in indices:
self.client.indices.delete(index=index)
@@ -1,7 +1,7 @@
from pymilvus import MilvusClient as Client
from pymilvus import FieldSchema, DataType
import json
import logging
from typing import Optional
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
@@ -10,15 +10,19 @@ from open_webui.config import (
MILVUS_DB,
MILVUS_TOKEN,
)
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class MilvusClient:
def __init__(self):
self.collection_prefix = "open_webui"
if MILVUS_TOKEN is None:
self.client = Client(uri=MILVUS_URI, database=MILVUS_DB)
self.client = Client(uri=MILVUS_URI, db_name=MILVUS_DB)
else:
self.client = Client(uri=MILVUS_URI, database=MILVUS_DB, token=MILVUS_TOKEN)
self.client = Client(uri=MILVUS_URI, db_name=MILVUS_DB, token=MILVUS_TOKEN)
def _result_to_get_result(self, result) -> GetResult:
ids = []
@@ -168,7 +172,7 @@ class MilvusClient:
try:
# Loop until there are no more items to fetch or the desired limit is reached
while remaining > 0:
print("remaining", remaining)
log.info(f"remaining: {remaining}")
current_fetch = min(
max_limit, remaining
) # Determine how many items to fetch in this iteration
@@ -195,10 +199,12 @@ class MilvusClient:
if results_count < current_fetch:
break
print(all_results)
log.debug(all_results)
return self._result_to_get_result([all_results])
except Exception as e:
print(e)
log.exception(
f"Error querying collection {collection_name} with limit {limit}: {e}"
)
return None
def get(self, collection_name: str) -> Optional[GetResult]:
@@ -49,7 +49,7 @@ class OpenSearchClient:
ids=ids, distances=distances, documents=documents, metadatas=metadatas
)
def _create_index(self, index_name: str, dimension: int):
def _create_index(self, collection_name: str, dimension: int):
body = {
"mappings": {
"properties": {
@@ -72,24 +72,28 @@ class OpenSearchClient:
}
}
}
self.client.indices.create(index=f"{self.index_prefix}_{index_name}", body=body)
self.client.indices.create(
index=f"{self.index_prefix}_{collection_name}", body=body
)
def _create_batches(self, items: list[VectorItem], batch_size=100):
for i in range(0, len(items), batch_size):
yield items[i : i + batch_size]
def has_collection(self, index_name: str) -> bool:
def has_collection(self, collection_name: str) -> bool:
# has_collection here means has index.
# We are simply adapting to the norms of the other DBs.
return self.client.indices.exists(index=f"{self.index_prefix}_{index_name}")
return self.client.indices.exists(
index=f"{self.index_prefix}_{collection_name}"
)
def delete_colleciton(self, index_name: str):
def delete_colleciton(self, collection_name: str):
# delete_collection here means delete index.
# We are simply adapting to the norms of the other DBs.
self.client.indices.delete(index=f"{self.index_prefix}_{index_name}")
self.client.indices.delete(index=f"{self.index_prefix}_{collection_name}")
def search(
self, index_name: str, vectors: list[list[float]], limit: int
self, collection_name: str, vectors: list[list[float]], limit: int
) -> Optional[SearchResult]:
query = {
"size": limit,
@@ -108,7 +112,7 @@ class OpenSearchClient:
}
result = self.client.search(
index=f"{self.index_prefix}_{index_name}", body=query
index=f"{self.index_prefix}_{collection_name}", body=query
)
return self._result_to_search_result(result)
@@ -141,21 +145,22 @@ class OpenSearchClient:
except Exception as e:
return None
def get_or_create_index(self, index_name: str, dimension: int):
if not self.has_index(index_name):
self._create_index(index_name, dimension)
def _create_index_if_not_exists(self, collection_name: str, dimension: int):
if not self.has_index(collection_name):
self._create_index(collection_name, dimension)
def get(self, index_name: str) -> Optional[GetResult]:
def get(self, collection_name: str) -> Optional[GetResult]:
query = {"query": {"match_all": {}}, "_source": ["text", "metadata"]}
result = self.client.search(
index=f"{self.index_prefix}_{index_name}", body=query
index=f"{self.index_prefix}_{collection_name}", body=query
)
return self._result_to_get_result(result)
def insert(self, index_name: str, items: list[VectorItem]):
if not self.has_index(index_name):
self._create_index(index_name, dimension=len(items[0]["vector"]))
def insert(self, collection_name: str, items: list[VectorItem]):
self._create_index_if_not_exists(
collection_name=collection_name, dimension=len(items[0]["vector"])
)
for batch in self._create_batches(items):
actions = [
@@ -173,15 +178,17 @@ class OpenSearchClient:
]
self.client.bulk(actions)
def upsert(self, index_name: str, items: list[VectorItem]):
if not self.has_index(index_name):
self._create_index(index_name, dimension=len(items[0]["vector"]))
def upsert(self, collection_name: str, items: list[VectorItem]):
self._create_index_if_not_exists(
collection_name=collection_name, dimension=len(items[0]["vector"])
)
for batch in self._create_batches(items):
actions = [
{
"index": {
"_id": item["id"],
"_index": f"{self.index_prefix}_{collection_name}",
"_source": {
"vector": item["vector"],
"text": item["text"],
@@ -193,9 +200,9 @@ class OpenSearchClient:
]
self.client.bulk(actions)
def delete(self, index_name: str, ids: list[str]):
def delete(self, collection_name: str, ids: list[str]):
actions = [
{"delete": {"_index": f"{self.index_prefix}_{index_name}", "_id": id}}
{"delete": {"_index": f"{self.index_prefix}_{collection_name}", "_id": id}}
for id in ids
]
self.client.bulk(body=actions)
@@ -1,4 +1,5 @@
from typing import Optional, List, Dict, Any
import logging
from sqlalchemy import (
cast,
column,
@@ -24,9 +25,14 @@ from sqlalchemy.exc import NoSuchTableError
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
from open_webui.config import PGVECTOR_DB_URL, PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH
from open_webui.env import SRC_LOG_LEVELS
VECTOR_LENGTH = PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH
Base = declarative_base()
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class DocumentChunk(Base):
__tablename__ = "document_chunk"
@@ -82,10 +88,10 @@ class PgvectorClient:
)
)
self.session.commit()
print("Initialization complete.")
log.info("Initialization complete.")
except Exception as e:
self.session.rollback()
print(f"Error during initialization: {e}")
log.exception(f"Error during initialization: {e}")
raise
def check_vector_length(self) -> None:
@@ -150,12 +156,12 @@ class PgvectorClient:
new_items.append(new_chunk)
self.session.bulk_save_objects(new_items)
self.session.commit()
print(
log.info(
f"Inserted {len(new_items)} items into collection '{collection_name}'."
)
except Exception as e:
self.session.rollback()
print(f"Error during insert: {e}")
log.exception(f"Error during insert: {e}")
raise
def upsert(self, collection_name: str, items: List[VectorItem]) -> None:
@@ -184,10 +190,12 @@ class PgvectorClient:
)
self.session.add(new_chunk)
self.session.commit()
print(f"Upserted {len(items)} items into collection '{collection_name}'.")
log.info(
f"Upserted {len(items)} items into collection '{collection_name}'."
)
except Exception as e:
self.session.rollback()
print(f"Error during upsert: {e}")
log.exception(f"Error during upsert: {e}")
raise
def search(
@@ -278,7 +286,7 @@ class PgvectorClient:
ids=ids, distances=distances, documents=documents, metadatas=metadatas
)
except Exception as e:
print(f"Error during search: {e}")
log.exception(f"Error during search: {e}")
return None
def query(
@@ -310,7 +318,7 @@ class PgvectorClient:
metadatas=metadatas,
)
except Exception as e:
print(f"Error during query: {e}")
log.exception(f"Error during query: {e}")
return None
def get(
@@ -334,7 +342,7 @@ class PgvectorClient:
return GetResult(ids=ids, documents=documents, metadatas=metadatas)
except Exception as e:
print(f"Error during get: {e}")
log.exception(f"Error during get: {e}")
return None
def delete(
@@ -356,22 +364,22 @@ class PgvectorClient:
)
deleted = query.delete(synchronize_session=False)
self.session.commit()
print(f"Deleted {deleted} items from collection '{collection_name}'.")
log.info(f"Deleted {deleted} items from collection '{collection_name}'.")
except Exception as e:
self.session.rollback()
print(f"Error during delete: {e}")
log.exception(f"Error during delete: {e}")
raise
def reset(self) -> None:
try:
deleted = self.session.query(DocumentChunk).delete()
self.session.commit()
print(
log.info(
f"Reset complete. Deleted {deleted} items from 'document_chunk' table."
)
except Exception as e:
self.session.rollback()
print(f"Error during reset: {e}")
log.exception(f"Error during reset: {e}")
raise
def close(self) -> None:
@@ -387,9 +395,9 @@ class PgvectorClient:
)
return exists
except Exception as e:
print(f"Error checking collection existence: {e}")
log.exception(f"Error checking collection existence: {e}")
return False
def delete_collection(self, collection_name: str) -> None:
self.delete(collection_name)
print(f"Collection '{collection_name}' deleted.")
log.info(f"Collection '{collection_name}' deleted.")
@@ -1,4 +1,5 @@
from typing import Optional
import logging
from qdrant_client import QdrantClient as Qclient
from qdrant_client.http.models import PointStruct
@@ -6,9 +7,13 @@ from qdrant_client.models import models
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
from open_webui.config import QDRANT_URI, QDRANT_API_KEY
from open_webui.env import SRC_LOG_LEVELS
NO_LIMIT = 999999999
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class QdrantClient:
def __init__(self):
@@ -49,7 +54,7 @@ class QdrantClient:
),
)
print(f"collection {collection_name_with_prefix} successfully created!")
log.info(f"collection {collection_name_with_prefix} successfully created!")
def _create_collection_if_not_exists(self, collection_name, dimension):
if not self.has_collection(collection_name=collection_name):
@@ -120,7 +125,7 @@ class QdrantClient:
)
return self._result_to_get_result(points.points)
except Exception as e:
print(e)
log.exception(f"Error querying a collection '{collection_name}': {e}")
return None
def get(self, collection_name: str) -> Optional[GetResult]:
@@ -0,0 +1,87 @@
import logging
from typing import Optional, List
import requests
from open_webui.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_perplexity(
api_key: str,
query: str,
count: int,
filter_list: Optional[list[str]] = None,
) -> list[SearchResult]:
"""Search using Perplexity API and return the results as a list of SearchResult objects.
Args:
api_key (str): A Perplexity API key
query (str): The query to search for
count (int): Maximum number of results to return
"""
# Handle PersistentConfig object
if hasattr(api_key, "__str__"):
api_key = str(api_key)
try:
url = "https://api.perplexity.ai/chat/completions"
# Create payload for the API call
payload = {
"model": "sonar",
"messages": [
{
"role": "system",
"content": "You are a search assistant. Provide factual information with citations.",
},
{"role": "user", "content": query},
],
"temperature": 0.2, # Lower temperature for more factual responses
"stream": False,
}
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
# Make the API request
response = requests.request("POST", url, json=payload, headers=headers)
# Parse the JSON response
json_response = response.json()
# Extract citations from the response
citations = json_response.get("citations", [])
# Create search results from citations
results = []
for i, citation in enumerate(citations[:count]):
# Extract content from the response to use as snippet
content = ""
if "choices" in json_response and json_response["choices"]:
if i == 0:
content = json_response["choices"][0]["message"]["content"]
result = {"link": citation, "title": f"Source {i+1}", "snippet": content}
results.append(result)
if filter_list:
results = get_filtered_results(results, filter_list)
return [
SearchResult(
link=result["link"], title=result["title"], snippet=result["snippet"]
)
for result in results[:count]
]
except Exception as e:
log.error(f"Error searching with Perplexity API: {e}")
return []
+51 -14
View File
@@ -54,7 +54,7 @@ MAX_FILE_SIZE = MAX_FILE_SIZE_MB * 1024 * 1024 # Convert MB to bytes
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["AUDIO"])
SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/")
SPEECH_CACHE_DIR = CACHE_DIR / "audio" / "speech"
SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
@@ -71,7 +71,7 @@ from pydub.utils import mediainfo
def is_mp4_audio(file_path):
"""Check if the given file is an MP4 audio file."""
if not os.path.isfile(file_path):
print(f"File not found: {file_path}")
log.error(f"File not found: {file_path}")
return False
info = mediainfo(file_path)
@@ -88,7 +88,7 @@ def convert_mp4_to_wav(file_path, output_path):
"""Convert MP4 audio file to WAV format."""
audio = AudioSegment.from_file(file_path, format="mp4")
audio.export(output_path, format="wav")
print(f"Converted {file_path} to {output_path}")
log.info(f"Converted {file_path} to {output_path}")
def set_faster_whisper_model(model: str, auto_update: bool = False):
@@ -266,7 +266,6 @@ async def speech(request: Request, user=Depends(get_verified_user)):
payload["model"] = request.app.state.config.TTS_MODEL
try:
# print(payload)
timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
async with aiohttp.ClientSession(
timeout=timeout, trust_env=True
@@ -468,7 +467,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
def transcribe(request: Request, file_path):
print("transcribe", file_path)
log.info(f"transcribe: {file_path}")
filename = os.path.basename(file_path)
file_dir = os.path.dirname(file_path)
id = filename.split(".")[0]
@@ -680,7 +679,22 @@ def transcription(
def get_available_models(request: Request) -> list[dict]:
available_models = []
if request.app.state.config.TTS_ENGINE == "openai":
available_models = [{"id": "tts-1"}, {"id": "tts-1-hd"}]
# Use custom endpoint if not using the official OpenAI API URL
if not request.app.state.config.TTS_OPENAI_API_BASE_URL.startswith(
"https://api.openai.com"
):
try:
response = requests.get(
f"{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/models"
)
response.raise_for_status()
data = response.json()
available_models = data.get("models", [])
except Exception as e:
log.error(f"Error fetching models from custom endpoint: {str(e)}")
available_models = [{"id": "tts-1"}, {"id": "tts-1-hd"}]
else:
available_models = [{"id": "tts-1"}, {"id": "tts-1-hd"}]
elif request.app.state.config.TTS_ENGINE == "elevenlabs":
try:
response = requests.get(
@@ -711,14 +725,37 @@ def get_available_voices(request) -> dict:
"""Returns {voice_id: voice_name} dict"""
available_voices = {}
if request.app.state.config.TTS_ENGINE == "openai":
available_voices = {
"alloy": "alloy",
"echo": "echo",
"fable": "fable",
"onyx": "onyx",
"nova": "nova",
"shimmer": "shimmer",
}
# Use custom endpoint if not using the official OpenAI API URL
if not request.app.state.config.TTS_OPENAI_API_BASE_URL.startswith(
"https://api.openai.com"
):
try:
response = requests.get(
f"{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/voices"
)
response.raise_for_status()
data = response.json()
voices_list = data.get("voices", [])
available_voices = {voice["id"]: voice["name"] for voice in voices_list}
except Exception as e:
log.error(f"Error fetching voices from custom endpoint: {str(e)}")
available_voices = {
"alloy": "alloy",
"echo": "echo",
"fable": "fable",
"onyx": "onyx",
"nova": "nova",
"shimmer": "shimmer",
}
else:
available_voices = {
"alloy": "alloy",
"echo": "echo",
"fable": "fable",
"onyx": "onyx",
"nova": "nova",
"shimmer": "shimmer",
}
elif request.app.state.config.TTS_ENGINE == "elevenlabs":
try:
available_voices = get_elevenlabs_voices(
+18 -26
View File
@@ -31,10 +31,7 @@ from open_webui.env import (
)
from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.responses import RedirectResponse, Response
from open_webui.config import (
OPENID_PROVIDER_URL,
ENABLE_OAUTH_SIGNUP,
)
from open_webui.config import OPENID_PROVIDER_URL, ENABLE_OAUTH_SIGNUP, ENABLE_LDAP
from pydantic import BaseModel
from open_webui.utils.misc import parse_duration, validate_email_format
from open_webui.utils.auth import (
@@ -51,8 +48,10 @@ from open_webui.utils.access_control import get_permissions
from typing import Optional, List
from ssl import CERT_REQUIRED, PROTOCOL_TLS
from ldap3 import Server, Connection, NONE, Tls
from ldap3.utils.conv import escape_filter_chars
if ENABLE_LDAP.value:
from ldap3 import Server, Connection, NONE, Tls
from ldap3.utils.conv import escape_filter_chars
router = APIRouter()
@@ -231,9 +230,12 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
entry = connection_app.entries[0]
username = str(entry[f"{LDAP_ATTRIBUTE_FOR_USERNAME}"]).lower()
mail = str(entry[f"{LDAP_ATTRIBUTE_FOR_MAIL}"])
if not mail or mail == "" or mail == "[]":
raise HTTPException(400, f"User {form_data.user} does not have mail.")
email = str(entry[f"{LDAP_ATTRIBUTE_FOR_MAIL}"])
if not email or email == "" or email == "[]":
raise HTTPException(400, f"User {form_data.user} does not have email.")
else:
email = email.lower()
cn = str(entry["cn"])
user_dn = entry.entry_dn
@@ -248,18 +250,10 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
if not connection_user.bind():
raise HTTPException(400, f"Authentication failed for {form_data.user}")
user = Users.get_user_by_email(mail)
user = Users.get_user_by_email(email)
if not user:
try:
user_count = Users.get_num_users()
if (
request.app.state.USER_COUNT
and user_count >= request.app.state.USER_COUNT
):
raise HTTPException(
status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
role = (
"admin"
@@ -268,7 +262,10 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
)
user = Auths.insert_new_auth(
email=mail, password=str(uuid.uuid4()), name=cn, role=role
email=email,
password=str(uuid.uuid4()),
name=cn,
role=role,
)
if not user:
@@ -281,7 +278,7 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
except Exception as err:
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
user = Auths.authenticate_user_by_trusted_header(mail)
user = Auths.authenticate_user_by_trusted_header(email)
if user:
token = create_token(
@@ -439,11 +436,6 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
)
user_count = Users.get_num_users()
if request.app.state.USER_COUNT and user_count >= request.app.state.USER_COUNT:
raise HTTPException(
status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
)
if not validate_email_format(form_data.email.lower()):
raise HTTPException(
status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT
@@ -613,7 +605,7 @@ async def get_admin_details(request: Request, user=Depends(get_current_user)):
admin_email = request.app.state.config.ADMIN_EMAIL
admin_name = None
print(admin_email, admin_name)
log.info(f"Admin details - Email: {admin_email}, Name: {admin_name}")
if admin_email:
admin = Users.get_user_by_email(admin_email)
+5
View File
@@ -70,6 +70,7 @@ async def set_direct_connections_config(
# CodeInterpreterConfig
############################
class CodeInterpreterConfigForm(BaseModel):
ENABLE_CODE_EXECUTION: bool
CODE_EXECUTION_ENGINE: str
CODE_EXECUTION_JUPYTER_URL: Optional[str]
CODE_EXECUTION_JUPYTER_AUTH: Optional[str]
@@ -89,6 +90,7 @@ class CodeInterpreterConfigForm(BaseModel):
@router.get("/code_execution", response_model=CodeInterpreterConfigForm)
async def get_code_execution_config(request: Request, user=Depends(get_admin_user)):
return {
"ENABLE_CODE_EXECUTION": request.app.state.config.ENABLE_CODE_EXECUTION,
"CODE_EXECUTION_ENGINE": request.app.state.config.CODE_EXECUTION_ENGINE,
"CODE_EXECUTION_JUPYTER_URL": request.app.state.config.CODE_EXECUTION_JUPYTER_URL,
"CODE_EXECUTION_JUPYTER_AUTH": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH,
@@ -111,6 +113,8 @@ async def set_code_execution_config(
request: Request, form_data: CodeInterpreterConfigForm, user=Depends(get_admin_user)
):
request.app.state.config.ENABLE_CODE_EXECUTION = form_data.ENABLE_CODE_EXECUTION
request.app.state.config.CODE_EXECUTION_ENGINE = form_data.CODE_EXECUTION_ENGINE
request.app.state.config.CODE_EXECUTION_JUPYTER_URL = (
form_data.CODE_EXECUTION_JUPYTER_URL
@@ -153,6 +157,7 @@ async def set_code_execution_config(
)
return {
"ENABLE_CODE_EXECUTION": request.app.state.config.ENABLE_CODE_EXECUTION,
"CODE_EXECUTION_ENGINE": request.app.state.config.CODE_EXECUTION_ENGINE,
"CODE_EXECUTION_JUPYTER_URL": request.app.state.config.CODE_EXECUTION_JUPYTER_URL,
"CODE_EXECUTION_JUPYTER_AUTH": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH,
+18 -2
View File
@@ -16,6 +16,7 @@ from open_webui.models.files import (
Files,
)
from open_webui.routers.retrieval import ProcessFileForm, process_file
from open_webui.routers.audio import transcribe
from open_webui.storage.provider import Storage
from open_webui.utils.auth import get_admin_user, get_verified_user
from pydantic import BaseModel
@@ -67,7 +68,22 @@ def upload_file(
)
try:
process_file(request, ProcessFileForm(file_id=id), user=user)
if file.content_type in [
"audio/mpeg",
"audio/wav",
"audio/ogg",
"audio/x-m4a",
]:
file_path = Storage.get_file(file_path)
result = transcribe(request, file_path)
process_file(
request,
ProcessFileForm(file_id=id, content=result.get("text", "")),
user=user,
)
else:
process_file(request, ProcessFileForm(file_id=id), user=user)
file_item = Files.get_file_by_id(id=id)
except Exception as e:
log.exception(e)
@@ -273,7 +289,7 @@ async def get_html_file_content_by_id(id: str, user=Depends(get_verified_user)):
# Check if the file already exists in the cache
if file_path.is_file():
print(f"file_path: {file_path}")
log.info(f"file_path: {file_path}")
return FileResponse(file_path)
else:
raise HTTPException(
+11 -5
View File
@@ -1,4 +1,5 @@
import os
import logging
from pathlib import Path
from typing import Optional
@@ -13,6 +14,11 @@ from open_webui.config import CACHE_DIR
from open_webui.constants import ERROR_MESSAGES
from fastapi import APIRouter, Depends, HTTPException, Request, status
from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MAIN"])
router = APIRouter()
@@ -68,7 +74,7 @@ async def create_new_function(
function = Functions.insert_new_function(user.id, function_type, form_data)
function_cache_dir = Path(CACHE_DIR) / "functions" / form_data.id
function_cache_dir = CACHE_DIR / "functions" / form_data.id
function_cache_dir.mkdir(parents=True, exist_ok=True)
if function:
@@ -79,7 +85,7 @@ async def create_new_function(
detail=ERROR_MESSAGES.DEFAULT("Error creating function"),
)
except Exception as e:
print(e)
log.exception(f"Failed to create a new function: {e}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
@@ -183,7 +189,7 @@ async def update_function_by_id(
FUNCTIONS[id] = function_module
updated = {**form_data.model_dump(exclude={"id"}), "type": function_type}
print(updated)
log.debug(updated)
function = Functions.update_function_by_id(id, updated)
@@ -299,7 +305,7 @@ async def update_function_valves_by_id(
Functions.update_function_valves_by_id(id, valves.model_dump())
return valves.model_dump()
except Exception as e:
print(e)
log.exception(f"Error updating function values by id {id}: {e}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
@@ -388,7 +394,7 @@ async def update_function_user_valves_by_id(
)
return user_valves.model_dump()
except Exception as e:
print(e)
log.exception(f"Error updating function user valves by id {id}: {e}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
+11 -5
View File
@@ -1,7 +1,7 @@
import os
from pathlib import Path
from typing import Optional
import logging
from open_webui.models.users import Users
from open_webui.models.groups import (
@@ -14,7 +14,13 @@ from open_webui.models.groups import (
from open_webui.config import CACHE_DIR
from open_webui.constants import ERROR_MESSAGES
from fastapi import APIRouter, Depends, HTTPException, Request, status
from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MAIN"])
router = APIRouter()
@@ -37,7 +43,7 @@ async def get_groups(user=Depends(get_verified_user)):
@router.post("/create", response_model=Optional[GroupResponse])
async def create_new_function(form_data: GroupForm, user=Depends(get_admin_user)):
async def create_new_group(form_data: GroupForm, user=Depends(get_admin_user)):
try:
group = Groups.insert_new_group(user.id, form_data)
if group:
@@ -48,7 +54,7 @@ async def create_new_function(form_data: GroupForm, user=Depends(get_admin_user)
detail=ERROR_MESSAGES.DEFAULT("Error creating group"),
)
except Exception as e:
print(e)
log.exception(f"Error creating a new group: {e}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
@@ -94,7 +100,7 @@ async def update_group_by_id(
detail=ERROR_MESSAGES.DEFAULT("Error updating group"),
)
except Exception as e:
print(e)
log.exception(f"Error updating group {id}: {e}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
@@ -118,7 +124,7 @@ async def delete_group_by_id(id: str, user=Depends(get_admin_user)):
detail=ERROR_MESSAGES.DEFAULT("Error deleting group"),
)
except Exception as e:
print(e)
log.exception(f"Error deleting group {id}: {e}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
+20 -4
View File
@@ -25,7 +25,7 @@ from pydantic import BaseModel
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["IMAGES"])
IMAGE_CACHE_DIR = Path(CACHE_DIR).joinpath("./image/generations/")
IMAGE_CACHE_DIR = CACHE_DIR / "image" / "generations"
IMAGE_CACHE_DIR.mkdir(parents=True, exist_ok=True)
@@ -144,6 +144,8 @@ async def update_config(
request.app.state.config.COMFYUI_BASE_URL = (
form_data.comfyui.COMFYUI_BASE_URL.strip("/")
)
request.app.state.config.COMFYUI_API_KEY = form_data.comfyui.COMFYUI_API_KEY
request.app.state.config.COMFYUI_WORKFLOW = form_data.comfyui.COMFYUI_WORKFLOW
request.app.state.config.COMFYUI_WORKFLOW_NODES = (
form_data.comfyui.COMFYUI_WORKFLOW_NODES
@@ -203,9 +205,17 @@ async def verify_url(request: Request, user=Depends(get_admin_user)):
request.app.state.config.ENABLE_IMAGE_GENERATION = False
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.INVALID_URL)
elif request.app.state.config.IMAGE_GENERATION_ENGINE == "comfyui":
headers = None
if request.app.state.config.COMFYUI_API_KEY:
headers = {
"Authorization": f"Bearer {request.app.state.config.COMFYUI_API_KEY}"
}
try:
r = requests.get(
url=f"{request.app.state.config.COMFYUI_BASE_URL}/object_info"
url=f"{request.app.state.config.COMFYUI_BASE_URL}/object_info",
headers=headers,
)
r.raise_for_status()
return True
@@ -351,7 +361,7 @@ def get_models(request: Request, user=Depends(get_verified_user)):
if model_node_id:
model_list_key = None
print(workflow[model_node_id]["class_type"])
log.info(workflow[model_node_id]["class_type"])
for key in info[workflow[model_node_id]["class_type"]]["input"][
"required"
]:
@@ -507,7 +517,13 @@ async def image_generations(
images = []
for image in res["data"]:
image_data, content_type = load_b64_image_data(image["b64_json"])
if "url" in image:
image_data, content_type = load_url_image_data(
image["url"], headers
)
else:
image_data, content_type = load_b64_image_data(image["b64_json"])
url = upload_image(request, data, image_data, content_type, user)
images.append({"url": url})
return images
+1 -1
View File
@@ -614,7 +614,7 @@ def add_files_to_knowledge_batch(
)
# Get files content
print(f"files/batch/add - {len(form_data)} files")
log.info(f"files/batch/add - {len(form_data)} files")
files: List[FileModel] = []
for form in form_data:
file = Files.get_file_by_id(form.file_id)
+133 -19
View File
@@ -14,6 +14,11 @@ from urllib.parse import urlparse
import aiohttp
from aiocache import cached
import requests
from open_webui.models.users import UserModel
from open_webui.env import (
ENABLE_FORWARD_USER_INFO_HEADERS,
)
from fastapi import (
Depends,
@@ -50,7 +55,7 @@ from open_webui.env import (
ENV,
SRC_LOG_LEVELS,
AIOHTTP_CLIENT_TIMEOUT,
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST,
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST,
BYPASS_MODEL_ACCESS_CONTROL,
)
from open_webui.constants import ERROR_MESSAGES
@@ -66,12 +71,26 @@ log.setLevel(SRC_LOG_LEVELS["OLLAMA"])
##########################################
async def send_get_request(url, key=None):
timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST)
async def send_get_request(url, key=None, user: UserModel = None):
timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
try:
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
async with session.get(
url, headers={**({"Authorization": f"Bearer {key}"} if key else {})}
url,
headers={
"Content-Type": "application/json",
**({"Authorization": f"Bearer {key}"} if key else {}),
**(
{
"X-OpenWebUI-User-Name": user.name,
"X-OpenWebUI-User-Id": user.id,
"X-OpenWebUI-User-Email": user.email,
"X-OpenWebUI-User-Role": user.role,
}
if ENABLE_FORWARD_USER_INFO_HEADERS and user
else {}
),
},
) as response:
return await response.json()
except Exception as e:
@@ -96,6 +115,7 @@ async def send_post_request(
stream: bool = True,
key: Optional[str] = None,
content_type: Optional[str] = None,
user: UserModel = None,
):
r = None
@@ -110,6 +130,16 @@ async def send_post_request(
headers={
"Content-Type": "application/json",
**({"Authorization": f"Bearer {key}"} if key else {}),
**(
{
"X-OpenWebUI-User-Name": user.name,
"X-OpenWebUI-User-Id": user.id,
"X-OpenWebUI-User-Email": user.email,
"X-OpenWebUI-User-Role": user.role,
}
if ENABLE_FORWARD_USER_INFO_HEADERS and user
else {}
),
},
)
r.raise_for_status()
@@ -186,12 +216,24 @@ async def verify_connection(
key = form_data.key
async with aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST)
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
) as session:
try:
async with session.get(
f"{url}/api/version",
headers={**({"Authorization": f"Bearer {key}"} if key else {})},
headers={
**({"Authorization": f"Bearer {key}"} if key else {}),
**(
{
"X-OpenWebUI-User-Name": user.name,
"X-OpenWebUI-User-Id": user.id,
"X-OpenWebUI-User-Email": user.email,
"X-OpenWebUI-User-Role": user.role,
}
if ENABLE_FORWARD_USER_INFO_HEADERS and user
else {}
),
},
) as r:
if r.status != 200:
detail = f"HTTP Error: {r.status}"
@@ -254,7 +296,7 @@ async def update_config(
@cached(ttl=3)
async def get_all_models(request: Request):
async def get_all_models(request: Request, user: UserModel = None):
log.info("get_all_models()")
if request.app.state.config.ENABLE_OLLAMA_API:
request_tasks = []
@@ -262,7 +304,7 @@ async def get_all_models(request: Request):
if (str(idx) not in request.app.state.config.OLLAMA_API_CONFIGS) and (
url not in request.app.state.config.OLLAMA_API_CONFIGS # Legacy support
):
request_tasks.append(send_get_request(f"{url}/api/tags"))
request_tasks.append(send_get_request(f"{url}/api/tags", user=user))
else:
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(
str(idx),
@@ -275,7 +317,9 @@ async def get_all_models(request: Request):
key = api_config.get("key", None)
if enable:
request_tasks.append(send_get_request(f"{url}/api/tags", key))
request_tasks.append(
send_get_request(f"{url}/api/tags", key, user=user)
)
else:
request_tasks.append(asyncio.ensure_future(asyncio.sleep(0, None)))
@@ -360,7 +404,7 @@ async def get_ollama_tags(
models = []
if url_idx is None:
models = await get_all_models(request)
models = await get_all_models(request, user=user)
else:
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS)
@@ -370,7 +414,19 @@ async def get_ollama_tags(
r = requests.request(
method="GET",
url=f"{url}/api/tags",
headers={**({"Authorization": f"Bearer {key}"} if key else {})},
headers={
**({"Authorization": f"Bearer {key}"} if key else {}),
**(
{
"X-OpenWebUI-User-Name": user.name,
"X-OpenWebUI-User-Id": user.id,
"X-OpenWebUI-User-Email": user.email,
"X-OpenWebUI-User-Role": user.role,
}
if ENABLE_FORWARD_USER_INFO_HEADERS and user
else {}
),
},
)
r.raise_for_status()
@@ -477,6 +533,7 @@ async def get_ollama_loaded_models(request: Request, user=Depends(get_verified_u
url, {}
), # Legacy support
).get("key", None),
user=user,
)
for idx, url in enumerate(request.app.state.config.OLLAMA_BASE_URLS)
]
@@ -509,6 +566,7 @@ async def pull_model(
url=f"{url}/api/pull",
payload=json.dumps(payload),
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
user=user,
)
@@ -527,7 +585,7 @@ async def push_model(
user=Depends(get_admin_user),
):
if url_idx is None:
await get_all_models(request)
await get_all_models(request, user=user)
models = request.app.state.OLLAMA_MODELS
if form_data.name in models:
@@ -545,6 +603,7 @@ async def push_model(
url=f"{url}/api/push",
payload=form_data.model_dump_json(exclude_none=True).encode(),
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
user=user,
)
@@ -571,6 +630,7 @@ async def create_model(
url=f"{url}/api/create",
payload=form_data.model_dump_json(exclude_none=True).encode(),
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
user=user,
)
@@ -588,7 +648,7 @@ async def copy_model(
user=Depends(get_admin_user),
):
if url_idx is None:
await get_all_models(request)
await get_all_models(request, user=user)
models = request.app.state.OLLAMA_MODELS
if form_data.source in models:
@@ -609,6 +669,16 @@ async def copy_model(
headers={
"Content-Type": "application/json",
**({"Authorization": f"Bearer {key}"} if key else {}),
**(
{
"X-OpenWebUI-User-Name": user.name,
"X-OpenWebUI-User-Id": user.id,
"X-OpenWebUI-User-Email": user.email,
"X-OpenWebUI-User-Role": user.role,
}
if ENABLE_FORWARD_USER_INFO_HEADERS and user
else {}
),
},
data=form_data.model_dump_json(exclude_none=True).encode(),
)
@@ -643,7 +713,7 @@ async def delete_model(
user=Depends(get_admin_user),
):
if url_idx is None:
await get_all_models(request)
await get_all_models(request, user=user)
models = request.app.state.OLLAMA_MODELS
if form_data.name in models:
@@ -665,6 +735,16 @@ async def delete_model(
headers={
"Content-Type": "application/json",
**({"Authorization": f"Bearer {key}"} if key else {}),
**(
{
"X-OpenWebUI-User-Name": user.name,
"X-OpenWebUI-User-Id": user.id,
"X-OpenWebUI-User-Email": user.email,
"X-OpenWebUI-User-Role": user.role,
}
if ENABLE_FORWARD_USER_INFO_HEADERS and user
else {}
),
},
)
r.raise_for_status()
@@ -693,7 +773,7 @@ async def delete_model(
async def show_model_info(
request: Request, form_data: ModelNameForm, user=Depends(get_verified_user)
):
await get_all_models(request)
await get_all_models(request, user=user)
models = request.app.state.OLLAMA_MODELS
if form_data.name not in models:
@@ -714,6 +794,16 @@ async def show_model_info(
headers={
"Content-Type": "application/json",
**({"Authorization": f"Bearer {key}"} if key else {}),
**(
{
"X-OpenWebUI-User-Name": user.name,
"X-OpenWebUI-User-Id": user.id,
"X-OpenWebUI-User-Email": user.email,
"X-OpenWebUI-User-Role": user.role,
}
if ENABLE_FORWARD_USER_INFO_HEADERS and user
else {}
),
},
data=form_data.model_dump_json(exclude_none=True).encode(),
)
@@ -757,7 +847,7 @@ async def embed(
log.info(f"generate_ollama_batch_embeddings {form_data}")
if url_idx is None:
await get_all_models(request)
await get_all_models(request, user=user)
models = request.app.state.OLLAMA_MODELS
model = form_data.model
@@ -783,6 +873,16 @@ async def embed(
headers={
"Content-Type": "application/json",
**({"Authorization": f"Bearer {key}"} if key else {}),
**(
{
"X-OpenWebUI-User-Name": user.name,
"X-OpenWebUI-User-Id": user.id,
"X-OpenWebUI-User-Email": user.email,
"X-OpenWebUI-User-Role": user.role,
}
if ENABLE_FORWARD_USER_INFO_HEADERS and user
else {}
),
},
data=form_data.model_dump_json(exclude_none=True).encode(),
)
@@ -826,7 +926,7 @@ async def embeddings(
log.info(f"generate_ollama_embeddings {form_data}")
if url_idx is None:
await get_all_models(request)
await get_all_models(request, user=user)
models = request.app.state.OLLAMA_MODELS
model = form_data.model
@@ -852,6 +952,16 @@ async def embeddings(
headers={
"Content-Type": "application/json",
**({"Authorization": f"Bearer {key}"} if key else {}),
**(
{
"X-OpenWebUI-User-Name": user.name,
"X-OpenWebUI-User-Id": user.id,
"X-OpenWebUI-User-Email": user.email,
"X-OpenWebUI-User-Role": user.role,
}
if ENABLE_FORWARD_USER_INFO_HEADERS and user
else {}
),
},
data=form_data.model_dump_json(exclude_none=True).encode(),
)
@@ -901,7 +1011,7 @@ async def generate_completion(
user=Depends(get_verified_user),
):
if url_idx is None:
await get_all_models(request)
await get_all_models(request, user=user)
models = request.app.state.OLLAMA_MODELS
model = form_data.model
@@ -931,6 +1041,7 @@ async def generate_completion(
url=f"{url}/api/generate",
payload=form_data.model_dump_json(exclude_none=True).encode(),
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
user=user,
)
@@ -1060,6 +1171,7 @@ async def generate_chat_completion(
stream=form_data.stream,
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
content_type="application/x-ndjson",
user=user,
)
@@ -1162,6 +1274,7 @@ async def generate_openai_completion(
payload=json.dumps(payload),
stream=payload.get("stream", False),
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
user=user,
)
@@ -1240,6 +1353,7 @@ async def generate_openai_chat_completion(
payload=json.dumps(payload),
stream=payload.get("stream", False),
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
user=user,
)
@@ -1253,7 +1367,7 @@ async def get_openai_models(
models = []
if url_idx is None:
model_list = await get_all_models(request)
model_list = await get_all_models(request, user=user)
models = [
{
"id": model["model"],
+49 -18
View File
@@ -22,10 +22,11 @@ from open_webui.config import (
)
from open_webui.env import (
AIOHTTP_CLIENT_TIMEOUT,
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST,
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST,
ENABLE_FORWARD_USER_INFO_HEADERS,
BYPASS_MODEL_ACCESS_CONTROL,
)
from open_webui.models.users import UserModel
from open_webui.constants import ERROR_MESSAGES
from open_webui.env import ENV, SRC_LOG_LEVELS
@@ -51,12 +52,25 @@ log.setLevel(SRC_LOG_LEVELS["OPENAI"])
##########################################
async def send_get_request(url, key=None):
timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST)
async def send_get_request(url, key=None, user: UserModel = None):
timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
try:
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
async with session.get(
url, headers={**({"Authorization": f"Bearer {key}"} if key else {})}
url,
headers={
**({"Authorization": f"Bearer {key}"} if key else {}),
**(
{
"X-OpenWebUI-User-Name": user.name,
"X-OpenWebUI-User-Id": user.id,
"X-OpenWebUI-User-Email": user.email,
"X-OpenWebUI-User-Role": user.role,
}
if ENABLE_FORWARD_USER_INFO_HEADERS and user
else {}
),
},
) as response:
return await response.json()
except Exception as e:
@@ -84,9 +98,15 @@ def openai_o1_o3_handler(payload):
payload["max_completion_tokens"] = payload["max_tokens"]
del payload["max_tokens"]
# Fix: O1 does not support the "system" parameter, Modify "system" to "user"
# Fix: o1 and o3 do not support the "system" role directly.
# For older models like "o1-mini" or "o1-preview", use role "user".
# For newer o1/o3 models, replace "system" with "developer".
if payload["messages"][0]["role"] == "system":
payload["messages"][0]["role"] = "user"
model_lower = payload["model"].lower()
if model_lower.startswith("o1-mini") or model_lower.startswith("o1-preview"):
payload["messages"][0]["role"] = "user"
else:
payload["messages"][0]["role"] = "developer"
return payload
@@ -172,7 +192,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
body = await request.body()
name = hashlib.sha256(body).hexdigest()
SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/")
SPEECH_CACHE_DIR = CACHE_DIR / "audio" / "speech"
SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
file_path = SPEECH_CACHE_DIR.joinpath(f"{name}.mp3")
file_body_path = SPEECH_CACHE_DIR.joinpath(f"{name}.json")
@@ -247,7 +267,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
raise HTTPException(status_code=401, detail=ERROR_MESSAGES.OPENAI_NOT_FOUND)
async def get_all_models_responses(request: Request) -> list:
async def get_all_models_responses(request: Request, user: UserModel) -> list:
if not request.app.state.config.ENABLE_OPENAI_API:
return []
@@ -271,7 +291,9 @@ async def get_all_models_responses(request: Request) -> list:
):
request_tasks.append(
send_get_request(
f"{url}/models", request.app.state.config.OPENAI_API_KEYS[idx]
f"{url}/models",
request.app.state.config.OPENAI_API_KEYS[idx],
user=user,
)
)
else:
@@ -291,6 +313,7 @@ async def get_all_models_responses(request: Request) -> list:
send_get_request(
f"{url}/models",
request.app.state.config.OPENAI_API_KEYS[idx],
user=user,
)
)
else:
@@ -352,13 +375,13 @@ async def get_filtered_models(models, user):
@cached(ttl=3)
async def get_all_models(request: Request) -> dict[str, list]:
async def get_all_models(request: Request, user: UserModel) -> dict[str, list]:
log.info("get_all_models()")
if not request.app.state.config.ENABLE_OPENAI_API:
return {"data": []}
responses = await get_all_models_responses(request)
responses = await get_all_models_responses(request, user=user)
def extract_data(response):
if response and "data" in response:
@@ -418,16 +441,14 @@ async def get_models(
}
if url_idx is None:
models = await get_all_models(request)
models = await get_all_models(request, user=user)
else:
url = request.app.state.config.OPENAI_API_BASE_URLS[url_idx]
key = request.app.state.config.OPENAI_API_KEYS[url_idx]
r = None
async with aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(
total=AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST
)
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
) as session:
try:
async with session.get(
@@ -507,7 +528,7 @@ async def verify_connection(
key = form_data.key
async with aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST)
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
) as session:
try:
async with session.get(
@@ -515,6 +536,16 @@ async def verify_connection(
headers={
"Authorization": f"Bearer {key}",
"Content-Type": "application/json",
**(
{
"X-OpenWebUI-User-Name": user.name,
"X-OpenWebUI-User-Id": user.id,
"X-OpenWebUI-User-Email": user.email,
"X-OpenWebUI-User-Role": user.role,
}
if ENABLE_FORWARD_USER_INFO_HEADERS
else {}
),
},
) as r:
if r.status != 200:
@@ -587,7 +618,7 @@ async def generate_chat_completion(
detail="Model not found",
)
await get_all_models(request)
await get_all_models(request, user=user)
model = request.app.state.OPENAI_MODELS.get(model_id)
if model:
idx = model["urlIdx"]
@@ -777,7 +808,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
if r is not None:
try:
res = await r.json()
print(res)
log.error(res)
if "error" in res:
detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
except Exception:
+11 -11
View File
@@ -101,7 +101,7 @@ async def process_pipeline_inlet_filter(request, payload, user, models):
if "detail" in res:
raise Exception(response.status, res["detail"])
except Exception as e:
print(f"Connection error: {e}")
log.exception(f"Connection error: {e}")
return payload
@@ -153,7 +153,7 @@ async def process_pipeline_outlet_filter(request, payload, user, models):
except Exception:
pass
except Exception as e:
print(f"Connection error: {e}")
log.exception(f"Connection error: {e}")
return payload
@@ -169,7 +169,7 @@ router = APIRouter()
@router.get("/list")
async def get_pipelines_list(request: Request, user=Depends(get_admin_user)):
responses = await get_all_models_responses(request)
responses = await get_all_models_responses(request, user)
log.debug(f"get_pipelines_list: get_openai_models_responses returned {responses}")
urlIdxs = [
@@ -196,7 +196,7 @@ async def upload_pipeline(
file: UploadFile = File(...),
user=Depends(get_admin_user),
):
print("upload_pipeline", urlIdx, file.filename)
log.info(f"upload_pipeline: urlIdx={urlIdx}, filename={file.filename}")
# Check if the uploaded file is a python file
if not (file.filename and file.filename.endswith(".py")):
raise HTTPException(
@@ -231,7 +231,7 @@ async def upload_pipeline(
return {**data}
except Exception as e:
# Handle connection error here
print(f"Connection error: {e}")
log.exception(f"Connection error: {e}")
detail = None
status_code = status.HTTP_404_NOT_FOUND
@@ -282,7 +282,7 @@ async def add_pipeline(
return {**data}
except Exception as e:
# Handle connection error here
print(f"Connection error: {e}")
log.exception(f"Connection error: {e}")
detail = None
if r is not None:
@@ -327,7 +327,7 @@ async def delete_pipeline(
return {**data}
except Exception as e:
# Handle connection error here
print(f"Connection error: {e}")
log.exception(f"Connection error: {e}")
detail = None
if r is not None:
@@ -361,7 +361,7 @@ async def get_pipelines(
return {**data}
except Exception as e:
# Handle connection error here
print(f"Connection error: {e}")
log.exception(f"Connection error: {e}")
detail = None
if r is not None:
@@ -400,7 +400,7 @@ async def get_pipeline_valves(
return {**data}
except Exception as e:
# Handle connection error here
print(f"Connection error: {e}")
log.exception(f"Connection error: {e}")
detail = None
if r is not None:
@@ -440,7 +440,7 @@ async def get_pipeline_valves_spec(
return {**data}
except Exception as e:
# Handle connection error here
print(f"Connection error: {e}")
log.exception(f"Connection error: {e}")
detail = None
if r is not None:
@@ -482,7 +482,7 @@ async def update_pipeline_valves(
return {**data}
except Exception as e:
# Handle connection error here
print(f"Connection error: {e}")
log.exception(f"Connection error: {e}")
detail = None
+122 -43
View File
@@ -59,7 +59,7 @@ from open_webui.retrieval.web.serpstack import search_serpstack
from open_webui.retrieval.web.tavily import search_tavily
from open_webui.retrieval.web.bing import search_bing
from open_webui.retrieval.web.exa import search_exa
from open_webui.retrieval.web.perplexity import search_perplexity
from open_webui.retrieval.utils import (
get_embedding_function,
@@ -352,10 +352,16 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
"status": True,
"pdf_extract_images": request.app.state.config.PDF_EXTRACT_IMAGES,
"RAG_FULL_CONTEXT": request.app.state.config.RAG_FULL_CONTEXT,
"BYPASS_EMBEDDING_AND_RETRIEVAL": request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL,
"enable_google_drive_integration": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
"enable_onedrive_integration": request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION,
"content_extraction": {
"engine": request.app.state.config.CONTENT_EXTRACTION_ENGINE,
"tika_server_url": request.app.state.config.TIKA_SERVER_URL,
"document_intelligence_config": {
"endpoint": request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT,
"key": request.app.state.config.DOCUMENT_INTELLIGENCE_KEY,
},
},
"chunk": {
"text_splitter": request.app.state.config.TEXT_SPLITTER,
@@ -373,10 +379,11 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
},
"web": {
"ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION": request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
"RAG_WEB_SEARCH_FULL_CONTEXT": request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT,
"BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL": request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL,
"search": {
"enabled": request.app.state.config.ENABLE_RAG_WEB_SEARCH,
"drive": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
"onedrive": request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION,
"engine": request.app.state.config.RAG_WEB_SEARCH_ENGINE,
"searxng_query_url": request.app.state.config.SEARXNG_QUERY_URL,
"google_pse_api_key": request.app.state.config.GOOGLE_PSE_API_KEY,
@@ -398,7 +405,9 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
"bing_search_v7_endpoint": request.app.state.config.BING_SEARCH_V7_ENDPOINT,
"bing_search_v7_subscription_key": request.app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY,
"exa_api_key": request.app.state.config.EXA_API_KEY,
"perplexity_api_key": request.app.state.config.PERPLEXITY_API_KEY,
"result_count": request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
"trust_env": request.app.state.config.RAG_WEB_SEARCH_TRUST_ENV,
"concurrent_requests": request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
"domain_filter_list": request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
},
@@ -411,9 +420,15 @@ class FileConfig(BaseModel):
max_count: Optional[int] = None
class DocumentIntelligenceConfigForm(BaseModel):
endpoint: str
key: str
class ContentExtractionConfig(BaseModel):
engine: str = ""
tika_server_url: Optional[str] = None
document_intelligence_config: Optional[DocumentIntelligenceConfigForm] = None
class ChunkParamUpdateForm(BaseModel):
@@ -451,6 +466,7 @@ class WebSearchConfig(BaseModel):
bing_search_v7_endpoint: Optional[str] = None
bing_search_v7_subscription_key: Optional[str] = None
exa_api_key: Optional[str] = None
perplexity_api_key: Optional[str] = None
result_count: Optional[int] = None
concurrent_requests: Optional[int] = None
trust_env: Optional[bool] = None
@@ -460,13 +476,15 @@ class WebSearchConfig(BaseModel):
class WebConfig(BaseModel):
search: WebSearchConfig
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION: Optional[bool] = None
RAG_WEB_SEARCH_FULL_CONTEXT: Optional[bool] = None
BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL: Optional[bool] = None
class ConfigUpdateForm(BaseModel):
RAG_FULL_CONTEXT: Optional[bool] = None
BYPASS_EMBEDDING_AND_RETRIEVAL: Optional[bool] = None
pdf_extract_images: Optional[bool] = None
enable_google_drive_integration: Optional[bool] = None
enable_onedrive_integration: Optional[bool] = None
file: Optional[FileConfig] = None
content_extraction: Optional[ContentExtractionConfig] = None
chunk: Optional[ChunkParamUpdateForm] = None
@@ -490,24 +508,45 @@ async def update_rag_config(
else request.app.state.config.RAG_FULL_CONTEXT
)
request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL = (
form_data.BYPASS_EMBEDDING_AND_RETRIEVAL
if form_data.BYPASS_EMBEDDING_AND_RETRIEVAL is not None
else request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL
)
request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = (
form_data.enable_google_drive_integration
if form_data.enable_google_drive_integration is not None
else request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION
)
request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION = (
form_data.enable_onedrive_integration
if form_data.enable_onedrive_integration is not None
else request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION
)
if form_data.file is not None:
request.app.state.config.FILE_MAX_SIZE = form_data.file.max_size
request.app.state.config.FILE_MAX_COUNT = form_data.file.max_count
if form_data.content_extraction is not None:
log.info(f"Updating text settings: {form_data.content_extraction}")
log.info(
f"Updating content extraction: {request.app.state.config.CONTENT_EXTRACTION_ENGINE} to {form_data.content_extraction.engine}"
)
request.app.state.config.CONTENT_EXTRACTION_ENGINE = (
form_data.content_extraction.engine
)
request.app.state.config.TIKA_SERVER_URL = (
form_data.content_extraction.tika_server_url
)
if form_data.content_extraction.document_intelligence_config is not None:
request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT = (
form_data.content_extraction.document_intelligence_config.endpoint
)
request.app.state.config.DOCUMENT_INTELLIGENCE_KEY = (
form_data.content_extraction.document_intelligence_config.key
)
if form_data.chunk is not None:
request.app.state.config.TEXT_SPLITTER = form_data.chunk.text_splitter
@@ -528,8 +567,8 @@ async def update_rag_config(
request.app.state.config.ENABLE_RAG_WEB_SEARCH = form_data.web.search.enabled
request.app.state.config.RAG_WEB_SEARCH_ENGINE = form_data.web.search.engine
request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT = (
form_data.web.RAG_WEB_SEARCH_FULL_CONTEXT
request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = (
form_data.web.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL
)
request.app.state.config.SEARXNG_QUERY_URL = (
@@ -580,6 +619,10 @@ async def update_rag_config(
request.app.state.config.EXA_API_KEY = form_data.web.search.exa_api_key
request.app.state.config.PERPLEXITY_API_KEY = (
form_data.web.search.perplexity_api_key
)
request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = (
form_data.web.search.result_count
)
@@ -597,6 +640,7 @@ async def update_rag_config(
"status": True,
"pdf_extract_images": request.app.state.config.PDF_EXTRACT_IMAGES,
"RAG_FULL_CONTEXT": request.app.state.config.RAG_FULL_CONTEXT,
"BYPASS_EMBEDDING_AND_RETRIEVAL": request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL,
"file": {
"max_size": request.app.state.config.FILE_MAX_SIZE,
"max_count": request.app.state.config.FILE_MAX_COUNT,
@@ -604,6 +648,10 @@ async def update_rag_config(
"content_extraction": {
"engine": request.app.state.config.CONTENT_EXTRACTION_ENGINE,
"tika_server_url": request.app.state.config.TIKA_SERVER_URL,
"document_intelligence_config": {
"endpoint": request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT,
"key": request.app.state.config.DOCUMENT_INTELLIGENCE_KEY,
},
},
"chunk": {
"text_splitter": request.app.state.config.TEXT_SPLITTER,
@@ -617,7 +665,7 @@ async def update_rag_config(
},
"web": {
"ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION": request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
"RAG_WEB_SEARCH_FULL_CONTEXT": request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT,
"BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL": request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL,
"search": {
"enabled": request.app.state.config.ENABLE_RAG_WEB_SEARCH,
"engine": request.app.state.config.RAG_WEB_SEARCH_ENGINE,
@@ -641,6 +689,7 @@ async def update_rag_config(
"bing_search_v7_endpoint": request.app.state.config.BING_SEARCH_V7_ENDPOINT,
"bing_search_v7_subscription_key": request.app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY,
"exa_api_key": request.app.state.config.EXA_API_KEY,
"perplexity_api_key": request.app.state.config.PERPLEXITY_API_KEY,
"result_count": request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
"concurrent_requests": request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
"trust_env": request.app.state.config.RAG_WEB_SEARCH_TRUST_ENV,
@@ -880,7 +929,12 @@ def process_file(
# Update the content in the file
# Usage: /files/{file_id}/data/content/update
VECTOR_DB_CLIENT.delete_collection(collection_name=f"file-{file.id}")
try:
# /files/{file_id}/data/content/update
VECTOR_DB_CLIENT.delete_collection(collection_name=f"file-{file.id}")
except:
# Audio file upload pipeline
pass
docs = [
Document(
@@ -937,6 +991,8 @@ def process_file(
engine=request.app.state.config.CONTENT_EXTRACTION_ENGINE,
TIKA_SERVER_URL=request.app.state.config.TIKA_SERVER_URL,
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,
)
docs = loader.load(
file.filename, file.meta.get("content_type"), file_path
@@ -979,36 +1035,45 @@ def process_file(
hash = calculate_sha256_string(text_content)
Files.update_file_hash_by_id(file.id, hash)
try:
result = save_docs_to_vector_db(
request,
docs=docs,
collection_name=collection_name,
metadata={
"file_id": file.id,
"name": file.filename,
"hash": hash,
},
add=(True if form_data.collection_name else False),
user=user,
)
if result:
Files.update_file_metadata_by_id(
file.id,
{
"collection_name": collection_name,
if not request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL:
try:
result = save_docs_to_vector_db(
request,
docs=docs,
collection_name=collection_name,
metadata={
"file_id": file.id,
"name": file.filename,
"hash": hash,
},
add=(True if form_data.collection_name else False),
user=user,
)
return {
"status": True,
"collection_name": collection_name,
"filename": file.filename,
"content": text_content,
}
except Exception as e:
raise e
if result:
Files.update_file_metadata_by_id(
file.id,
{
"collection_name": collection_name,
},
)
return {
"status": True,
"collection_name": collection_name,
"filename": file.filename,
"content": text_content,
}
except Exception as e:
raise e
else:
return {
"status": True,
"collection_name": None,
"filename": file.filename,
"content": text_content,
}
except Exception as e:
log.exception(e)
if "No pandoc was found" in str(e):
@@ -1124,9 +1189,13 @@ def process_web(
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
)
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
)
else:
collection_name = None
return {
"status": True,
@@ -1138,6 +1207,7 @@ def process_web(
},
"meta": {
"name": form_data.url,
"source": form_data.url,
},
},
}
@@ -1163,6 +1233,7 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
- SERPLY_API_KEY
- TAVILY_API_KEY
- EXA_API_KEY
- PERPLEXITY_API_KEY
- SEARCHAPI_API_KEY + SEARCHAPI_ENGINE (by default `google`)
- SERPAPI_API_KEY + SERPAPI_ENGINE (by default `google`)
Args:
@@ -1327,6 +1398,13 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
)
elif engine == "perplexity":
return search_perplexity(
request.app.state.config.PERPLEXITY_API_KEY,
query,
request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
)
else:
raise Exception("No search engine API key found in environment variables")
@@ -1368,9 +1446,11 @@ async def process_web_search(
)
docs = await loader.aload()
if request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT:
if request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL:
return {
"status": True,
"collection_name": None,
"filenames": urls,
"docs": [
{
"content": doc.page_content,
@@ -1378,7 +1458,6 @@ async def process_web_search(
}
for doc in docs
],
"filenames": urls,
"loaded_count": len(docs),
}
else:
@@ -1553,11 +1632,11 @@ def reset_upload_dir(user=Depends(get_admin_user)) -> bool:
elif os.path.isdir(file_path):
shutil.rmtree(file_path) # Remove the directory
except Exception as e:
print(f"Failed to delete {file_path}. Reason: {e}")
log.exception(f"Failed to delete {file_path}. Reason: {e}")
else:
print(f"The directory {folder} does not exist")
log.warning(f"The directory {folder} does not exist")
except Exception as e:
print(f"Failed to process the directory {folder}. Reason: {e}")
log.exception(f"Failed to process the directory {folder}. Reason: {e}")
return True
+46
View File
@@ -20,6 +20,10 @@ from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.constants import TASKS
from open_webui.routers.pipelines import process_pipeline_inlet_filter
from open_webui.utils.filter import (
get_sorted_filter_ids,
process_filter_functions,
)
from open_webui.utils.task import get_task_model_id
from open_webui.config import (
@@ -221,6 +225,12 @@ async def generate_title(
},
}
# Process the payload through the pipeline
try:
payload = await process_pipeline_inlet_filter(request, payload, user, models)
except Exception as e:
raise e
try:
return await generate_chat_completion(request, form_data=payload, user=user)
except Exception as e:
@@ -290,6 +300,12 @@ async def generate_chat_tags(
},
}
# Process the payload through the pipeline
try:
payload = await process_pipeline_inlet_filter(request, payload, user, models)
except Exception as e:
raise e
try:
return await generate_chat_completion(request, form_data=payload, user=user)
except Exception as e:
@@ -356,6 +372,12 @@ async def generate_image_prompt(
},
}
# Process the payload through the pipeline
try:
payload = await process_pipeline_inlet_filter(request, payload, user, models)
except Exception as e:
raise e
try:
return await generate_chat_completion(request, form_data=payload, user=user)
except Exception as e:
@@ -433,6 +455,12 @@ async def generate_queries(
},
}
# Process the payload through the pipeline
try:
payload = await process_pipeline_inlet_filter(request, payload, user, models)
except Exception as e:
raise e
try:
return await generate_chat_completion(request, form_data=payload, user=user)
except Exception as e:
@@ -514,6 +542,12 @@ async def generate_autocompletion(
},
}
# Process the payload through the pipeline
try:
payload = await process_pipeline_inlet_filter(request, payload, user, models)
except Exception as e:
raise e
try:
return await generate_chat_completion(request, form_data=payload, user=user)
except Exception as e:
@@ -584,6 +618,12 @@ async def generate_emoji(
},
}
# Process the payload through the pipeline
try:
payload = await process_pipeline_inlet_filter(request, payload, user, models)
except Exception as e:
raise e
try:
return await generate_chat_completion(request, form_data=payload, user=user)
except Exception as e:
@@ -644,6 +684,12 @@ async def generate_moa_response(
},
}
# Process the payload through the pipeline
try:
payload = await process_pipeline_inlet_filter(request, payload, user, models)
except Exception as e:
raise e
try:
return await generate_chat_completion(request, form_data=payload, user=user)
except Exception as e:
+10 -5
View File
@@ -1,3 +1,4 @@
import logging
from pathlib import Path
from typing import Optional
@@ -15,6 +16,10 @@ from fastapi import APIRouter, Depends, HTTPException, Request, status
from open_webui.utils.tools import get_tools_specs
from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.utils.access_control import has_access, has_permission
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MAIN"])
router = APIRouter()
@@ -100,7 +105,7 @@ async def create_new_tools(
specs = get_tools_specs(TOOLS[form_data.id])
tools = Tools.insert_new_tool(user.id, form_data, specs)
tool_cache_dir = Path(CACHE_DIR) / "tools" / form_data.id
tool_cache_dir = CACHE_DIR / "tools" / form_data.id
tool_cache_dir.mkdir(parents=True, exist_ok=True)
if tools:
@@ -111,7 +116,7 @@ async def create_new_tools(
detail=ERROR_MESSAGES.DEFAULT("Error creating tools"),
)
except Exception as e:
print(e)
log.exception(f"Failed to load the tool by id {form_data.id}: {e}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(str(e)),
@@ -193,7 +198,7 @@ async def update_tools_by_id(
"specs": specs,
}
print(updated)
log.debug(updated)
tools = Tools.update_tool_by_id(id, updated)
if tools:
@@ -343,7 +348,7 @@ async def update_tools_valves_by_id(
Tools.update_tool_valves_by_id(id, valves.model_dump())
return valves.model_dump()
except Exception as e:
print(e)
log.exception(f"Failed to update tool valves by id {id}: {e}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(str(e)),
@@ -421,7 +426,7 @@ async def update_tools_user_valves_by_id(
)
return user_valves.model_dump()
except Exception as e:
print(e)
log.exception(f"Failed to update user valves by id {id}: {e}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(str(e)),
+6 -1
View File
@@ -1,4 +1,5 @@
import black
import logging
import markdown
from open_webui.models.chats import ChatTitleMessagesForm
@@ -13,8 +14,12 @@ from open_webui.utils.misc import get_gravatar_url
from open_webui.utils.pdf_generator import PDFGenerator
from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.utils.code_interpreter import execute_code_jupyter
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MAIN"])
router = APIRouter()
@@ -96,7 +101,7 @@ async def download_chat_as_pdf(
headers={"Content-Disposition": "attachment;filename=chat.pdf"},
)
except Exception as e:
print(e)
log.exception(f"Error generating PDF: {e}")
raise HTTPException(status_code=400, detail=str(e))
+2 -1
View File
@@ -12,6 +12,7 @@ from open_webui.env import (
ENABLE_WEBSOCKET_SUPPORT,
WEBSOCKET_MANAGER,
WEBSOCKET_REDIS_URL,
WEBSOCKET_REDIS_LOCK_TIMEOUT,
)
from open_webui.utils.auth import decode_token
from open_webui.socket.utils import RedisDict, RedisLock
@@ -61,7 +62,7 @@ if WEBSOCKET_MANAGER == "redis":
clean_up_lock = RedisLock(
redis_url=WEBSOCKET_REDIS_URL,
lock_name="usage_cleanup_lock",
timeout_secs=TIMEOUT_DURATION * 2,
timeout_secs=WEBSOCKET_REDIS_LOCK_TIMEOUT,
)
aquire_func = clean_up_lock.aquire_lock
renew_func = clean_up_lock.renew_lock
Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 306 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 303 KiB

@@ -3,13 +3,13 @@
"short_name": "WebUI",
"icons": [
{
"src": "/favicon/web-app-manifest-192x192.png",
"src": "/static/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/favicon/web-app-manifest-512x512.png",
"src": "/static/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

+38 -9
View File
@@ -1,10 +1,12 @@
import os
import shutil
import json
import logging
from abc import ABC, abstractmethod
from typing import BinaryIO, Tuple
import boto3
from botocore.config import Config
from botocore.exceptions import ClientError
from open_webui.config import (
S3_ACCESS_KEY_ID,
@@ -13,6 +15,8 @@ from open_webui.config import (
S3_KEY_PREFIX,
S3_REGION_NAME,
S3_SECRET_ACCESS_KEY,
S3_USE_ACCELERATE_ENDPOINT,
S3_ADDRESSING_STYLE,
GCS_BUCKET_NAME,
GOOGLE_APPLICATION_CREDENTIALS_JSON,
AZURE_STORAGE_ENDPOINT,
@@ -27,6 +31,11 @@ from open_webui.constants import ERROR_MESSAGES
from azure.identity import DefaultAzureCredential
from azure.storage.blob import BlobServiceClient
from azure.core.exceptions import ResourceNotFoundError
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MAIN"])
class StorageProvider(ABC):
@@ -71,7 +80,7 @@ class LocalStorageProvider(StorageProvider):
if os.path.isfile(file_path):
os.remove(file_path)
else:
print(f"File {file_path} not found in local storage.")
log.warning(f"File {file_path} not found in local storage.")
@staticmethod
def delete_all_files() -> None:
@@ -85,20 +94,40 @@ class LocalStorageProvider(StorageProvider):
elif os.path.isdir(file_path):
shutil.rmtree(file_path) # Remove the directory
except Exception as e:
print(f"Failed to delete {file_path}. Reason: {e}")
log.exception(f"Failed to delete {file_path}. Reason: {e}")
else:
print(f"Directory {UPLOAD_DIR} not found in local storage.")
log.warning(f"Directory {UPLOAD_DIR} not found in local storage.")
class S3StorageProvider(StorageProvider):
def __init__(self):
self.s3_client = boto3.client(
"s3",
region_name=S3_REGION_NAME,
endpoint_url=S3_ENDPOINT_URL,
aws_access_key_id=S3_ACCESS_KEY_ID,
aws_secret_access_key=S3_SECRET_ACCESS_KEY,
config = Config(
s3={
"use_accelerate_endpoint": S3_USE_ACCELERATE_ENDPOINT,
"addressing_style": S3_ADDRESSING_STYLE,
},
)
# If access key and secret are provided, use them for authentication
if S3_ACCESS_KEY_ID and S3_SECRET_ACCESS_KEY:
self.s3_client = boto3.client(
"s3",
region_name=S3_REGION_NAME,
endpoint_url=S3_ENDPOINT_URL,
aws_access_key_id=S3_ACCESS_KEY_ID,
aws_secret_access_key=S3_SECRET_ACCESS_KEY,
config=config,
)
else:
# If no explicit credentials are provided, fall back to default AWS credentials
# This supports workload identity (IAM roles for EC2, EKS, etc.)
self.s3_client = boto3.client(
"s3",
region_name=S3_REGION_NAME,
endpoint_url=S3_ENDPOINT_URL,
config=config,
)
self.bucket_name = S3_BUCKET_NAME
self.key_prefix = S3_KEY_PREFIX if S3_KEY_PREFIX else ""
@@ -187,6 +187,17 @@ class TestS3StorageProvider:
assert not (upload_dir / self.filename).exists()
assert not (upload_dir / self.filename_extra).exists()
def test_init_without_credentials(self, monkeypatch):
"""Test that S3StorageProvider can initialize without explicit credentials."""
# Temporarily unset the environment variables
monkeypatch.setattr(provider, "S3_ACCESS_KEY_ID", None)
monkeypatch.setattr(provider, "S3_SECRET_ACCESS_KEY", None)
# Should not raise an exception
storage = provider.S3StorageProvider()
assert storage.s3_client is not None
assert storage.bucket_name == provider.S3_BUCKET_NAME
class TestGCSStorageProvider:
Storage = provider.GCSStorageProvider()
+249
View File
@@ -0,0 +1,249 @@
from contextlib import asynccontextmanager
from dataclasses import asdict, dataclass
from enum import Enum
import re
from typing import (
TYPE_CHECKING,
Any,
AsyncGenerator,
Dict,
MutableMapping,
Optional,
cast,
)
import uuid
from asgiref.typing import (
ASGI3Application,
ASGIReceiveCallable,
ASGIReceiveEvent,
ASGISendCallable,
ASGISendEvent,
Scope as ASGIScope,
)
from loguru import logger
from starlette.requests import Request
from open_webui.env import AUDIT_LOG_LEVEL, MAX_BODY_LOG_SIZE
from open_webui.utils.auth import get_current_user, get_http_authorization_cred
from open_webui.models.users import UserModel
if TYPE_CHECKING:
from loguru import Logger
@dataclass(frozen=True)
class AuditLogEntry:
# `Metadata` audit level properties
id: str
user: dict[str, Any]
audit_level: str
verb: str
request_uri: str
user_agent: Optional[str] = None
source_ip: Optional[str] = None
# `Request` audit level properties
request_object: Any = None
# `Request Response` level
response_object: Any = None
response_status_code: Optional[int] = None
class AuditLevel(str, Enum):
NONE = "NONE"
METADATA = "METADATA"
REQUEST = "REQUEST"
REQUEST_RESPONSE = "REQUEST_RESPONSE"
class AuditLogger:
"""
A helper class that encapsulates audit logging functionality. It uses Logurus logger with an auditable binding to ensure that audit log entries are filtered correctly.
Parameters:
logger (Logger): An instance of Logurus logger.
"""
def __init__(self, logger: "Logger"):
self.logger = logger.bind(auditable=True)
def write(
self,
audit_entry: AuditLogEntry,
*,
log_level: str = "INFO",
extra: Optional[dict] = None,
):
entry = asdict(audit_entry)
if extra:
entry["extra"] = extra
self.logger.log(
log_level,
"",
**entry,
)
class AuditContext:
"""
Captures and aggregates the HTTP request and response bodies during the processing of a request. It ensures that only a configurable maximum amount of data is stored to prevent excessive memory usage.
Attributes:
request_body (bytearray): Accumulated request payload.
response_body (bytearray): Accumulated response payload.
max_body_size (int): Maximum number of bytes to capture.
metadata (Dict[str, Any]): A dictionary to store additional audit metadata (user, http verb, user agent, etc.).
"""
def __init__(self, max_body_size: int = MAX_BODY_LOG_SIZE):
self.request_body = bytearray()
self.response_body = bytearray()
self.max_body_size = max_body_size
self.metadata: Dict[str, Any] = {}
def add_request_chunk(self, chunk: bytes):
if len(self.request_body) < self.max_body_size:
self.request_body.extend(
chunk[: self.max_body_size - len(self.request_body)]
)
def add_response_chunk(self, chunk: bytes):
if len(self.response_body) < self.max_body_size:
self.response_body.extend(
chunk[: self.max_body_size - len(self.response_body)]
)
class AuditLoggingMiddleware:
"""
ASGI middleware that intercepts HTTP requests and responses to perform audit logging. It captures request/response bodies (depending on audit level), headers, HTTP methods, and user information, then logs a structured audit entry at the end of the request cycle.
"""
AUDITED_METHODS = {"PUT", "PATCH", "DELETE", "POST"}
def __init__(
self,
app: ASGI3Application,
*,
excluded_paths: Optional[list[str]] = None,
max_body_size: int = MAX_BODY_LOG_SIZE,
audit_level: AuditLevel = AuditLevel.NONE,
) -> None:
self.app = app
self.audit_logger = AuditLogger(logger)
self.excluded_paths = excluded_paths or []
self.max_body_size = max_body_size
self.audit_level = audit_level
async def __call__(
self,
scope: ASGIScope,
receive: ASGIReceiveCallable,
send: ASGISendCallable,
) -> None:
if scope["type"] != "http":
return await self.app(scope, receive, send)
request = Request(scope=cast(MutableMapping, scope))
if self._should_skip_auditing(request):
return await self.app(scope, receive, send)
async with self._audit_context(request) as context:
async def send_wrapper(message: ASGISendEvent) -> None:
if self.audit_level == AuditLevel.REQUEST_RESPONSE:
await self._capture_response(message, context)
await send(message)
original_receive = receive
async def receive_wrapper() -> ASGIReceiveEvent:
nonlocal original_receive
message = await original_receive()
if self.audit_level in (
AuditLevel.REQUEST,
AuditLevel.REQUEST_RESPONSE,
):
await self._capture_request(message, context)
return message
await self.app(scope, receive_wrapper, send_wrapper)
@asynccontextmanager
async def _audit_context(
self, request: Request
) -> AsyncGenerator[AuditContext, None]:
"""
async context manager that ensures that an audit log entry is recorded after the request is processed.
"""
context = AuditContext()
try:
yield context
finally:
await self._log_audit_entry(request, context)
async def _get_authenticated_user(self, request: Request) -> UserModel:
auth_header = request.headers.get("Authorization")
assert auth_header
user = get_current_user(request, None, get_http_authorization_cred(auth_header))
return user
def _should_skip_auditing(self, request: Request) -> bool:
if (
request.method not in {"POST", "PUT", "PATCH", "DELETE"}
or AUDIT_LOG_LEVEL == "NONE"
or not request.headers.get("authorization")
):
return True
# match either /api/<resource>/...(for the endpoint /api/chat case) or /api/v1/<resource>/...
pattern = re.compile(
r"^/api(?:/v1)?/(" + "|".join(self.excluded_paths) + r")\b"
)
if pattern.match(request.url.path):
return True
return False
async def _capture_request(self, message: ASGIReceiveEvent, context: AuditContext):
if message["type"] == "http.request":
body = message.get("body", b"")
context.add_request_chunk(body)
async def _capture_response(self, message: ASGISendEvent, context: AuditContext):
if message["type"] == "http.response.start":
context.metadata["response_status_code"] = message["status"]
elif message["type"] == "http.response.body":
body = message.get("body", b"")
context.add_response_chunk(body)
async def _log_audit_entry(self, request: Request, context: AuditContext):
try:
user = await self._get_authenticated_user(request)
entry = AuditLogEntry(
id=str(uuid.uuid4()),
user=user.model_dump(include={"id", "name", "email", "role"}),
audit_level=self.audit_level.value,
verb=request.method,
request_uri=str(request.url),
response_status_code=context.metadata.get("response_status_code", None),
source_ip=request.client.host if request.client else None,
user_agent=request.headers.get("user-agent"),
request_object=context.request_body.decode("utf-8", errors="replace"),
response_object=context.response_body.decode("utf-8", errors="replace"),
)
self.audit_logger.write(entry)
except Exception as e:
logger.error(f"Failed to log audit entry: {str(e)}")
+23 -10
View File
@@ -14,14 +14,22 @@ from typing import Optional, Union, List, Dict
from open_webui.models.users import Users
from open_webui.constants import ERROR_MESSAGES
from open_webui.env import WEBUI_SECRET_KEY, TRUSTED_SIGNATURE_KEY, STATIC_DIR
from open_webui.env import (
WEBUI_SECRET_KEY,
TRUSTED_SIGNATURE_KEY,
STATIC_DIR,
SRC_LOG_LEVELS,
)
from fastapi import Depends, HTTPException, Request, Response, status
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"])
SESSION_SECRET = WEBUI_SECRET_KEY
ALGORITHM = "HS256"
@@ -50,7 +58,7 @@ def verify_signature(payload: str, signature: str) -> bool:
def override_static(path: str, content: str):
# Ensure path is safe
if "/" in path or ".." in path:
print(f"Invalid path: {path}")
log.error(f"Invalid path: {path}")
return
file_path = os.path.join(STATIC_DIR, path)
@@ -64,7 +72,7 @@ def get_license_data(app, key):
if key:
try:
res = requests.post(
"https://api.openwebui.com/api/v1/license",
"https://api.openwebui.com/api/v1/license/",
json={"key": key, "version": "1"},
timeout=5,
)
@@ -75,18 +83,19 @@ def get_license_data(app, key):
if k == "resources":
for p, c in v.items():
globals().get("override_static", lambda a, b: None)(p, c)
elif k == "user_count":
elif k == "count":
setattr(app.state, "USER_COUNT", v)
elif k == "webui_name":
elif k == "name":
setattr(app.state, "WEBUI_NAME", v)
elif k == "metadata":
setattr(app.state, "LICENSE_METADATA", v)
return True
else:
print(
log.error(
f"License: retrieval issue: {getattr(res, 'text', 'unknown error')}"
)
except Exception as ex:
print(f"License: Uncaught Exception: {ex}")
log.exception(f"License: Uncaught Exception: {ex}")
return False
@@ -142,6 +151,7 @@ def get_http_authorization_cred(auth_header: str):
def get_current_user(
request: Request,
background_tasks: BackgroundTasks,
auth_token: HTTPAuthorizationCredentials = Depends(bearer_security),
):
token = None
@@ -194,7 +204,10 @@ def get_current_user(
detail=ERROR_MESSAGES.INVALID_TOKEN,
)
else:
Users.update_user_last_active_by_id(user.id)
# Refresh the user's last active timestamp asynchronously
# to prevent blocking the request
if background_tasks:
background_tasks.add_task(Users.update_user_last_active_by_id, user.id)
return user
else:
raise HTTPException(
+12 -7
View File
@@ -66,7 +66,7 @@ async def generate_direct_chat_completion(
user: Any,
models: dict,
):
print("generate_direct_chat_completion")
log.info("generate_direct_chat_completion")
metadata = form_data.pop("metadata", {})
@@ -103,7 +103,7 @@ async def generate_direct_chat_completion(
}
)
print("res", res)
log.info(f"res: {res}")
if res.get("status", False):
# Define a generator to stream responses
@@ -149,7 +149,7 @@ async def generate_direct_chat_completion(
}
)
if "error" in res:
if "error" in res and res["error"]:
raise Exception(res["error"])
return res
@@ -285,7 +285,7 @@ chat_completion = generate_chat_completion
async def chat_completed(request: Request, form_data: dict, user: Any):
if not request.app.state.MODELS:
await get_all_models(request)
await get_all_models(request, user=user)
if getattr(request.state, "direct", False) and hasattr(request.state, "model"):
models = {
@@ -328,9 +328,14 @@ async def chat_completed(request: Request, form_data: dict, user: Any):
}
try:
filter_functions = [
Functions.get_function_by_id(filter_id)
for filter_id in get_sorted_filter_ids(model)
]
result, _ = await process_filter_functions(
request=request,
filter_ids=get_sorted_filter_ids(model),
filter_functions=filter_functions,
filter_type="outlet",
form_data=data,
extra_params=extra_params,
@@ -351,7 +356,7 @@ async def chat_action(request: Request, action_id: str, form_data: dict, user: A
raise Exception(f"Action not found: {action_id}")
if not request.app.state.MODELS:
await get_all_models(request)
await get_all_models(request, user=user)
if getattr(request.state, "direct", False) and hasattr(request.state, "model"):
models = {
@@ -432,7 +437,7 @@ async def chat_action(request: Request, action_id: str, form_data: dict, user: A
)
)
except Exception as e:
print(e)
log.exception(f"Failed to get user values: {e}")
params = {**params, "__user__": __user__}
+188 -126
View File
@@ -1,148 +1,210 @@
import asyncio
import json
import logging
import uuid
from typing import Optional
import aiohttp
import websockets
import requests
from urllib.parse import urljoin
from pydantic import BaseModel
from open_webui.env import SRC_LOG_LEVELS
logger = logging.getLogger(__name__)
logger.setLevel(SRC_LOG_LEVELS["MAIN"])
async def execute_code_jupyter(
jupyter_url, code, token=None, password=None, timeout=10
):
class ResultModel(BaseModel):
"""
Executes Python code in a Jupyter kernel.
Supports authentication with a token or password.
:param jupyter_url: Jupyter server URL (e.g., "http://localhost:8888")
:param code: Code to execute
:param token: Jupyter authentication token (optional)
:param password: Jupyter password (optional)
:param timeout: WebSocket timeout in seconds (default: 10s)
:return: Dictionary with stdout, stderr, and result
- Images are prefixed with "base64:image/png," and separated by newlines if multiple.
Execute Code Result Model
"""
session = requests.Session() # Maintain cookies
headers = {} # Headers for requests
# Authenticate using password
if password and not token:
stdout: Optional[str] = ""
stderr: Optional[str] = ""
result: Optional[str] = ""
class JupyterCodeExecuter:
"""
Execute code in jupyter notebook
"""
def __init__(
self,
base_url: str,
code: str,
token: str = "",
password: str = "",
timeout: int = 60,
):
"""
:param base_url: Jupyter server URL (e.g., "http://localhost:8888")
:param code: Code to execute
:param token: Jupyter authentication token (optional)
:param password: Jupyter password (optional)
:param timeout: WebSocket timeout in seconds (default: 60s)
"""
self.base_url = base_url.rstrip("/")
self.code = code
self.token = token
self.password = password
self.timeout = timeout
self.kernel_id = ""
self.session = aiohttp.ClientSession(base_url=self.base_url)
self.params = {}
self.result = ResultModel()
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
if self.kernel_id:
try:
async with self.session.delete(
f"/api/kernels/{self.kernel_id}", params=self.params
) as response:
response.raise_for_status()
except Exception as err:
logger.exception("close kernel failed, %s", err)
await self.session.close()
async def run(self) -> ResultModel:
try:
login_url = urljoin(jupyter_url, "/login")
response = session.get(login_url)
await self.sign_in()
await self.init_kernel()
await self.execute_code()
except Exception as err:
logger.exception("execute code failed, %s", err)
self.result.stderr = f"Error: {err}"
return self.result
async def sign_in(self) -> None:
# password authentication
if self.password and not self.token:
async with self.session.get("/login") as response:
response.raise_for_status()
xsrf_token = response.cookies["_xsrf"].value
if not xsrf_token:
raise ValueError("_xsrf token not found")
self.session.cookie_jar.update_cookies(response.cookies)
self.session.headers.update({"X-XSRFToken": xsrf_token})
async with self.session.post(
"/login",
data={"_xsrf": xsrf_token, "password": self.password},
allow_redirects=False,
) as response:
response.raise_for_status()
self.session.cookie_jar.update_cookies(response.cookies)
# token authentication
if self.token:
self.params.update({"token": self.token})
async def init_kernel(self) -> None:
async with self.session.post(
url="/api/kernels", params=self.params
) as response:
response.raise_for_status()
xsrf_token = session.cookies.get("_xsrf")
if not xsrf_token:
raise ValueError("Failed to fetch _xsrf token")
login_data = {"_xsrf": xsrf_token, "password": password}
login_response = session.post(
login_url, data=login_data, cookies=session.cookies
)
login_response.raise_for_status()
headers["X-XSRFToken"] = xsrf_token
except Exception as e:
return {
"stdout": "",
"stderr": f"Authentication Error: {str(e)}",
"result": "",
}
# Construct API URLs with authentication token if provided
params = f"?token={token}" if token else ""
kernel_url = urljoin(jupyter_url, f"/api/kernels{params}")
try:
response = session.post(kernel_url, headers=headers, cookies=session.cookies)
response.raise_for_status()
kernel_id = response.json()["id"]
websocket_url = urljoin(
jupyter_url.replace("http", "ws"),
f"/api/kernels/{kernel_id}/channels{params}",
)
kernel_data = await response.json()
self.kernel_id = kernel_data["id"]
def init_ws(self) -> (str, dict):
ws_base = self.base_url.replace("http", "ws")
ws_params = "?" + "&".join([f"{key}={val}" for key, val in self.params.items()])
websocket_url = f"{ws_base}/api/kernels/{self.kernel_id}/channels{ws_params if len(ws_params) > 1 else ''}"
ws_headers = {}
if password and not token:
ws_headers["X-XSRFToken"] = session.cookies.get("_xsrf")
cookies = {name: value for name, value in session.cookies.items()}
ws_headers["Cookie"] = "; ".join(
[f"{name}={value}" for name, value in cookies.items()]
)
if self.password and not self.token:
ws_headers = {
"Cookie": "; ".join(
[
f"{cookie.key}={cookie.value}"
for cookie in self.session.cookie_jar
]
),
**self.session.headers,
}
return websocket_url, ws_headers
async def execute_code(self) -> None:
# initialize ws
websocket_url, ws_headers = self.init_ws()
# execute
async with websockets.connect(
websocket_url, additional_headers=ws_headers
) as ws:
msg_id = str(uuid.uuid4())
execute_request = {
"header": {
"msg_id": msg_id,
"msg_type": "execute_request",
"username": "user",
"session": str(uuid.uuid4()),
"date": "",
"version": "5.3",
},
"parent_header": {},
"metadata": {},
"content": {
"code": code,
"silent": False,
"store_history": True,
"user_expressions": {},
"allow_stdin": False,
"stop_on_error": True,
},
"channel": "shell",
}
await ws.send(json.dumps(execute_request))
await self.execute_in_jupyter(ws)
stdout, stderr, result = "", "", []
while True:
try:
message = await asyncio.wait_for(ws.recv(), timeout)
message_data = json.loads(message)
if message_data.get("parent_header", {}).get("msg_id") == msg_id:
msg_type = message_data.get("msg_type")
if msg_type == "stream":
if message_data["content"]["name"] == "stdout":
stdout += message_data["content"]["text"]
elif message_data["content"]["name"] == "stderr":
stderr += message_data["content"]["text"]
elif msg_type in ("execute_result", "display_data"):
data = message_data["content"]["data"]
if "image/png" in data:
result.append(
f"data:image/png;base64,{data['image/png']}"
)
elif "text/plain" in data:
result.append(data["text/plain"])
elif msg_type == "error":
stderr += "\n".join(message_data["content"]["traceback"])
elif (
msg_type == "status"
and message_data["content"]["execution_state"] == "idle"
):
async def execute_in_jupyter(self, ws) -> None:
# send message
msg_id = uuid.uuid4().hex
await ws.send(
json.dumps(
{
"header": {
"msg_id": msg_id,
"msg_type": "execute_request",
"username": "user",
"session": uuid.uuid4().hex,
"date": "",
"version": "5.3",
},
"parent_header": {},
"metadata": {},
"content": {
"code": self.code,
"silent": False,
"store_history": True,
"user_expressions": {},
"allow_stdin": False,
"stop_on_error": True,
},
"channel": "shell",
}
)
)
# parse message
stdout, stderr, result = "", "", []
while True:
try:
# wait for message
message = await asyncio.wait_for(ws.recv(), self.timeout)
message_data = json.loads(message)
# msg id not match, skip
if message_data.get("parent_header", {}).get("msg_id") != msg_id:
continue
# check message type
msg_type = message_data.get("msg_type")
match msg_type:
case "stream":
if message_data["content"]["name"] == "stdout":
stdout += message_data["content"]["text"]
elif message_data["content"]["name"] == "stderr":
stderr += message_data["content"]["text"]
case "execute_result" | "display_data":
data = message_data["content"]["data"]
if "image/png" in data:
result.append(f"data:image/png;base64,{data['image/png']}")
elif "text/plain" in data:
result.append(data["text/plain"])
case "error":
stderr += "\n".join(message_data["content"]["traceback"])
case "status":
if message_data["content"]["execution_state"] == "idle":
break
except asyncio.TimeoutError:
stderr += "\nExecution timed out."
break
except asyncio.TimeoutError:
stderr += "\nExecution timed out."
break
self.result.stdout = stdout.strip()
self.result.stderr = stderr.strip()
self.result.result = "\n".join(result).strip() if result else ""
except Exception as e:
return {"stdout": "", "stderr": f"Error: {str(e)}", "result": ""}
finally:
if kernel_id:
requests.delete(
f"{kernel_url}/{kernel_id}", headers=headers, cookies=session.cookies
)
return {
"stdout": stdout.strip(),
"stderr": stderr.strip(),
"result": "\n".join(result).strip() if result else "",
}
async def execute_code_jupyter(
base_url: str, code: str, token: str = "", password: str = "", timeout: int = 60
) -> dict:
async with JupyterCodeExecuter(
base_url, code, token, password, timeout
) as executor:
result = await executor.run()
return result.model_dump()
+24 -12
View File
@@ -1,9 +1,15 @@
import inspect
import logging
from open_webui.utils.plugin import load_function_module_by_id
from open_webui.models.functions import Functions
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MAIN"])
def get_sorted_filter_ids(model):
def get_sorted_filter_ids(model: dict):
def get_priority(function_id):
function = Functions.get_function_by_id(function_id)
if function is not None and hasattr(function, "valves"):
@@ -27,12 +33,13 @@ def get_sorted_filter_ids(model):
async def process_filter_functions(
request, filter_ids, filter_type, form_data, extra_params
request, filter_functions, filter_type, form_data, extra_params
):
skip_files = None
for filter_id in filter_ids:
filter = Functions.get_function_by_id(filter_id)
for function in filter_functions:
filter = function
filter_id = function.id
if not filter:
continue
@@ -42,6 +49,11 @@ async def process_filter_functions(
function_module, _, _ = load_function_module_by_id(filter_id)
request.app.state.FUNCTIONS[filter_id] = function_module
# Prepare handler function
handler = getattr(function_module, filter_type, None)
if not handler:
continue
# Check if the function has a file_handler variable
if filter_type == "inlet" and hasattr(function_module, "file_handler"):
skip_files = function_module.file_handler
@@ -53,15 +65,15 @@ async def process_filter_functions(
**(valves if valves else {})
)
# Prepare handler function
handler = getattr(function_module, filter_type, None)
if not handler:
continue
try:
# Prepare parameters
sig = inspect.signature(handler)
params = {"body": form_data} | {
params = {"body": form_data}
if filter_type == "stream":
params = {"event": form_data}
params = params | {
k: v
for k, v in {
**extra_params,
@@ -80,7 +92,7 @@ async def process_filter_functions(
)
)
except Exception as e:
print(e)
log.exception(f"Failed to get user values: {e}")
# Execute handler
if inspect.iscoroutinefunction(handler):
@@ -89,7 +101,7 @@ async def process_filter_functions(
form_data = handler(**params)
except Exception as e:
print(f"Error in {filter_type} handler {filter_id}: {e}")
log.exception(f"Error in {filter_type} handler {filter_id}: {e}")
raise e
# Handle file cleanup for inlet
+140
View File
@@ -0,0 +1,140 @@
import json
import logging
import sys
from typing import TYPE_CHECKING
from loguru import logger
from open_webui.env import (
AUDIT_LOG_FILE_ROTATION_SIZE,
AUDIT_LOG_LEVEL,
AUDIT_LOGS_FILE_PATH,
GLOBAL_LOG_LEVEL,
)
if TYPE_CHECKING:
from loguru import Record
def stdout_format(record: "Record") -> str:
"""
Generates a formatted string for log records that are output to the console. This format includes a timestamp, log level, source location (module, function, and line), the log message, and any extra data (serialized as JSON).
Parameters:
record (Record): A Loguru record that contains logging details including time, level, name, function, line, message, and any extra context.
Returns:
str: A formatted log string intended for stdout.
"""
record["extra"]["extra_json"] = json.dumps(record["extra"])
return (
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
"<level>{level: <8}</level> | "
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - "
"<level>{message}</level> - {extra[extra_json]}"
"\n{exception}"
)
class InterceptHandler(logging.Handler):
"""
Intercepts log records from Python's standard logging module
and redirects them to Loguru's logger.
"""
def emit(self, record):
"""
Called by the standard logging module for each log event.
It transforms the standard `LogRecord` into a format compatible with Loguru
and passes it to Loguru's logger.
"""
try:
level = logger.level(record.levelname).name
except ValueError:
level = record.levelno
frame, depth = sys._getframe(6), 6
while frame and frame.f_code.co_filename == logging.__file__:
frame = frame.f_back
depth += 1
logger.opt(depth=depth, exception=record.exc_info).log(
level, record.getMessage()
)
def file_format(record: "Record"):
"""
Formats audit log records into a structured JSON string for file output.
Parameters:
record (Record): A Loguru record containing extra audit data.
Returns:
str: A JSON-formatted string representing the audit data.
"""
audit_data = {
"id": record["extra"].get("id", ""),
"timestamp": int(record["time"].timestamp()),
"user": record["extra"].get("user", dict()),
"audit_level": record["extra"].get("audit_level", ""),
"verb": record["extra"].get("verb", ""),
"request_uri": record["extra"].get("request_uri", ""),
"response_status_code": record["extra"].get("response_status_code", 0),
"source_ip": record["extra"].get("source_ip", ""),
"user_agent": record["extra"].get("user_agent", ""),
"request_object": record["extra"].get("request_object", b""),
"response_object": record["extra"].get("response_object", b""),
"extra": record["extra"].get("extra", {}),
}
record["extra"]["file_extra"] = json.dumps(audit_data, default=str)
return "{extra[file_extra]}\n"
def start_logger():
"""
Initializes and configures Loguru's logger with distinct handlers:
A console (stdout) handler for general log messages (excluding those marked as auditable).
An optional file handler for audit logs if audit logging is enabled.
Additionally, this function reconfigures Pythons standard logging to route through Loguru and adjusts logging levels for Uvicorn.
Parameters:
enable_audit_logging (bool): Determines whether audit-specific log entries should be recorded to file.
"""
logger.remove()
logger.add(
sys.stdout,
level=GLOBAL_LOG_LEVEL,
format=stdout_format,
filter=lambda record: "auditable" not in record["extra"],
)
if AUDIT_LOG_LEVEL != "NONE":
try:
logger.add(
AUDIT_LOGS_FILE_PATH,
level="INFO",
rotation=AUDIT_LOG_FILE_ROTATION_SIZE,
compression="zip",
format=file_format,
filter=lambda record: record["extra"].get("auditable") is True,
)
except Exception as e:
logger.error(f"Failed to initialize audit log file handler: {str(e)}")
logging.basicConfig(
handlers=[InterceptHandler()], level=GLOBAL_LOG_LEVEL, force=True
)
for uvicorn_logger_name in ["uvicorn", "uvicorn.error"]:
uvicorn_logger = logging.getLogger(uvicorn_logger_name)
uvicorn_logger.setLevel(GLOBAL_LOG_LEVEL)
uvicorn_logger.handlers = []
for uvicorn_logger_name in ["uvicorn.access"]:
uvicorn_logger = logging.getLogger(uvicorn_logger_name)
uvicorn_logger.setLevel(GLOBAL_LOG_LEVEL)
uvicorn_logger.handlers = [InterceptHandler()]
logger.info(f"GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}")
+287 -140
View File
@@ -68,6 +68,7 @@ from open_webui.utils.misc import (
get_last_user_message,
get_last_assistant_message,
prepend_to_first_user_message_content,
convert_logit_bias_input_to_json,
)
from open_webui.utils.tools import get_tools
from open_webui.utils.plugin import load_function_module_by_id
@@ -351,24 +352,25 @@ async def chat_web_search_handler(
all_results.append(results)
files = form_data.get("files", [])
if request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT:
files.append(
{
"docs": results.get("docs", []),
"name": searchQuery,
"type": "web_search_docs",
"urls": results["filenames"],
}
)
else:
if results.get("collection_name"):
files.append(
{
"collection_name": results["collection_name"],
"name": searchQuery,
"type": "web_search_results",
"type": "web_search",
"urls": results["filenames"],
}
)
elif results.get("docs"):
files.append(
{
"docs": results.get("docs", []),
"name": searchQuery,
"type": "web_search",
"urls": results["filenames"],
}
)
form_data["files"] = files
except Exception as e:
log.exception(e)
@@ -518,6 +520,7 @@ async def chat_completion_files_handler(
sources = []
if files := body.get("metadata", {}).get("files", None):
queries = []
try:
queries_response = await generate_queries(
request,
@@ -543,8 +546,8 @@ async def chat_completion_files_handler(
queries_response = {"queries": [queries_response]}
queries = queries_response.get("queries", [])
except Exception as e:
queries = []
except:
pass
if len(queries) == 0:
queries = [get_last_user_message(body["messages"])]
@@ -556,6 +559,7 @@ async def chat_completion_files_handler(
sources = await loop.run_in_executor(
executor,
lambda: get_sources_from_files(
request=request,
files=files,
queries=queries,
embedding_function=lambda query: request.app.state.EMBEDDING_FUNCTION(
@@ -607,11 +611,18 @@ def apply_params_to_form_data(form_data, model):
if "reasoning_effort" in params:
form_data["reasoning_effort"] = params["reasoning_effort"]
if "logit_bias" in params:
try:
form_data["logit_bias"] = json.loads(
convert_logit_bias_input_to_json(params["logit_bias"])
)
except Exception as e:
print(f"Error parsing logit_bias: {e}")
return form_data
async def process_chat_payload(request, form_data, metadata, user, model):
async def process_chat_payload(request, form_data, user, metadata, model):
form_data = apply_params_to_form_data(form_data, model)
log.debug(f"form_data: {form_data}")
@@ -704,9 +715,14 @@ async def process_chat_payload(request, form_data, metadata, user, model):
raise e
try:
filter_functions = [
Functions.get_function_by_id(filter_id)
for filter_id in get_sorted_filter_ids(model)
]
form_data, flags = await process_filter_functions(
request=request,
filter_ids=get_sorted_filter_ids(model),
filter_functions=filter_functions,
filter_type="inlet",
form_data=form_data,
extra_params=extra_params,
@@ -738,6 +754,7 @@ async def process_chat_payload(request, form_data, metadata, user, model):
tool_ids = form_data.pop("tool_ids", None)
files = form_data.pop("files", None)
# Remove files duplicates
if files:
files = list({json.dumps(f, sort_keys=True): f for f in files}.values())
@@ -795,8 +812,6 @@ async def process_chat_payload(request, form_data, metadata, user, model):
if len(sources) > 0:
context_string = ""
for source_idx, source in enumerate(sources):
source_id = source.get("source", {}).get("name", "")
if "document" in source:
for doc_idx, doc_context in enumerate(source["document"]):
context_string += f"<source><source_id>{source_idx}</source_id><source_context>{doc_context}</source_context></source>\n"
@@ -854,7 +869,7 @@ async def process_chat_payload(request, form_data, metadata, user, model):
async def process_chat_response(
request, response, form_data, user, events, metadata, tasks
request, response, form_data, user, metadata, model, events, tasks
):
async def background_tasks_handler():
message_map = Chats.get_messages_by_chat_id(metadata["chat_id"])
@@ -1048,6 +1063,26 @@ async def process_chat_response(
):
return response
extra_params = {
"__event_emitter__": event_emitter,
"__event_call__": event_caller,
"__user__": {
"id": user.id,
"email": user.email,
"name": user.name,
"role": user.role,
},
"__metadata__": metadata,
"__request__": request,
"__model__": model,
}
filter_functions = [
Functions.get_function_by_id(filter_id)
for filter_id in get_sorted_filter_ids(model)
]
print(f"{filter_functions=}")
# Streaming response
if event_emitter and event_caller:
task_id = str(uuid4()) # Create a unique task ID.
@@ -1127,12 +1162,12 @@ async def process_chat_response(
if reasoning_duration is not None:
if raw:
content = f'{content}\n<{block["tag"]}>{block["content"]}</{block["tag"]}>\n'
content = f'{content}\n<{block["start_tag"]}>{block["content"]}<{block["end_tag"]}>\n'
else:
content = f'{content}\n<details type="reasoning" done="true" duration="{reasoning_duration}">\n<summary>Thought for {reasoning_duration} seconds</summary>\n{reasoning_display_content}\n</details>\n'
else:
if raw:
content = f'{content}\n<{block["tag"]}>{block["content"]}</{block["tag"]}>\n'
content = f'{content}\n<{block["start_tag"]}>{block["content"]}<{block["end_tag"]}>\n'
else:
content = f'{content}\n<details type="reasoning" done="false">\n<summary>Thinking…</summary>\n{reasoning_display_content}\n</details>\n'
@@ -1228,9 +1263,9 @@ async def process_chat_response(
return attributes
if content_blocks[-1]["type"] == "text":
for tag in tags:
for start_tag, end_tag in tags:
# Match start tag e.g., <tag> or <tag attr="value">
start_tag_pattern = rf"<{tag}(\s.*?)?>"
start_tag_pattern = rf"<{re.escape(start_tag)}(\s.*?)?>"
match = re.search(start_tag_pattern, content)
if match:
attr_content = (
@@ -1263,7 +1298,8 @@ async def process_chat_response(
content_blocks.append(
{
"type": content_type,
"tag": tag,
"start_tag": start_tag,
"end_tag": end_tag,
"attributes": attributes,
"content": "",
"started_at": time.time(),
@@ -1275,9 +1311,10 @@ async def process_chat_response(
break
elif content_blocks[-1]["type"] == content_type:
tag = content_blocks[-1]["tag"]
start_tag = content_blocks[-1]["start_tag"]
end_tag = content_blocks[-1]["end_tag"]
# Match end tag e.g., </tag>
end_tag_pattern = rf"</{tag}>"
end_tag_pattern = rf"<{re.escape(end_tag)}>"
# Check if the content has the end tag
if re.search(end_tag_pattern, content):
@@ -1285,7 +1322,7 @@ async def process_chat_response(
block_content = content_blocks[-1]["content"]
# Strip start and end tags from the content
start_tag_pattern = rf"<{tag}(.*?)>"
start_tag_pattern = rf"<{re.escape(start_tag)}(.*?)>"
block_content = re.sub(
start_tag_pattern, "", block_content
).strip()
@@ -1350,7 +1387,7 @@ async def process_chat_response(
# Clean processed content
content = re.sub(
rf"<{tag}(.*?)>(.|\n)*?</{tag}>",
rf"<{re.escape(start_tag)}(.*?)>(.|\n)*?<{re.escape(end_tag)}>",
"",
content,
flags=re.DOTALL,
@@ -1388,19 +1425,24 @@ async def process_chat_response(
# We might want to disable this by default
DETECT_REASONING = True
DETECT_SOLUTION = True
DETECT_CODE_INTERPRETER = metadata.get("features", {}).get(
"code_interpreter", False
)
reasoning_tags = [
"think",
"thinking",
"reason",
"reasoning",
"thought",
"Thought",
("think", "/think"),
("thinking", "/thinking"),
("reason", "/reason"),
("reasoning", "/reasoning"),
("thought", "/thought"),
("Thought", "/Thought"),
("|begin_of_thought|", "|end_of_thought|"),
]
code_interpreter_tags = ["code_interpreter"]
code_interpreter_tags = [("code_interpreter", "/code_interpreter")]
solution_tags = [("|begin_of_solution|", "|end_of_solution|")]
try:
for event in events:
@@ -1444,119 +1486,204 @@ async def process_chat_response(
try:
data = json.loads(data)
if "selected_model_id" in data:
model_id = data["selected_model_id"]
Chats.upsert_message_to_chat_by_id_and_message_id(
metadata["chat_id"],
metadata["message_id"],
{
"selectedModelId": model_id,
},
)
else:
choices = data.get("choices", [])
if not choices:
continue
data, _ = await process_filter_functions(
request=request,
filter_functions=filter_functions,
filter_type="stream",
form_data=data,
extra_params=extra_params,
)
delta = choices[0].get("delta", {})
delta_tool_calls = delta.get("tool_calls", None)
if delta_tool_calls:
for delta_tool_call in delta_tool_calls:
tool_call_index = delta_tool_call.get("index")
if tool_call_index is not None:
if (
len(response_tool_calls)
<= tool_call_index
):
response_tool_calls.append(
delta_tool_call
)
else:
delta_name = delta_tool_call.get(
"function", {}
).get("name")
delta_arguments = delta_tool_call.get(
"function", {}
).get("arguments")
if delta_name:
response_tool_calls[
tool_call_index
]["function"]["name"] += delta_name
if delta_arguments:
response_tool_calls[
tool_call_index
]["function"][
"arguments"
] += delta_arguments
value = delta.get("content")
if value:
content = f"{content}{value}"
if not content_blocks:
content_blocks.append(
{
"type": "text",
"content": "",
}
)
content_blocks[-1]["content"] = (
content_blocks[-1]["content"] + value
if data:
if "selected_model_id" in data:
model_id = data["selected_model_id"]
Chats.upsert_message_to_chat_by_id_and_message_id(
metadata["chat_id"],
metadata["message_id"],
{
"selectedModelId": model_id,
},
)
if DETECT_REASONING:
content, content_blocks, _ = (
tag_content_handler(
"reasoning",
reasoning_tags,
content,
content_blocks,
else:
choices = data.get("choices", [])
if not choices:
usage = data.get("usage", {})
if usage:
await event_emitter(
{
"type": "chat:completion",
"data": {
"usage": usage,
},
}
)
)
continue
if DETECT_CODE_INTERPRETER:
content, content_blocks, end = (
tag_content_handler(
"code_interpreter",
code_interpreter_tags,
content,
content_blocks,
delta = choices[0].get("delta", {})
delta_tool_calls = delta.get("tool_calls", None)
if delta_tool_calls:
for delta_tool_call in delta_tool_calls:
tool_call_index = delta_tool_call.get(
"index"
)
)
if end:
break
if tool_call_index is not None:
if (
len(response_tool_calls)
<= tool_call_index
):
response_tool_calls.append(
delta_tool_call
)
else:
delta_name = delta_tool_call.get(
"function", {}
).get("name")
delta_arguments = (
delta_tool_call.get(
"function", {}
).get("arguments")
)
if delta_name:
response_tool_calls[
tool_call_index
]["function"][
"name"
] += delta_name
if delta_arguments:
response_tool_calls[
tool_call_index
]["function"][
"arguments"
] += delta_arguments
value = delta.get("content")
reasoning_content = delta.get("reasoning_content")
if reasoning_content:
if (
not content_blocks
or content_blocks[-1]["type"] != "reasoning"
):
reasoning_block = {
"type": "reasoning",
"start_tag": "think",
"end_tag": "/think",
"attributes": {
"type": "reasoning_content"
},
"content": "",
"started_at": time.time(),
}
content_blocks.append(reasoning_block)
else:
reasoning_block = content_blocks[-1]
reasoning_block["content"] += reasoning_content
if ENABLE_REALTIME_CHAT_SAVE:
# Save message in the database
Chats.upsert_message_to_chat_by_id_and_message_id(
metadata["chat_id"],
metadata["message_id"],
{
"content": serialize_content_blocks(
content_blocks
),
},
)
else:
data = {
"content": serialize_content_blocks(
content_blocks
),
)
}
await event_emitter(
{
"type": "chat:completion",
"data": data,
}
)
if value:
if (
content_blocks
and content_blocks[-1]["type"]
== "reasoning"
and content_blocks[-1]
.get("attributes", {})
.get("type")
== "reasoning_content"
):
reasoning_block = content_blocks[-1]
reasoning_block["ended_at"] = time.time()
reasoning_block["duration"] = int(
reasoning_block["ended_at"]
- reasoning_block["started_at"]
)
content_blocks.append(
{
"type": "text",
"content": "",
}
)
content = f"{content}{value}"
if not content_blocks:
content_blocks.append(
{
"type": "text",
"content": "",
}
)
content_blocks[-1]["content"] = (
content_blocks[-1]["content"] + value
)
if DETECT_REASONING:
content, content_blocks, _ = (
tag_content_handler(
"reasoning",
reasoning_tags,
content,
content_blocks,
)
)
if DETECT_CODE_INTERPRETER:
content, content_blocks, end = (
tag_content_handler(
"code_interpreter",
code_interpreter_tags,
content,
content_blocks,
)
)
if end:
break
if DETECT_SOLUTION:
content, content_blocks, _ = (
tag_content_handler(
"solution",
solution_tags,
content,
content_blocks,
)
)
if ENABLE_REALTIME_CHAT_SAVE:
# Save message in the database
Chats.upsert_message_to_chat_by_id_and_message_id(
metadata["chat_id"],
metadata["message_id"],
{
"content": serialize_content_blocks(
content_blocks
),
},
)
else:
data = {
"content": serialize_content_blocks(
content_blocks
),
}
await event_emitter(
{
"type": "chat:completion",
"data": data,
}
)
except Exception as e:
done = "data: [DONE]" in line
if done:
@@ -1855,7 +1982,10 @@ async def process_chat_response(
}
)
print(content_blocks, serialize_content_blocks(content_blocks))
log.info(f"content_blocks={content_blocks}")
log.info(
f"serialize_content_blocks={serialize_content_blocks(content_blocks)}"
)
try:
res = await generate_chat_completion(
@@ -1926,7 +2056,7 @@ async def process_chat_response(
await background_tasks_handler()
except asyncio.CancelledError:
print("Task was cancelled!")
log.warning("Task was cancelled!")
await event_emitter({"type": "task-cancelled"})
if not ENABLE_REALTIME_CHAT_SAVE:
@@ -1947,17 +2077,34 @@ async def process_chat_response(
return {"status": True, "task_id": task_id}
else:
# Fallback to the original response
async def stream_wrapper(original_generator, events):
def wrap_item(item):
return f"data: {item}\n\n"
for event in events:
yield wrap_item(json.dumps(event))
event, _ = await process_filter_functions(
request=request,
filter_functions=filter_functions,
filter_type="stream",
form_data=event,
extra_params=extra_params,
)
if event:
yield wrap_item(json.dumps(event))
async for data in original_generator:
yield data
data, _ = await process_filter_functions(
request=request,
filter_functions=filter_functions,
filter_type="stream",
form_data=data,
extra_params=extra_params,
)
if data:
yield data
return StreamingResponse(
stream_wrapper(response.body_iterator, events),
+19 -1
View File
@@ -2,12 +2,18 @@ import hashlib
import re
import time
import uuid
import logging
from datetime import timedelta
from pathlib import Path
from typing import Callable, Optional
import json
import collections.abc
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MAIN"])
def deep_update(d, u):
@@ -412,7 +418,7 @@ def parse_ollama_modelfile(model_text):
elif param_type is bool:
value = value.lower() == "true"
except Exception as e:
print(e)
log.exception(f"Failed to parse parameter {param}: {e}")
continue
data["params"][param] = value
@@ -445,3 +451,15 @@ def parse_ollama_modelfile(model_text):
data["params"]["messages"] = messages
return data
def convert_logit_bias_input_to_json(user_input):
logit_bias_pairs = user_input.split(",")
logit_bias_json = {}
for pair in logit_bias_pairs:
token, bias = pair.split(":")
token = str(token.strip())
bias = int(bias.strip())
bias = 100 if bias > 100 else -100 if bias < -100 else bias
logit_bias_json[token] = bias
return json.dumps(logit_bias_json)
+6 -5
View File
@@ -22,6 +22,7 @@ from open_webui.config import (
)
from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL
from open_webui.models.users import UserModel
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
@@ -29,17 +30,17 @@ log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MAIN"])
async def get_all_base_models(request: Request):
async def get_all_base_models(request: Request, user: UserModel = None):
function_models = []
openai_models = []
ollama_models = []
if request.app.state.config.ENABLE_OPENAI_API:
openai_models = await openai.get_all_models(request)
openai_models = await openai.get_all_models(request, user=user)
openai_models = openai_models["data"]
if request.app.state.config.ENABLE_OLLAMA_API:
ollama_models = await ollama.get_all_models(request)
ollama_models = await ollama.get_all_models(request, user=user)
ollama_models = [
{
"id": model["model"],
@@ -58,8 +59,8 @@ async def get_all_base_models(request: Request):
return models
async def get_all_models(request):
models = await get_all_base_models(request)
async def get_all_models(request, user: UserModel = None):
models = await get_all_base_models(request, user=user)
# If there are no models, return an empty list
if len(models) == 0:
+2 -11
View File
@@ -146,7 +146,7 @@ class OAuthManager:
nested_claims = oauth_claim.split(".")
for nested_claim in nested_claims:
claim_data = claim_data.get(nested_claim, {})
user_oauth_groups = claim_data if isinstance(claim_data, list) else None
user_oauth_groups = claim_data if isinstance(claim_data, list) else []
user_current_groups: list[GroupModel] = Groups.get_groups_by_member_id(user.id)
all_available_groups: list[GroupModel] = Groups.get_groups()
@@ -234,7 +234,7 @@ class OAuthManager:
log.warning(f"OAuth callback error: {e}")
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
user_data: UserInfo = token.get("userinfo")
if not user_data or "email" not in user_data:
if not user_data or auth_manager_config.OAUTH_EMAIL_CLAIM not in user_data:
user_data: UserInfo = await client.userinfo(token=token)
if not user_data:
log.warning(f"OAuth callback failed, user data is missing: {token}")
@@ -315,15 +315,6 @@ class OAuthManager:
if not user:
user_count = Users.get_num_users()
if (
request.app.state.USER_COUNT
and user_count >= request.app.state.USER_COUNT
):
raise HTTPException(
403,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
# If the user does not exist, check if signups are enabled
if auth_manager_config.ENABLE_OAUTH_SIGNUP:
# Check if an existing user with the same email already exists
+8 -1
View File
@@ -62,6 +62,7 @@ def apply_model_params_to_body_openai(params: dict, form_data: dict) -> dict:
"reasoning_effort": str,
"seed": lambda x: x,
"stop": lambda x: [bytes(s, "utf-8").decode("unicode_escape") for s in x],
"logit_bias": lambda x: x,
}
return apply_model_params_to_body(params, form_data, mappings)
@@ -124,7 +125,7 @@ def convert_messages_openai_to_ollama(messages: list[dict]) -> list[dict]:
tool_call_id = message.get("tool_call_id", None)
# Check if the content is a string (just a simple message)
if isinstance(content, str):
if isinstance(content, str) and not tool_calls:
# If the content is a string, it's pure text
new_message["content"] = content
@@ -230,6 +231,12 @@ def convert_payload_openai_to_ollama(openai_payload: dict) -> dict:
"system"
] # To prevent Ollama warning of invalid option provided
# If there is the "stop" parameter in the openai_payload, remap it to the ollama_payload.options
if "stop" in openai_payload:
ollama_options = ollama_payload.get("options", {})
ollama_options["stop"] = openai_payload.get("stop")
ollama_payload["options"] = ollama_options
if "metadata" in openai_payload:
ollama_payload["metadata"] = openai_payload["metadata"]
+1 -1
View File
@@ -110,7 +110,7 @@ class PDFGenerator:
# When running using `pip install -e .` the static directory is in the site packages.
# This path only works if `open-webui serve` is run from the root of this project.
if not FONTS_DIR.exists():
FONTS_DIR = Path("./backend/static/fonts")
FONTS_DIR = Path(".") / "backend" / "static" / "fonts"
pdf.add_font("NotoSans", "", f"{FONTS_DIR}/NotoSans-Regular.ttf")
pdf.add_font("NotoSans", "b", f"{FONTS_DIR}/NotoSans-Bold.ttf")
+1 -1
View File
@@ -45,7 +45,7 @@ def extract_frontmatter(content):
frontmatter[key.strip()] = value.strip()
except Exception as e:
print(f"An error occurred: {e}")
log.exception(f"Failed to extract frontmatter: {e}")
return {}
return frontmatter
+2 -2
View File
@@ -104,7 +104,7 @@ async def convert_streaming_response_ollama_to_openai(ollama_streaming_response)
data = json.loads(data)
model = data.get("model", "ollama")
message_content = data.get("message", {}).get("content", "")
message_content = data.get("message", {}).get("content", None)
tool_calls = data.get("message", {}).get("tool_calls", None)
openai_tool_calls = None
@@ -118,7 +118,7 @@ async def convert_streaming_response_ollama_to_openai(ollama_streaming_response)
usage = convert_ollama_usage_to_openai(data)
data = openai_chat_chunk_message_template(
model, message_content if not done else None, openai_tool_calls, usage
model, message_content, openai_tool_calls, usage
)
line = f"data: {json.dumps(data)}\n\n"
+1 -1
View File
@@ -104,7 +104,7 @@ def replace_prompt_variable(template: str, prompt: str) -> str:
def replace_messages_variable(
template: str, messages: Optional[list[str]] = None
template: str, messages: Optional[list[dict]] = None
) -> str:
def replacement_function(match):
full_match = match.group(0)
+14 -8
View File
@@ -1,10 +1,10 @@
fastapi==0.115.7
uvicorn[standard]==0.30.6
uvicorn[standard]==0.34.0
pydantic==2.10.6
python-multipart==0.0.18
python-socketio==5.11.3
python-jose==3.3.0
python-jose==3.4.0
passlib[bcrypt]==1.7.4
requests==2.32.3
@@ -13,14 +13,14 @@ async-timeout
aiocache
aiofiles
sqlalchemy==2.0.32
sqlalchemy==2.0.38
alembic==1.14.0
peewee==3.17.8
peewee==3.17.9
peewee-migrate==1.12.2
psycopg2-binary==2.9.9
pgvector==0.3.5
PyMySQL==1.1.1
bcrypt==4.2.0
bcrypt==4.3.0
pymongo
redis
@@ -31,14 +31,17 @@ APScheduler==3.10.4
RestrictedPython==8.0
loguru==0.7.2
asgiref==3.8.1
# AI libraries
openai
anthropic
google-generativeai==0.7.2
tiktoken
langchain==0.3.7
langchain-community==0.3.7
langchain==0.3.19
langchain-community==0.3.18
fake-useragent==1.5.1
chromadb==0.6.2
@@ -46,6 +49,8 @@ pymilvus==2.5.0
qdrant-client~=1.12.0
opensearch-py==2.8.0
playwright==1.49.1 # Caution: version must match docker-compose.playwright.yaml
elasticsearch==8.17.1
transformers
sentence-transformers==3.3.1
@@ -71,6 +76,7 @@ validators==0.34.0
psutil
sentencepiece
soundfile==0.13.1
azure-ai-documentintelligence==1.0.0
opencv-python-headless==4.11.0.86
rapidocr-onnxruntime==1.3.24
@@ -81,7 +87,7 @@ faster-whisper==1.1.1
PyJWT[crypto]==2.10.1
authlib==1.4.1
black==24.8.0
black==25.1.0
langfuse==2.44.0
youtube-transcript-api==0.6.3
pytube==15.0.0
+235 -6
View File
@@ -1,13 +1,14 @@
{
"name": "open-webui",
"version": "0.5.15",
"version": "0.5.20",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "open-webui",
"version": "0.5.15",
"version": "0.5.20",
"dependencies": {
"@azure/msal-browser": "^4.5.0",
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-python": "^6.1.6",
"@codemirror/language-data": "^6.5.1",
@@ -41,6 +42,7 @@
"i18next-resources-to-backend": "^1.2.0",
"idb": "^7.1.1",
"js-sha256": "^0.10.1",
"jspdf": "^3.0.0",
"katex": "^0.16.21",
"kokoro-js": "^1.1.1",
"marked": "^9.1.0",
@@ -134,10 +136,32 @@
"node": ">=6.0.0"
}
},
"node_modules/@azure/msal-browser": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.5.0.tgz",
"integrity": "sha512-H7mWmu8yI0n0XxhJobrgncXI6IU5h8DKMiWDHL5y+Dc58cdg26GbmaMUehbUkdKAQV2OTiFa4FUa6Fdu/wIxBg==",
"license": "MIT",
"dependencies": {
"@azure/msal-common": "15.2.0"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@azure/msal-common": {
"version": "15.2.0",
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.2.0.tgz",
"integrity": "sha512-HiYfGAKthisUYqHG1nImCf/uzcyS31wng3o+CycWLIM9chnYJ9Lk6jZ30Y6YiYYpTQ9+z/FGUpiKKekd3Arc0A==",
"license": "MIT",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@babel/runtime": {
"version": "7.24.1",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz",
"integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==",
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.9.tgz",
"integrity": "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==",
"license": "MIT",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
@@ -3170,6 +3194,13 @@
"integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==",
"dev": true
},
"node_modules/@types/raf": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/resolve": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
@@ -3199,6 +3230,13 @@
"integrity": "sha512-MQ1AnmTLOncwEf9IVU+B2e4Hchrku5N67NkgcAHW0p3sdzPe0FNMANxEm6OJUzPniEQGkeT3OROLlCwZJLWFZA==",
"dev": true
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/unist": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz",
@@ -3794,6 +3832,18 @@
"node": ">= 4.0.0"
}
},
"node_modules/atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
"license": "(MIT OR Apache-2.0)",
"bin": {
"atob": "bin/atob.js"
},
"engines": {
"node": ">= 4.5.0"
}
},
"node_modules/aws-sign2": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
@@ -3829,6 +3879,16 @@
"dev": true,
"optional": true
},
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -4033,6 +4093,18 @@
"node": "10.* || >= 12.*"
}
},
"node_modules/btoa": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
"integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==",
"license": "(MIT OR Apache-2.0)",
"bin": {
"btoa": "bin/btoa.js"
},
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
@@ -4131,6 +4203,33 @@
"node": ">=6"
}
},
"node_modules/canvg": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz",
"integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==",
"license": "MIT",
"optional": true,
"dependencies": {
"@babel/runtime": "^7.12.5",
"@types/raf": "^3.4.0",
"core-js": "^3.8.3",
"raf": "^3.4.1",
"regenerator-runtime": "^0.13.7",
"rgbcolor": "^1.0.1",
"stackblur-canvas": "^2.0.0",
"svg-pathdata": "^6.0.3"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/canvg/node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"license": "MIT",
"optional": true
},
"node_modules/caseless": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
@@ -4599,6 +4698,18 @@
"node": ">= 0.6"
}
},
"node_modules/core-js": {
"version": "3.40.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.40.0.tgz",
"integrity": "sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
@@ -4643,6 +4754,16 @@
"node": ">= 8"
}
},
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"license": "MIT",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/css-select": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
@@ -6106,6 +6227,12 @@
"pend": "~1.2.0"
}
},
"node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
"license": "MIT"
},
"node_modules/figures": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
@@ -6701,6 +6828,20 @@
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
"integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="
},
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"license": "MIT",
"optional": true,
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/htmlparser2": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
@@ -7213,6 +7354,34 @@
"graceful-fs": "^4.1.6"
}
},
"node_modules/jspdf": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.0.tgz",
"integrity": "sha512-QvuQZvOI8CjfjVgtajdL0ihrDYif1cN5gXiF9lb9Pd9JOpmocvnNyFO9sdiJ/8RA5Bu8zyGOUjJLj5kiku16ug==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0",
"atob": "^2.1.2",
"btoa": "^1.2.1",
"fflate": "^0.8.1"
},
"optionalDependencies": {
"canvg": "^3.0.6",
"core-js": "^3.6.0",
"dompurify": "^3.2.4",
"html2canvas": "^1.0.0-rc.5"
}
},
"node_modules/jspdf/node_modules/dompurify": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz",
"integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optional": true,
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/jsprim": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz",
@@ -9029,7 +9198,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
"dev": true
"devOptional": true
},
"node_modules/periscopic": {
"version": "3.1.0",
@@ -9755,6 +9924,16 @@
"rimraf": "bin.js"
}
},
"node_modules/raf": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
"license": "MIT",
"optional": true,
"dependencies": {
"performance-now": "^2.1.0"
}
},
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@@ -9894,6 +10073,16 @@
"integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==",
"dev": true
},
"node_modules/rgbcolor": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
"optional": true,
"engines": {
"node": ">= 0.8.15"
}
},
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@@ -10785,6 +10974,16 @@
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
"dev": true
},
"node_modules/stackblur-canvas": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.1.14"
}
},
"node_modules/std-env": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz",
@@ -11166,6 +11365,16 @@
"@types/estree": "*"
}
},
"node_modules/svg-pathdata": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/symlink-or-copy": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/symlink-or-copy/-/symlink-or-copy-1.3.1.tgz",
@@ -11258,6 +11467,16 @@
"streamx": "^2.12.5"
}
},
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"license": "MIT",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -11597,6 +11816,16 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true
},
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"license": "MIT",
"optional": true,
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+3 -1
View File
@@ -1,6 +1,6 @@
{
"name": "open-webui",
"version": "0.5.15",
"version": "0.5.20",
"private": true,
"scripts": {
"dev": "npm run pyodide:fetch && vite dev --host",
@@ -51,6 +51,7 @@
},
"type": "module",
"dependencies": {
"@azure/msal-browser": "^4.5.0",
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-python": "^6.1.6",
"@codemirror/language-data": "^6.5.1",
@@ -84,6 +85,7 @@
"i18next-resources-to-backend": "^1.2.0",
"idb": "^7.1.1",
"js-sha256": "^0.10.1",
"jspdf": "^3.0.0",
"katex": "^0.16.21",
"kokoro-js": "^1.1.1",
"marked": "^9.1.0",
+13 -8
View File
@@ -7,12 +7,12 @@ authors = [
license = { file = "LICENSE" }
dependencies = [
"fastapi==0.115.7",
"uvicorn[standard]==0.30.6",
"uvicorn[standard]==0.34.0",
"pydantic==2.10.6",
"python-multipart==0.0.18",
"python-socketio==5.11.3",
"python-jose==3.3.0",
"python-jose==3.4.0",
"passlib[bcrypt]==1.7.4",
"requests==2.32.3",
@@ -21,14 +21,14 @@ dependencies = [
"aiocache",
"aiofiles",
"sqlalchemy==2.0.32",
"sqlalchemy==2.0.38",
"alembic==1.14.0",
"peewee==3.17.8",
"peewee==3.17.9",
"peewee-migrate==1.12.2",
"psycopg2-binary==2.9.9",
"pgvector==0.3.5",
"PyMySQL==1.1.1",
"bcrypt==4.2.0",
"bcrypt==4.3.0",
"pymongo",
"redis",
@@ -40,13 +40,16 @@ dependencies = [
"RestrictedPython==8.0",
"loguru==0.7.2",
"asgiref==3.8.1",
"openai",
"anthropic",
"google-generativeai==0.7.2",
"tiktoken",
"langchain==0.3.7",
"langchain-community==0.3.7",
"langchain==0.3.19",
"langchain-community==0.3.18",
"fake-useragent==1.5.1",
"chromadb==0.6.2",
@@ -54,6 +57,7 @@ dependencies = [
"qdrant-client~=1.12.0",
"opensearch-py==2.8.0",
"playwright==1.49.1",
"elasticsearch==8.17.1",
"transformers",
"sentence-transformers==3.3.1",
@@ -78,6 +82,7 @@ dependencies = [
"psutil",
"sentencepiece",
"soundfile==0.13.1",
"azure-ai-documentintelligence==1.0.0",
"opencv-python-headless==4.11.0.86",
"rapidocr-onnxruntime==1.3.24",
@@ -88,7 +93,7 @@ dependencies = [
"PyJWT[crypto]==2.10.1",
"authlib==1.4.1",
"black==24.8.0",
"black==25.1.0",
"langfuse==2.44.0",
"youtube-transcript-api==0.6.3",
"pytube==15.0.0",
+29 -9
View File
@@ -2,12 +2,14 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/png" href="/favicon/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="/favicon/favicon.svg" />
<link rel="shortcut icon" href="/favicon/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
<link rel="icon" type="image/png" href="/static/favicon.png" />
<link rel="icon" type="image/png" href="/static/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
<link rel="shortcut icon" href="/static/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-title" content="Open WebUI" />
<link rel="manifest" href="/favicon/site.webmanifest" />
<link rel="manifest" href="/manifest.json" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover"
@@ -74,6 +76,28 @@
}
}
});
function setSplashImage() {
const logo = document.getElementById('logo');
const isDarkMode = document.documentElement.classList.contains('dark');
if (isDarkMode) {
const darkImage = new Image();
darkImage.src = '/static/splash-dark.png';
darkImage.onload = () => {
logo.src = '/static/splash-dark.png';
logo.style.filter = ''; // Ensure no inversion is applied if splash-dark.png exists
};
darkImage.onerror = () => {
logo.style.filter = 'invert(1)'; // Invert image if splash-dark.png is missing
};
}
}
// Runs after classes are assigned
window.onload = setSplashImage;
})();
</script>
@@ -176,10 +200,6 @@
background: #000;
}
html.dark #splash-screen img {
filter: invert(1);
}
html.her #splash-screen {
background: #983724;
}
+7
View File
@@ -32,9 +32,15 @@ type ChunkConfigForm = {
chunk_overlap: number;
};
type DocumentIntelligenceConfigForm = {
key: string;
endpoint: string;
};
type ContentExtractConfigForm = {
engine: string;
tika_server_url: string | null;
document_intelligence_config: DocumentIntelligenceConfigForm | null;
};
type YoutubeConfigForm = {
@@ -46,6 +52,7 @@ type YoutubeConfigForm = {
type RAGConfigForm = {
pdf_extract_images?: boolean;
enable_google_drive_integration?: boolean;
enable_onedrive_integration?: boolean;
chunk?: ChunkConfigForm;
content_extraction?: ContentExtractConfigForm;
web_loader_ssl_verification?: boolean;
+28 -1
View File
@@ -1,5 +1,5 @@
<script>
import { getContext } from 'svelte';
import { getContext, onMount } from 'svelte';
const i18n = getContext('i18n');
import { WEBUI_BASE_URL } from '$lib/constants';
@@ -10,6 +10,32 @@
export let show = true;
export let getStartedHandler = () => {};
function setLogoImage() {
const logo = document.getElementById('logo');
if (logo) {
const isDarkMode = document.documentElement.classList.contains('dark');
if (isDarkMode) {
const darkImage = new Image();
darkImage.src = '/static/favicon-dark.png';
darkImage.onload = () => {
logo.src = '/static/favicon-dark.png';
logo.style.filter = ''; // Ensure no inversion is applied if splash-dark.png exists
};
darkImage.onerror = () => {
logo.style.filter = 'invert(1)'; // Invert image if splash-dark.png is missing
};
}
}
}
$: if (show) {
setLogoImage();
}
</script>
{#if show}
@@ -18,6 +44,7 @@
<div class="flex space-x-2">
<div class=" self-center">
<img
id="logo"
crossorigin="anonymous"
src="{WEBUI_BASE_URL}/static/favicon.png"
class=" w-6 rounded-full"
@@ -1,8 +1,7 @@
<script>
import { getContext, createEventDispatcher, onMount, tick } from 'svelte';
import { getContext, onMount, tick } from 'svelte';
import { goto } from '$app/navigation';
const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
import CodeEditor from '$lib/components/common/CodeEditor.svelte';
@@ -15,6 +14,8 @@
let loading = false;
let showConfirm = false;
export let onSave = () => {};
export let edit = false;
export let clone = false;
@@ -256,7 +257,7 @@ class Pipe:
const saveHandler = async () => {
loading = true;
dispatch('save', {
onSave({
id,
name,
meta,
@@ -371,10 +372,10 @@ class Pipe:
value={content}
lang="python"
{boilerplate}
on:change={(e) => {
_content = e.detail.value;
onChange={(e) => {
_content = e;
}}
on:save={async () => {
onSave={async () => {
if (formElement) {
formElement.requestSubmit();
}
@@ -45,6 +45,16 @@
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
<div class="mb-2.5">
<div class=" flex w-full justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Enable Code Execution')}
</div>
<Switch bind:state={config.ENABLE_CODE_EXECUTION} />
</div>
</div>
<div class="mb-2.5">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Code Execution Engine')}</div>
@@ -274,6 +274,7 @@
newConfig[newIdx] = OPENAI_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
});
OPENAI_API_CONFIGS = newConfig;
updateOpenAIHandler();
}}
/>
{/each}
File diff suppressed because it is too large Load Diff
@@ -103,10 +103,12 @@
<div class="overflow-y-scroll scrollbar-hidden h-full">
{#if evaluationConfig !== null}
<div class="">
<div class="text-sm font-medium mb-2">{$i18n.t('General Settings')}</div>
<div class="mb-3">
<div class=" mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
<div class=" mb-2">
<div class="flex justify-between items-center text-xs">
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
<div class="mb-2.5 flex w-full justify-between">
<div class=" text-xs font-medium">{$i18n.t('Arena Models')}</div>
<Tooltip content={$i18n.t(`Message rating should be enabled to use this feature`)}>
@@ -116,46 +118,50 @@
</div>
{#if evaluationConfig.ENABLE_EVALUATION_ARENA_MODELS}
<hr class=" border-gray-50 dark:border-gray-700/10 my-2" />
<div class="flex justify-between items-center mb-2">
<div class="text-sm font-medium">{$i18n.t('Manage Arena Models')}</div>
<div>
<Tooltip content={$i18n.t('Add Arena Model')}>
<button
class="p-1"
type="button"
on:click={() => {
showAddModel = true;
}}
>
<Plus />
</button>
</Tooltip>
</div>
</div>
<div class="flex flex-col gap-2">
{#if (evaluationConfig?.EVALUATION_ARENA_MODELS ?? []).length > 0}
{#each evaluationConfig.EVALUATION_ARENA_MODELS as model, index}
<Model
{model}
on:edit={(e) => {
editModelHandler(e.detail, index);
}}
on:delete={(e) => {
deleteModelHandler(index);
}}
/>
{/each}
{:else}
<div class=" text-center text-xs text-gray-500">
{$i18n.t(
`Using the default arena model with all models. Click the plus button to add custom models.`
)}
<div class="mb-3">
<div class=" mb-2.5 text-base font-medium flex justify-between items-center">
<div>
{$i18n.t('Manage')}
</div>
{/if}
<div>
<Tooltip content={$i18n.t('Add Arena Model')}>
<button
class="p-1"
type="button"
on:click={() => {
showAddModel = true;
}}
>
<Plus />
</button>
</Tooltip>
</div>
</div>
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
<div class="flex flex-col gap-2">
{#if (evaluationConfig?.EVALUATION_ARENA_MODELS ?? []).length > 0}
{#each evaluationConfig.EVALUATION_ARENA_MODELS as model, index}
<Model
{model}
on:edit={(e) => {
editModelHandler(e.detail, index);
}}
on:delete={(e) => {
deleteModelHandler(index);
}}
/>
{/each}
{:else}
<div class=" text-center text-xs text-gray-500">
{$i18n.t(
`Using the default arena model with all models. Click the plus button to add custom models.`
)}
</div>
{/if}
</div>
</div>
{/if}
</div>
@@ -1,4 +1,6 @@
<script lang="ts">
import DOMPurify from 'dompurify';
import { getBackendConfig, getVersionUpdates, getWebhookUrl, updateWebhookUrl } from '$lib/apis';
import {
getAdminConfig,
@@ -220,15 +222,44 @@
<div class="">
{$i18n.t('License')}
</div>
<a
class=" text-xs text-gray-500 hover:underline"
href="https://docs.openwebui.com/enterprise"
target="_blank"
>
{$i18n.t(
'Upgrade to a licensed plan for enhanced capabilities, including custom theming and branding, and dedicated support.'
)}
</a>
{#if $config?.license_metadata}
<a
href="https://docs.openwebui.com/enterprise"
target="_blank"
class="text-gray-500 mt-0.5"
>
<span class=" capitalize text-black dark:text-white"
>{$config?.license_metadata?.type}
license</span
>
registered to
<span class=" capitalize text-black dark:text-white"
>{$config?.license_metadata?.organization_name}</span
>
for
<span class=" font-medium text-black dark:text-white"
>{$config?.license_metadata?.seats ?? 'Unlimited'} users.</span
>
</a>
{#if $config?.license_metadata?.html}
<div class="mt-0.5">
{@html DOMPurify.sanitize($config?.license_metadata?.html)}
</div>
{/if}
{:else}
<a
class=" text-xs hover:underline"
href="https://docs.openwebui.com/enterprise"
target="_blank"
>
<span class="text-gray-500">
{$i18n.t(
'Upgrade to a licensed plan for enhanced capabilities, including custom theming and branding, and dedicated support.'
)}
</span>
</a>
{/if}
</div>
<!-- <button
@@ -16,6 +16,8 @@
import Spinner from '$lib/components/common/Spinner.svelte';
import Minus from '$lib/components/icons/Minus.svelte';
import Plus from '$lib/components/icons/Plus.svelte';
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
export let show = false;
export let initHandler = () => {};
@@ -26,6 +28,9 @@
let defaultModelIds = [];
let modelIds = [];
let sortKey = '';
let sortOrder = '';
let loading = false;
let showResetModal = false;
@@ -71,6 +76,9 @@
// Add remaining IDs not in MODEL_ORDER_LIST, sorted alphabetically
...allModelIds.filter((id) => !orderedSet.has(id)).sort((a, b) => a.localeCompare(b))
];
sortKey = '';
sortOrder = '';
};
const submitHandler = async () => {
loading = true;
@@ -145,9 +153,45 @@
>
<div>
<div class="flex flex-col w-full">
<div class="mb-1 flex justify-between">
<button
class="mb-1 flex gap-2"
type="button"
on:click={() => {
sortKey = 'model';
if (sortOrder === 'asc') {
sortOrder = 'desc';
} else {
sortOrder = 'asc';
}
modelIds = modelIds
.filter((id) => id !== '')
.sort((a, b) => {
const nameA = $models.find((model) => model.id === a)?.name || a;
const nameB = $models.find((model) => model.id === b)?.name || b;
return sortOrder === 'desc'
? nameA.localeCompare(nameB)
: nameB.localeCompare(nameA);
});
}}
>
<div class="text-xs text-gray-500">{$i18n.t('Reorder Models')}</div>
</div>
{#if sortKey === 'model'}
<span class="font-normal self-center">
{#if sortOrder === 'asc'}
<ChevronUp className="size-3" />
{:else}
<ChevronDown className="size-3" />
{/if}
</span>
{:else}
<span class="invisible">
<ChevronUp className="size-3" />
</span>
{/if}
</button>
<ModelList bind:modelIds />
</div>
@@ -21,14 +21,24 @@
modelIds = modelList;
};
onMount(() => {
sortable = Sortable.create(modelListElement, {
animation: 150,
onUpdate: async (event) => {
positionChangeHandler();
}
});
});
$: if (modelIds) {
init();
}
const init = () => {
if (sortable) {
sortable.destroy();
}
if (modelListElement) {
sortable = Sortable.create(modelListElement, {
animation: 150,
onUpdate: async (event) => {
positionChangeHandler();
}
});
}
};
</script>
{#if modelIds.length > 0}
+332 -288
View File
@@ -29,7 +29,8 @@
'tavily',
'jina',
'bing',
'exa'
'exa',
'perplexity'
];
let youtubeLanguage = 'en';
@@ -85,384 +86,427 @@
>
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
{#if webConfig}
<div>
<div class=" mb-1 text-sm font-medium">
{$i18n.t('Web Search')}
</div>
<div class="">
<div class="mb-3">
<div class=" mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
<div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Enable Web Search')}
{$i18n.t('Web Search')}
</div>
<div class="flex items-center relative">
<Switch bind:state={webConfig.search.enabled} />
</div>
<Switch bind:state={webConfig.search.enabled} />
</div>
</div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Web Search Engine')}</div>
<div class="flex items-center relative">
<select
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
bind:value={webConfig.search.engine}
placeholder={$i18n.t('Select a engine')}
required
>
<option disabled selected value="">{$i18n.t('Select a engine')}</option>
{#each webSearchEngines as engine}
<option value={engine}>{engine}</option>
{/each}
</select>
<div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Web Search Engine')}
</div>
<div class="flex items-center relative">
<select
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
bind:value={webConfig.search.engine}
placeholder={$i18n.t('Select a engine')}
required
>
<option disabled selected value="">{$i18n.t('Select a engine')}</option>
{#each webSearchEngines as engine}
<option value={engine}>{engine}</option>
{/each}
</select>
</div>
</div>
</div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Full Context Mode')}</div>
<div class="flex items-center relative">
<Tooltip
content={webConfig.RAG_WEB_SEARCH_FULL_CONTEXT
? 'Inject the entire web results as context for comprehensive processing, this is recommended for complex queries.'
: 'Default to segmented retrieval for focused and relevant content extraction, this is recommended for most cases.'}
>
<Switch bind:state={webConfig.RAG_WEB_SEARCH_FULL_CONTEXT} />
</Tooltip>
</div>
</div>
{#if webConfig.search.engine !== ''}
<div class="mt-1.5">
{#if webConfig.search.engine !== ''}
{#if webConfig.search.engine === 'searxng'}
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Searxng Query URL')}
</div>
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Searxng Query URL')}
</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"
type="text"
placeholder={$i18n.t('Enter Searxng Query URL')}
bind:value={webConfig.search.searxng_query_url}
autocomplete="off"
/>
<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"
type="text"
placeholder={$i18n.t('Enter Searxng Query URL')}
bind:value={webConfig.search.searxng_query_url}
autocomplete="off"
/>
</div>
</div>
</div>
</div>
{:else if webConfig.search.engine === 'google_pse'}
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Google PSE API Key')}
</div>
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Google PSE API Key')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Google PSE API Key')}
bind:value={webConfig.search.google_pse_api_key}
/>
</div>
<div class="mt-1.5">
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Google PSE Engine Id')}
<SensitiveInput
placeholder={$i18n.t('Enter Google PSE API Key')}
bind:value={webConfig.search.google_pse_api_key}
/>
</div>
<div class="mt-1.5">
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Google PSE Engine Id')}
</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"
type="text"
placeholder={$i18n.t('Enter Google PSE Engine Id')}
bind:value={webConfig.search.google_pse_engine_id}
autocomplete="off"
/>
<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"
type="text"
placeholder={$i18n.t('Enter Google PSE Engine Id')}
bind:value={webConfig.search.google_pse_engine_id}
autocomplete="off"
/>
</div>
</div>
</div>
</div>
{:else if webConfig.search.engine === 'brave'}
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Brave Search API Key')}
</div>
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Brave Search API Key')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Brave Search API Key')}
bind:value={webConfig.search.brave_search_api_key}
/>
<SensitiveInput
placeholder={$i18n.t('Enter Brave Search API Key')}
bind:value={webConfig.search.brave_search_api_key}
/>
</div>
</div>
{:else if webConfig.search.engine === 'kagi'}
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Kagi Search API Key')}
</div>
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Kagi Search API Key')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Kagi Search API Key')}
bind:value={webConfig.search.kagi_search_api_key}
/>
<SensitiveInput
placeholder={$i18n.t('Enter Kagi Search API Key')}
bind:value={webConfig.search.kagi_search_api_key}
/>
</div>
.
</div>
{:else if webConfig.search.engine === 'mojeek'}
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Mojeek Search API Key')}
</div>
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Mojeek Search API Key')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Mojeek Search API Key')}
bind:value={webConfig.search.mojeek_search_api_key}
/>
<SensitiveInput
placeholder={$i18n.t('Enter Mojeek Search API Key')}
bind:value={webConfig.search.mojeek_search_api_key}
/>
</div>
</div>
{:else if webConfig.search.engine === 'bocha'}
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Bocha Search API Key')}
</div>
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Bocha Search API Key')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Bocha Search API Key')}
bind:value={webConfig.search.bocha_search_api_key}
/>
<SensitiveInput
placeholder={$i18n.t('Enter Bocha Search API Key')}
bind:value={webConfig.search.bocha_search_api_key}
/>
</div>
</div>
{:else if webConfig.search.engine === 'serpstack'}
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Serpstack API Key')}
</div>
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Serpstack API Key')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Serpstack API Key')}
bind:value={webConfig.search.serpstack_api_key}
/>
<SensitiveInput
placeholder={$i18n.t('Enter Serpstack API Key')}
bind:value={webConfig.search.serpstack_api_key}
/>
</div>
</div>
{:else if webConfig.search.engine === 'serper'}
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Serper API Key')}
</div>
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Serper API Key')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Serper API Key')}
bind:value={webConfig.search.serper_api_key}
/>
<SensitiveInput
placeholder={$i18n.t('Enter Serper API Key')}
bind:value={webConfig.search.serper_api_key}
/>
</div>
</div>
{:else if webConfig.search.engine === 'serply'}
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Serply API Key')}
</div>
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Serply API Key')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Serply API Key')}
bind:value={webConfig.search.serply_api_key}
/>
<SensitiveInput
placeholder={$i18n.t('Enter Serply API Key')}
bind:value={webConfig.search.serply_api_key}
/>
</div>
</div>
{:else if webConfig.search.engine === 'searchapi'}
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('SearchApi API Key')}
</div>
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('SearchApi API Key')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter SearchApi API Key')}
bind:value={webConfig.search.searchapi_api_key}
/>
</div>
<div class="mt-1.5">
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('SearchApi Engine')}
<SensitiveInput
placeholder={$i18n.t('Enter SearchApi API Key')}
bind:value={webConfig.search.searchapi_api_key}
/>
</div>
<div class="mt-1.5">
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('SearchApi Engine')}
</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"
type="text"
placeholder={$i18n.t('Enter SearchApi Engine')}
bind:value={webConfig.search.searchapi_engine}
autocomplete="off"
/>
<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"
type="text"
placeholder={$i18n.t('Enter SearchApi Engine')}
bind:value={webConfig.search.searchapi_engine}
autocomplete="off"
/>
</div>
</div>
</div>
</div>
{:else if webConfig.search.engine === 'serpapi'}
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('SerpApi API Key')}
</div>
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('SerpApi API Key')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter SerpApi API Key')}
bind:value={webConfig.search.serpapi_api_key}
/>
</div>
<div class="mt-1.5">
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('SerpApi Engine')}
<SensitiveInput
placeholder={$i18n.t('Enter SerpApi API Key')}
bind:value={webConfig.search.serpapi_api_key}
/>
</div>
<div class="mt-1.5">
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('SerpApi Engine')}
</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"
type="text"
placeholder={$i18n.t('Enter SerpApi Engine')}
bind:value={webConfig.search.serpapi_engine}
autocomplete="off"
/>
<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"
type="text"
placeholder={$i18n.t('Enter SerpApi Engine')}
bind:value={webConfig.search.serpapi_engine}
autocomplete="off"
/>
</div>
</div>
</div>
</div>
{:else if webConfig.search.engine === 'tavily'}
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Tavily API Key')}
</div>
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Tavily API Key')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Tavily API Key')}
bind:value={webConfig.search.tavily_api_key}
/>
<SensitiveInput
placeholder={$i18n.t('Enter Tavily API Key')}
bind:value={webConfig.search.tavily_api_key}
/>
</div>
</div>
{:else if webConfig.search.engine === 'jina'}
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Jina API Key')}
</div>
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Jina API Key')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Jina API Key')}
bind:value={webConfig.search.jina_api_key}
/>
<SensitiveInput
placeholder={$i18n.t('Enter Jina API Key')}
bind:value={webConfig.search.jina_api_key}
/>
</div>
</div>
{:else if webConfig.search.engine === 'exa'}
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Exa API Key')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Exa API Key')}
bind:value={webConfig.search.exa_api_key}
/>
</div>
</div>
{:else if webConfig.search.engine === 'perplexity'}
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Exa API Key')}
{$i18n.t('Perplexity API Key')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Exa API Key')}
bind:value={webConfig.search.exa_api_key}
placeholder={$i18n.t('Enter Perplexity API Key')}
bind:value={webConfig.search.perplexity_api_key}
/>
</div>
{:else if webConfig.search.engine === 'bing'}
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Bing Search V7 Endpoint')}
</div>
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Bing Search V7 Endpoint')}
</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"
type="text"
placeholder={$i18n.t('Enter Bing Search V7 Endpoint')}
bind:value={webConfig.search.bing_search_v7_endpoint}
autocomplete="off"
/>
<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"
type="text"
placeholder={$i18n.t('Enter Bing Search V7 Endpoint')}
bind:value={webConfig.search.bing_search_v7_endpoint}
autocomplete="off"
/>
</div>
</div>
</div>
</div>
<div class="mt-2">
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Bing Search V7 Subscription Key')}
<div class="mt-2">
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Bing Search V7 Subscription Key')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Bing Search V7 Subscription Key')}
bind:value={webConfig.search.bing_search_v7_subscription_key}
/>
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Bing Search V7 Subscription Key')}
bind:value={webConfig.search.bing_search_v7_subscription_key}
/>
</div>
{/if}
</div>
{/if}
{/if}
{#if webConfig.search.enabled}
<div class="mt-2 flex gap-2 mb-1">
<div class="w-full">
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Search Result Count')}
{#if webConfig.search.enabled}
<div class="mb-2.5 flex w-full flex-col">
<div class="flex gap-2">
<div class="w-full">
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Search Result Count')}
</div>
<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"
placeholder={$i18n.t('Search Result Count')}
bind:value={webConfig.search.result_count}
required
/>
</div>
<div class="w-full">
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Concurrent Requests')}
</div>
<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"
placeholder={$i18n.t('Concurrent Requests')}
bind:value={webConfig.search.concurrent_requests}
required
/>
</div>
</div>
</div>
<div class="mb-2.5 flex w-full flex-col">
<div class=" text-xs font-medium mb-1">
{$i18n.t('Domain Filter List')}
</div>
<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"
placeholder={$i18n.t('Search Result Count')}
bind:value={webConfig.search.result_count}
required
placeholder={$i18n.t(
'Enter domains separated by commas (e.g., example.com,site.org)'
)}
bind:value={webConfig.search.domain_filter_list}
/>
</div>
{/if}
<div class="w-full">
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Concurrent Requests')}
</div>
<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"
placeholder={$i18n.t('Concurrent Requests')}
bind:value={webConfig.search.concurrent_requests}
required
/>
<div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">
<Tooltip content={$i18n.t('Full Context Mode')} placement="top-start">
{$i18n.t('Bypass Embedding and Retrieval')}
</Tooltip>
</div>
<div class="flex items-center relative">
<Tooltip
content={webConfig.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL
? 'Inject the entire content as context for comprehensive processing, this is recommended for complex queries.'
: 'Default to segmented retrieval for focused and relevant content extraction, this is recommended for most cases.'}
>
<Switch bind:state={webConfig.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL} />
</Tooltip>
</div>
</div>
<div class="mt-2">
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Domain Filter List')}
<div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Trust Proxy Environment')}
</div>
<div class="flex items-center relative">
<Tooltip
content={webConfig.search.trust_env
? 'Use proxy designated by http_proxy and https_proxy environment variables to fetch page contents'
: 'Use no proxy to fetch page contents.'}
>
<Switch bind:state={webConfig.search.trust_env} />
</Tooltip>
</div>
<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"
placeholder={$i18n.t(
'Enter domains separated by commas (e.g., example.com,site.org)'
)}
bind:value={webConfig.search.domain_filter_list}
/>
</div>
{/if}
</div>
<hr class="border-gray-100 dark:border-gray-850 my-2" />
<div>
<div class=" mb-1 text-sm font-medium">
{$i18n.t('Web Loader Settings')}
</div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class="mb-3">
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Loader')}</div>
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
<div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Bypass SSL verification for Websites')}
</div>
<button
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
webConfig.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION =
!webConfig.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION;
submitHandler();
}}
type="button"
>
{#if webConfig.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION === false}
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if}
</button>
<div class="flex items-center relative">
<Switch bind:state={webConfig.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION} />
</div>
</div>
</div>
<div class=" mt-2 mb-1 text-sm font-medium">
{$i18n.t('Youtube Loader Settings')}
</div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Language')}</div>
<div class=" flex-1 self-center">
<div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Youtube Language')}
</div>
<div class="flex items-center relative">
<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"
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden"
type="text"
placeholder={$i18n.t('Enter language codes')}
bind:value={youtubeLanguage}
@@ -470,14 +514,14 @@
/>
</div>
</div>
</div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Proxy URL')}</div>
<div class=" flex-1 self-center">
<div class=" mb-2.5 flex flex-col w-full justify-between">
<div class=" mb-1 text-xs font-medium">
{$i18n.t('Youtube Proxy URL')}
</div>
<div class="flex items-center relative">
<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"
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden"
type="text"
placeholder={$i18n.t('Enter proxy URL (e.g. https://user:password@host:port)')}
bind:value={youtubeProxyUrl}
+34 -2
View File
@@ -28,6 +28,7 @@
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
import About from '$lib/components/chat/Settings/About.svelte';
import Banner from '$lib/components/common/Banner.svelte';
const i18n = getContext('i18n');
@@ -124,12 +125,43 @@
/>
<UserChatsModal bind:show={showUserChatsModal} user={selectedUser} />
{#if ($config?.license_metadata?.seats ?? null) !== null && users.length > $config?.license_metadata?.seats}
<div class=" mt-1 mb-2 text-xs text-red-500">
<Banner
className="mx-0"
banner={{
type: 'error',
title: 'License Error',
content:
'Exceeded the number of seats in your license. Please contact support to increase the number of seats.',
dismissable: true
}}
/>
</div>
{/if}
<div class="mt-0.5 mb-2 gap-1 flex flex-col md:flex-row justify-between">
<div class="flex md:self-center text-lg font-medium px-0.5">
{$i18n.t('Users')}
<div class="flex-shrink-0">
{$i18n.t('Users')}
</div>
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
<span class="text-lg font-medium text-gray-500 dark:text-gray-300">{users.length}</span>
{#if ($config?.license_metadata?.seats ?? null) !== null}
{#if users.length > $config?.license_metadata?.seats}
<span class="text-lg font-medium text-red-500"
>{users.length} of {$config?.license_metadata?.seats}
<span class="text-sm font-normal">available users</span></span
>
{:else}
<span class="text-lg font-medium text-gray-500 dark:text-gray-300"
>{users.length} of {$config?.license_metadata?.seats}
<span class="text-sm font-normal">available users</span></span
>
{/if}
{:else}
<span class="text-lg font-medium text-gray-500 dark:text-gray-300">{users.length}</span>
{/if}
</div>
<div class="flex gap-1">
@@ -157,22 +157,6 @@
}
files = [...files, fileItem];
// Check if the file is an audio file and transcribe/convert it to text file
if (['audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/x-m4a'].includes(file['type'])) {
const res = await transcribeAudio(localStorage.token, file).catch((error) => {
toast.error(`${error}`);
return null;
});
if (res) {
console.log(res);
const blob = new Blob([res.text], { type: 'text/plain' });
file = blobToFile(blob, `${file.name}.txt`);
fileItem.name = file.name;
fileItem.size = file.size;
}
}
try {
// During the file upload, file content is automatically extracted.
+46 -5
View File
@@ -187,15 +187,20 @@
setToolIds();
}
$: if (atSelectedModel || selectedModels) {
setToolIds();
}
const setToolIds = async () => {
if (!$tools) {
tools.set(await getTools(localStorage.token));
}
if (selectedModels.length !== 1) {
if (selectedModels.length !== 1 && !atSelectedModel) {
return;
}
const model = $models.find((m) => m.id === selectedModels[0]);
const model = atSelectedModel ?? $models.find((m) => m.id === selectedModels[0]);
if (model) {
selectedToolIds = (model?.info?.meta?.toolIds ?? []).filter((id) =>
$tools.find((t) => t.id === id)
@@ -836,6 +841,7 @@
content: m.content,
info: m.info ? m.info : undefined,
timestamp: m.timestamp,
...(m.usage ? { usage: m.usage } : {}),
...(m.sources ? { sources: m.sources } : {})
})),
model_item: $models.find((m) => m.id === modelId),
@@ -1273,7 +1279,9 @@
const chatInputElement = document.getElementById('chat-input');
if (chatInputElement) {
await tick();
chatInputElement.style.height = '';
chatInputElement.style.height = Math.min(chatInputElement.scrollHeight, 320) + 'px';
}
const _files = JSON.parse(JSON.stringify(files));
@@ -1488,7 +1496,10 @@
params?.system ?? $settings?.system ?? '',
$user.name,
$settings?.userLocation
? await getAndUpdateUserLocation(localStorage.token)
? await getAndUpdateUserLocation(localStorage.token).catch((err) => {
console.error(err);
return undefined;
})
: undefined
)}${
(responseMessage?.userContext ?? null)
@@ -1573,7 +1584,12 @@
variables: {
...getPromptVariables(
$user.name,
$settings?.userLocation ? await getAndUpdateUserLocation(localStorage.token) : undefined
$settings?.userLocation
? await getAndUpdateUserLocation(localStorage.token).catch((err) => {
console.error(err);
return undefined;
})
: undefined
)
},
model_item: $models.find((m) => m.id === model.id),
@@ -1921,9 +1937,33 @@
<PaneGroup direction="horizontal" class="w-full h-full">
<Pane defaultSize={50} class="h-full flex w-full relative">
{#if $banners.length > 0 && !history.currentId && !$chatId && selectedModels.length <= 1}
{#if !history.currentId && !$chatId && selectedModels.length <= 1 && ($banners.length > 0 || ($config?.license_metadata?.type ?? null) === 'trial' || (($config?.license_metadata?.seats ?? null) !== null && $config?.user_count > $config?.license_metadata?.seats))}
<div class="absolute top-12 left-0 right-0 w-full z-30">
<div class=" flex flex-col gap-1 w-full">
{#if ($config?.license_metadata?.type ?? null) === 'trial'}
<Banner
banner={{
type: 'info',
title: 'Trial License',
content: $i18n.t(
'You are currently using a trial license. Please contact support to upgrade your license.'
)
}}
/>
{/if}
{#if ($config?.license_metadata?.seats ?? null) !== null && $config?.user_count > $config?.license_metadata?.seats}
<Banner
banner={{
type: 'error',
title: 'License Error',
content: $i18n.t(
'Exceeded the number of seats in your license. Please contact support to increase the number of seats.'
)
}}
/>
{/if}
{#each $banners.filter( (b) => (b.dismissible ? !JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]').includes(b.id) : true) ) as banner}
<Banner
{banner}
@@ -1965,6 +2005,7 @@
bind:autoScroll
bind:prompt
{selectedModels}
{atSelectedModel}
{sendPrompt}
{showMessage}
{submitMessage}
@@ -16,6 +16,7 @@
export let modelIds = [];
export let models = [];
export let atSelectedModel;
export let submitPrompt;
@@ -126,7 +127,8 @@
<div class=" w-full font-primary" in:fade={{ duration: 200, delay: 300 }}>
<Suggestions
className="grid grid-cols-2"
suggestionPrompts={models[selectedModelIdx]?.info?.meta?.suggestion_prompts ??
suggestionPrompts={atSelectedModel?.info?.meta?.suggestion_prompts ??
models[selectedModelIdx]?.info?.meta?.suggestion_prompts ??
$config?.default_prompt_suggestions ??
[]}
on:select={(e) => {
@@ -121,7 +121,8 @@
toast.error('Model not selected');
return;
}
prompt = `Explain this section to me in more detail\n\n\`\`\`\n${selectedText}\n\`\`\``;
const explainText = $i18n.t('Explain this section to me in more detail');
prompt = `${explainText}\n\n\`\`\`\n${selectedText}\n\`\`\``;
responseContent = '';
const [res, controller] = await chatCompletion(localStorage.token, {
@@ -246,7 +247,7 @@
>
<ChatBubble className="size-3 shrink-0" />
<div class="shrink-0">Ask</div>
<div class="shrink-0">{$i18n.t('Ask')}</div>
</button>
<button
class="px-1 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-sm flex items-center gap-1 min-w-fit"
@@ -257,7 +258,7 @@
>
<LightBlub className="size-3 shrink-0" />
<div class="shrink-0">Explain</div>
<div class="shrink-0">{$i18n.t('Explain')}</div>
</button>
</div>
{:else}
+129 -91
View File
@@ -2,6 +2,7 @@
import { toast } from 'svelte-sonner';
import { v4 as uuidv4 } from 'uuid';
import { createPicker, getAuthToken } from '$lib/utils/google-drive-picker';
import { pickAndDownloadFile } from '$lib/utils/onedrive-file-picker';
import { onMount, tick, getContext, createEventDispatcher, onDestroy } from 'svelte';
const dispatch = createEventDispatcher();
@@ -84,6 +85,8 @@
let loaded = false;
let recording = false;
let isComposing = false;
let chatInputContainerElement;
let chatInputElement;
@@ -173,22 +176,6 @@
}
files = [...files, fileItem];
// Check if the file is an audio file and transcribe/convert it to text file
if (['audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/x-m4a'].includes(file['type'])) {
const res = await transcribeAudio(localStorage.token, file).catch((error) => {
toast.error(`${error}`);
return null;
});
if (res) {
console.log(res);
const blob = new Blob([res.text], { type: 'text/plain' });
file = blobToFile(blob, `${file.name}.txt`);
fileItem.name = file.name;
fileItem.size = file.size;
}
}
try {
// During the file upload, file content is automatically extracted.
@@ -691,12 +678,13 @@
bind:value={prompt}
id="chat-input"
messageInput={true}
shiftEnter={!$mobile ||
!(
'ontouchstart' in window ||
navigator.maxTouchPoints > 0 ||
navigator.msMaxTouchPoints > 0
)}
shiftEnter={!($settings?.ctrlEnterToSend ?? false) &&
(!$mobile ||
!(
'ontouchstart' in window ||
navigator.maxTouchPoints > 0 ||
navigator.msMaxTouchPoints > 0
))}
placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
largeTextAsFile={$settings?.largeTextAsFile ?? false}
autocomplete={$config?.features.enable_autocomplete_generation}
@@ -721,6 +709,8 @@
console.log(res);
return res;
}}
oncompositionstart={() => (isComposing = true)}
oncompositionend={() => (isComposing = false)}
on:keydown={async (e) => {
e = e.detail.event;
@@ -820,15 +810,24 @@
navigator.msMaxTouchPoints > 0
)
) {
// Prevent Enter key from creating a new line
// Uses keyCode '13' for Enter key for chinese/japanese keyboards
if (e.keyCode === 13 && !e.shiftKey) {
e.preventDefault();
if (isComposing) {
return;
}
// Submit the prompt when Enter key is pressed
if (prompt !== '' && e.keyCode === 13 && !e.shiftKey) {
dispatch('submit', prompt);
// Uses keyCode '13' for Enter key for chinese/japanese keyboards.
//
// Depending on the user's settings, it will send the message
// either when Enter is pressed or when Ctrl+Enter is pressed.
const enterPressed =
($settings?.ctrlEnterToSend ?? false)
? (e.key === 'Enter' || e.keyCode === 13) && isCtrlPressed
: (e.key === 'Enter' || e.keyCode === 13) && !e.shiftKey;
if (enterPressed) {
e.preventDefault();
if (prompt !== '' || files.length > 0) {
dispatch('submit', prompt);
}
}
}
}
@@ -891,34 +890,19 @@
class="scrollbar-hidden bg-transparent dark:text-gray-100 outline-hidden w-full pt-3 px-1 resize-none"
placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
bind:value={prompt}
on:keypress={(e) => {
if (
!$mobile ||
!(
'ontouchstart' in window ||
navigator.maxTouchPoints > 0 ||
navigator.msMaxTouchPoints > 0
)
) {
// Prevent Enter key from creating a new line
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
}
// Submit the prompt when Enter key is pressed
if (prompt !== '' && e.key === 'Enter' && !e.shiftKey) {
dispatch('submit', prompt);
}
}
}}
on:compositionstart={() => (isComposing = true)}
on:compositionend={() => (isComposing = false)}
on:keydown={async (e) => {
const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
console.log('keydown', e);
const commandsContainerElement =
document.getElementById('commands-container');
if (e.key === 'Escape') {
stopResponse();
}
// Command/Ctrl + Shift + Enter to submit a message pair
if (isCtrlPressed && e.key === 'Enter' && e.shiftKey) {
e.preventDefault();
@@ -954,51 +938,87 @@
editButton?.click();
}
if (commandsContainerElement && e.key === 'ArrowUp') {
e.preventDefault();
commandsElement.selectUp();
if (commandsContainerElement) {
if (commandsContainerElement && e.key === 'ArrowUp') {
e.preventDefault();
commandsElement.selectUp();
const commandOptionButton = [
...document.getElementsByClassName('selected-command-option-button')
]?.at(-1);
commandOptionButton.scrollIntoView({ block: 'center' });
}
const commandOptionButton = [
...document.getElementsByClassName('selected-command-option-button')
]?.at(-1);
commandOptionButton.scrollIntoView({ block: 'center' });
}
if (commandsContainerElement && e.key === 'ArrowDown') {
e.preventDefault();
commandsElement.selectDown();
if (commandsContainerElement && e.key === 'ArrowDown') {
e.preventDefault();
commandsElement.selectDown();
const commandOptionButton = [
...document.getElementsByClassName('selected-command-option-button')
]?.at(-1);
commandOptionButton.scrollIntoView({ block: 'center' });
}
const commandOptionButton = [
...document.getElementsByClassName('selected-command-option-button')
]?.at(-1);
commandOptionButton.scrollIntoView({ block: 'center' });
}
if (commandsContainerElement && e.key === 'Enter') {
e.preventDefault();
if (commandsContainerElement && e.key === 'Enter') {
e.preventDefault();
const commandOptionButton = [
...document.getElementsByClassName('selected-command-option-button')
]?.at(-1);
const commandOptionButton = [
...document.getElementsByClassName('selected-command-option-button')
]?.at(-1);
if (e.shiftKey) {
prompt = `${prompt}\n`;
} else if (commandOptionButton) {
commandOptionButton?.click();
} else {
document.getElementById('send-message-button')?.click();
}
}
if (commandsContainerElement && e.key === 'Tab') {
e.preventDefault();
const commandOptionButton = [
...document.getElementsByClassName('selected-command-option-button')
]?.at(-1);
if (e.shiftKey) {
prompt = `${prompt}\n`;
} else if (commandOptionButton) {
commandOptionButton?.click();
} else {
document.getElementById('send-message-button')?.click();
}
} else {
if (
!$mobile ||
!(
'ontouchstart' in window ||
navigator.maxTouchPoints > 0 ||
navigator.msMaxTouchPoints > 0
)
) {
if (isComposing) {
return;
}
console.log('keypress', e);
// Prevent Enter key from creating a new line
const isCtrlPressed = e.ctrlKey || e.metaKey;
const enterPressed =
($settings?.ctrlEnterToSend ?? false)
? (e.key === 'Enter' || e.keyCode === 13) && isCtrlPressed
: (e.key === 'Enter' || e.keyCode === 13) && !e.shiftKey;
console.log('Enter pressed:', enterPressed);
if (enterPressed) {
e.preventDefault();
}
// Submit the prompt when Enter key is pressed
if ((prompt !== '' || files.length > 0) && enterPressed) {
dispatch('submit', prompt);
}
}
}
if (commandsContainerElement && e.key === 'Tab') {
e.preventDefault();
const commandOptionButton = [
...document.getElementsByClassName('selected-command-option-button')
]?.at(-1);
commandOptionButton?.click();
} else if (e.key === 'Tab') {
if (e.key === 'Tab') {
const words = findWordIndices(prompt);
if (words.length > 0) {
@@ -1108,6 +1128,21 @@
);
}
}}
uploadOneDriveHandler={async () => {
try {
const fileData = await pickAndDownloadFile();
if (fileData) {
const file = new File([fileData.blob], fileData.name, {
type: fileData.blob.type || 'application/octet-stream'
});
await uploadFileHandler(file);
} else {
console.log('No file was selected from OneDrive');
}
} catch (error) {
console.error('OneDrive Error:', error);
}
}}
onClose={async () => {
await tick();
@@ -1285,14 +1320,17 @@
stream = null;
if (!$TTSWorker) {
await TTSWorker.set(
new KokoroWorker({
dtype: $settings.audio?.tts?.engineConfig?.dtype ?? 'fp32'
})
);
if ($settings.audio?.tts?.engine === 'browser-kokoro') {
// If the user has not initialized the TTS worker, initialize it
if (!$TTSWorker) {
await TTSWorker.set(
new KokoroWorker({
dtype: $settings.audio?.tts?.engineConfig?.dtype ?? 'fp32'
})
);
await $TTSWorker.init();
await $TTSWorker.init();
}
}
showCallOverlay.set(true);
@@ -231,7 +231,6 @@
mediaRecorder.onstart = () => {
console.log('Recording started');
audioChunks = [];
analyseAudio(audioStream);
};
mediaRecorder.ondataavailable = (event) => {
@@ -245,7 +244,7 @@
stopRecordingCallback();
};
mediaRecorder.start();
analyseAudio(audioStream);
}
};
@@ -321,6 +320,9 @@
if (hasSound) {
// BIG RED TEXT
console.log('%c%s', 'color: red; font-size: 20px;', '🔊 Sound detected');
if (mediaRecorder && mediaRecorder.state !== 'recording') {
mediaRecorder.start();
}
if (!hasStartedSpeaking) {
hasStartedSpeaking = true;

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