github-actions[bot] commited on
Commit
d1803fa
·
0 Parent(s):

GitHub deploy: fbc6d523c33fe9dbf84a4d24a7dc64efec35791b

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +20 -0
  2. .env.example +22 -0
  3. .eslintignore +13 -0
  4. .eslintrc.cjs +31 -0
  5. .gitattributes +44 -0
  6. .github/workflows/deploy-to-hf-spaces.yml +63 -0
  7. .gitignore +312 -0
  8. .npmrc +1 -0
  9. .prettierignore +316 -0
  10. .prettierrc +10 -0
  11. CHANGELOG.md +0 -0
  12. CODE_OF_CONDUCT.md +99 -0
  13. CONTRIBUTOR_LICENSE_AGREEMENT +7 -0
  14. Dockerfile +178 -0
  15. INSTALLATION.md +35 -0
  16. LICENSE +33 -0
  17. LICENSE_HISTORY +53 -0
  18. Makefile +33 -0
  19. README.md +278 -0
  20. TROUBLESHOOTING.md +36 -0
  21. backend/.dockerignore +14 -0
  22. backend/.gitignore +12 -0
  23. backend/dev.sh +3 -0
  24. backend/open_webui/__init__.py +103 -0
  25. backend/open_webui/alembic.ini +114 -0
  26. backend/open_webui/config.py +0 -0
  27. backend/open_webui/constants.py +121 -0
  28. backend/open_webui/env.py +773 -0
  29. backend/open_webui/functions.py +319 -0
  30. backend/open_webui/internal/db.py +164 -0
  31. backend/open_webui/internal/migrations/001_initial_schema.py +254 -0
  32. backend/open_webui/internal/migrations/002_add_local_sharing.py +48 -0
  33. backend/open_webui/internal/migrations/003_add_auth_api_key.py +48 -0
  34. backend/open_webui/internal/migrations/004_add_archived.py +46 -0
  35. backend/open_webui/internal/migrations/005_add_updated_at.py +130 -0
  36. backend/open_webui/internal/migrations/006_migrate_timestamps_and_charfields.py +130 -0
  37. backend/open_webui/internal/migrations/007_add_user_last_active_at.py +79 -0
  38. backend/open_webui/internal/migrations/008_add_memory.py +53 -0
  39. backend/open_webui/internal/migrations/009_add_models.py +61 -0
  40. backend/open_webui/internal/migrations/010_migrate_modelfiles_to_models.py +130 -0
  41. backend/open_webui/internal/migrations/011_add_user_settings.py +48 -0
  42. backend/open_webui/internal/migrations/012_add_tools.py +61 -0
  43. backend/open_webui/internal/migrations/013_add_user_info.py +48 -0
  44. backend/open_webui/internal/migrations/014_add_files.py +55 -0
  45. backend/open_webui/internal/migrations/015_add_functions.py +61 -0
  46. backend/open_webui/internal/migrations/016_add_valves_and_is_active.py +50 -0
  47. backend/open_webui/internal/migrations/017_add_user_oauth_sub.py +45 -0
  48. backend/open_webui/internal/migrations/018_add_function_is_global.py +49 -0
  49. backend/open_webui/internal/wrappers.py +90 -0
  50. backend/open_webui/main.py +1976 -0
.dockerignore ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .github
2
+ .DS_Store
3
+ docs
4
+ kubernetes
5
+ node_modules
6
+ /.svelte-kit
7
+ /package
8
+ .env
9
+ .env.*
10
+ vite.config.js.timestamp-*
11
+ vite.config.ts.timestamp-*
12
+ __pycache__
13
+ .idea
14
+ venv
15
+ _old
16
+ uploads
17
+ .ipynb_checkpoints
18
+ **/*.db
19
+ _test
20
+ backend/data/*
.env.example ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ollama URL for the backend to connect
2
+ # The path '/ollama' will be redirected to the specified backend URL
3
+ OLLAMA_BASE_URL='http://localhost:11434'
4
+
5
+ OPENAI_API_BASE_URL=''
6
+ OPENAI_API_KEY=''
7
+
8
+ # AUTOMATIC1111_BASE_URL="http://localhost:7860"
9
+
10
+ # For production, you should only need one host as
11
+ # fastapi serves the svelte-kit built frontend and backend from the same host and port.
12
+ # To test with CORS locally, you can set something like
13
+ # CORS_ALLOW_ORIGIN='http://localhost:5173;http://localhost:8080'
14
+ CORS_ALLOW_ORIGIN='*'
15
+
16
+ # For production you should set this to match the proxy configuration (127.0.0.1)
17
+ FORWARDED_ALLOW_IPS='*'
18
+
19
+ # DO NOT TRACK
20
+ SCARF_NO_ANALYTICS=true
21
+ DO_NOT_TRACK=true
22
+ ANONYMIZED_TELEMETRY=false
.eslintignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .DS_Store
2
+ node_modules
3
+ /build
4
+ /.svelte-kit
5
+ /package
6
+ .env
7
+ .env.*
8
+ !.env.example
9
+
10
+ # Ignore files for PNPM, NPM and YARN
11
+ pnpm-lock.yaml
12
+ package-lock.json
13
+ yarn.lock
.eslintrc.cjs ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ root: true,
3
+ extends: [
4
+ 'eslint:recommended',
5
+ 'plugin:@typescript-eslint/recommended',
6
+ 'plugin:svelte/recommended',
7
+ 'plugin:cypress/recommended',
8
+ 'prettier'
9
+ ],
10
+ parser: '@typescript-eslint/parser',
11
+ plugins: ['@typescript-eslint'],
12
+ parserOptions: {
13
+ sourceType: 'module',
14
+ ecmaVersion: 2020,
15
+ extraFileExtensions: ['.svelte']
16
+ },
17
+ env: {
18
+ browser: true,
19
+ es2017: true,
20
+ node: true
21
+ },
22
+ overrides: [
23
+ {
24
+ files: ['*.svelte'],
25
+ parser: 'svelte-eslint-parser',
26
+ parserOptions: {
27
+ parser: '@typescript-eslint/parser'
28
+ }
29
+ }
30
+ ]
31
+ };
.gitattributes ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # TypeScript
2
+ *.ts text eol=lf
3
+ *.tsx text eol=lf
4
+ # JavaScript
5
+ *.js text eol=lf
6
+ *.jsx text eol=lf
7
+ *.mjs text eol=lf
8
+ *.cjs text eol=lf
9
+ # Svelte
10
+ *.svelte text eol=lf
11
+ # HTML/CSS
12
+ *.html text eol=lf
13
+ *.css text eol=lf
14
+ *.scss text eol=lf
15
+ *.less text eol=lf
16
+ # Config files and JSON
17
+ *.json text eol=lf
18
+ *.jsonc text eol=lf
19
+ *.yml text eol=lf
20
+ *.yaml text eol=lf
21
+ *.toml text eol=lf
22
+ # Shell scripts
23
+ *.sh text eol=lf
24
+ # Markdown & docs
25
+ *.md text eol=lf
26
+ *.mdx text eol=lf
27
+ *.txt text eol=lf
28
+ # Git-related
29
+ .gitattributes text eol=lf
30
+ .gitignore text eol=lf
31
+ # Prettier and other dotfiles
32
+ .prettierrc text eol=lf
33
+ .prettierignore text eol=lf
34
+ .eslintrc text eol=lf
35
+ .eslintignore text eol=lf
36
+ .stylelintrc text eol=lf
37
+ .editorconfig text eol=lf
38
+ # Misc
39
+ *.env text eol=lf
40
+ *.lock text eol=lf
41
+ *.png filter=lfs diff=lfs merge=lfs -text
42
+ *.woff2 filter=lfs diff=lfs merge=lfs -text
43
+ *.ttf filter=lfs diff=lfs merge=lfs -text
44
+ *.jpg filter=lfs diff=lfs merge=lfs -text
.github/workflows/deploy-to-hf-spaces.yml ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Deploy to HuggingFace Spaces
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - dev
7
+ - main
8
+ workflow_dispatch:
9
+
10
+ jobs:
11
+ check-secret:
12
+ runs-on: ubuntu-latest
13
+ outputs:
14
+ token-set: ${{ steps.check-key.outputs.defined }}
15
+ steps:
16
+ - id: check-key
17
+ env:
18
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
19
+ if: "${{ env.HF_TOKEN != '' }}"
20
+ run: echo "defined=true" >> $GITHUB_OUTPUT
21
+
22
+ deploy:
23
+ runs-on: ubuntu-latest
24
+ needs: [check-secret]
25
+ if: needs.check-secret.outputs.token-set == 'true'
26
+ env:
27
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
28
+ steps:
29
+ - name: Checkout repository
30
+ uses: actions/checkout@v4
31
+
32
+ - name: Remove git history
33
+ run: rm -rf .git
34
+
35
+ - name: Prepend YAML front matter to README.md
36
+ run: |
37
+ echo "---" > temp_readme.md
38
+ echo "title: Open WebUI" >> temp_readme.md
39
+ echo "emoji: 🐳" >> temp_readme.md
40
+ echo "colorFrom: purple" >> temp_readme.md
41
+ echo "colorTo: gray" >> temp_readme.md
42
+ echo "sdk: docker" >> temp_readme.md
43
+ echo "app_port: 8080" >> temp_readme.md
44
+ echo "---" >> temp_readme.md
45
+ cat README.md >> temp_readme.md
46
+ mv temp_readme.md README.md
47
+
48
+ - name: Configure git
49
+ run: |
50
+ git config --global user.email "71052323+github-actions[bot]@users.noreply.github.com"
51
+ git config --global user.name "github-actions[bot]"
52
+ - name: Set up Git and push to Space
53
+ run: |
54
+ git init --initial-branch=main
55
+ git lfs install
56
+ git lfs track "*.png"
57
+ git lfs track "*.woff2"
58
+ git lfs track "*.ttf"
59
+ git lfs track "*.jpg"
60
+ rm demo.gif
61
+ git add .
62
+ git commit -m "GitHub deploy: ${{ github.sha }}"
63
+ git push --force https://lenaya:${HF_TOKEN}@huggingface.co/spaces/lenaya/open-webui main
.gitignore ADDED
@@ -0,0 +1,312 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ x.py
2
+ yarn.lock
3
+ .DS_Store
4
+ node_modules
5
+ /build
6
+ /.svelte-kit
7
+ /package
8
+ .env
9
+ .env.*
10
+ !.env.example
11
+ vite.config.js.timestamp-*
12
+ vite.config.ts.timestamp-*
13
+ # Byte-compiled / optimized / DLL files
14
+ __pycache__/
15
+ *.py[cod]
16
+ *$py.class
17
+ .nvmrc
18
+ CLAUDE.md
19
+ # C extensions
20
+ *.so
21
+
22
+ # Pyodide distribution
23
+ static/pyodide/*
24
+ !static/pyodide/pyodide-lock.json
25
+
26
+ # Distribution / packaging
27
+ .Python
28
+ build/
29
+ develop-eggs/
30
+ dist/
31
+ downloads/
32
+ eggs/
33
+ .eggs/
34
+ lib64/
35
+ parts/
36
+ sdist/
37
+ var/
38
+ wheels/
39
+ share/python-wheels/
40
+ *.egg-info/
41
+ .installed.cfg
42
+ *.egg
43
+ MANIFEST
44
+
45
+ # PyInstaller
46
+ # Usually these files are written by a python script from a template
47
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
48
+ *.manifest
49
+ *.spec
50
+
51
+ # Installer logs
52
+ pip-log.txt
53
+ pip-delete-this-directory.txt
54
+
55
+ # Unit test / coverage reports
56
+ htmlcov/
57
+ .tox/
58
+ .nox/
59
+ .coverage
60
+ .coverage.*
61
+ .cache
62
+ nosetests.xml
63
+ coverage.xml
64
+ *.cover
65
+ *.py,cover
66
+ .hypothesis/
67
+ .pytest_cache/
68
+ cover/
69
+
70
+ # Translations
71
+ *.mo
72
+ *.pot
73
+
74
+ # Django stuff:
75
+ *.log
76
+ local_settings.py
77
+ db.sqlite3
78
+ db.sqlite3-journal
79
+
80
+ # Flask stuff:
81
+ instance/
82
+ .webassets-cache
83
+
84
+ # Scrapy stuff:
85
+ .scrapy
86
+
87
+ # Sphinx documentation
88
+ docs/_build/
89
+
90
+ # PyBuilder
91
+ .pybuilder/
92
+ target/
93
+
94
+ # Jupyter Notebook
95
+ .ipynb_checkpoints
96
+
97
+ # IPython
98
+ profile_default/
99
+ ipython_config.py
100
+
101
+ # pyenv
102
+ # For a library or package, you might want to ignore these files since the code is
103
+ # intended to run in multiple environments; otherwise, check them in:
104
+ # .python-version
105
+
106
+ # pipenv
107
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
108
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
109
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
110
+ # install all needed dependencies.
111
+ #Pipfile.lock
112
+
113
+ # poetry
114
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
115
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
116
+ # commonly ignored for libraries.
117
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
118
+ #poetry.lock
119
+
120
+ # pdm
121
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
122
+ #pdm.lock
123
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
124
+ # in version control.
125
+ # https://pdm.fming.dev/#use-with-ide
126
+ .pdm.toml
127
+
128
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
129
+ __pypackages__/
130
+
131
+ # Celery stuff
132
+ celerybeat-schedule
133
+ celerybeat.pid
134
+
135
+ # SageMath parsed files
136
+ *.sage.py
137
+
138
+ # Environments
139
+ .env
140
+ .venv
141
+ env/
142
+ venv/
143
+ ENV/
144
+ env.bak/
145
+ venv.bak/
146
+
147
+ # Spyder project settings
148
+ .spyderproject
149
+ .spyproject
150
+
151
+ # Rope project settings
152
+ .ropeproject
153
+
154
+ # mkdocs documentation
155
+ /site
156
+
157
+ # mypy
158
+ .mypy_cache/
159
+ .dmypy.json
160
+ dmypy.json
161
+
162
+ # Pyre type checker
163
+ .pyre/
164
+
165
+ # pytype static type analyzer
166
+ .pytype/
167
+
168
+ # Cython debug symbols
169
+ cython_debug/
170
+
171
+ # PyCharm
172
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
173
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
174
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
175
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
176
+ .idea/
177
+
178
+ # Logs
179
+ logs
180
+ *.log
181
+ npm-debug.log*
182
+ yarn-debug.log*
183
+ yarn-error.log*
184
+ lerna-debug.log*
185
+ .pnpm-debug.log*
186
+
187
+ # Diagnostic reports (https://nodejs.org/api/report.html)
188
+ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
189
+
190
+ # Runtime data
191
+ pids
192
+ *.pid
193
+ *.seed
194
+ *.pid.lock
195
+
196
+ # Directory for instrumented libs generated by jscoverage/JSCover
197
+ lib-cov
198
+
199
+ # Coverage directory used by tools like istanbul
200
+ coverage
201
+ *.lcov
202
+
203
+ # nyc test coverage
204
+ .nyc_output
205
+
206
+ # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
207
+ .grunt
208
+
209
+ # Bower dependency directory (https://bower.io/)
210
+ bower_components
211
+
212
+ # node-waf configuration
213
+ .lock-wscript
214
+
215
+ # Compiled binary addons (https://nodejs.org/api/addons.html)
216
+ build/Release
217
+
218
+ # Dependency directories
219
+ node_modules/
220
+ jspm_packages/
221
+
222
+ # Snowpack dependency directory (https://snowpack.dev/)
223
+ web_modules/
224
+
225
+ # TypeScript cache
226
+ *.tsbuildinfo
227
+
228
+ # Optional npm cache directory
229
+ .npm
230
+
231
+ # Optional eslint cache
232
+ .eslintcache
233
+
234
+ # Optional stylelint cache
235
+ .stylelintcache
236
+
237
+ # Microbundle cache
238
+ .rpt2_cache/
239
+ .rts2_cache_cjs/
240
+ .rts2_cache_es/
241
+ .rts2_cache_umd/
242
+
243
+ # Optional REPL history
244
+ .node_repl_history
245
+
246
+ # Output of 'npm pack'
247
+ *.tgz
248
+
249
+ # Yarn Integrity file
250
+ .yarn-integrity
251
+
252
+ # dotenv environment variable files
253
+ .env
254
+ .env.development.local
255
+ .env.test.local
256
+ .env.production.local
257
+ .env.local
258
+
259
+ # parcel-bundler cache (https://parceljs.org/)
260
+ .cache
261
+ .parcel-cache
262
+
263
+ # Next.js build output
264
+ .next
265
+ out
266
+
267
+ # Nuxt.js build / generate output
268
+ .nuxt
269
+ dist
270
+
271
+ # Gatsby files
272
+ .cache/
273
+ # Comment in the public line in if your project uses Gatsby and not Next.js
274
+ # https://nextjs.org/blog/next-9-1#public-directory-support
275
+ # public
276
+
277
+ # vuepress build output
278
+ .vuepress/dist
279
+
280
+ # vuepress v2.x temp and cache directory
281
+ .temp
282
+ .cache
283
+
284
+ # Docusaurus cache and generated files
285
+ .docusaurus
286
+
287
+ # Serverless directories
288
+ .serverless/
289
+
290
+ # FuseBox cache
291
+ .fusebox/
292
+
293
+ # DynamoDB Local files
294
+ .dynamodb/
295
+
296
+ # TernJS port file
297
+ .tern-port
298
+
299
+ # Stores VSCode versions used for testing VSCode extensions
300
+ .vscode-test
301
+
302
+ # yarn v2
303
+ .yarn/cache
304
+ .yarn/unplugged
305
+ .yarn/build-state.yml
306
+ .yarn/install-state.gz
307
+ .pnp.*
308
+
309
+ # cypress artifacts
310
+ cypress/videos
311
+ cypress/screenshots
312
+ .vscode/settings.json
.npmrc ADDED
@@ -0,0 +1 @@
 
 
1
+ engine-strict=true
.prettierignore ADDED
@@ -0,0 +1,316 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ignore files for PNPM, NPM and YARN
2
+ pnpm-lock.yaml
3
+ package-lock.json
4
+ yarn.lock
5
+
6
+ kubernetes/
7
+
8
+ # Copy of .gitignore
9
+ .DS_Store
10
+ node_modules
11
+ /build
12
+ /.svelte-kit
13
+ /package
14
+ .env
15
+ .env.*
16
+ !.env.example
17
+ vite.config.js.timestamp-*
18
+ vite.config.ts.timestamp-*
19
+ # Byte-compiled / optimized / DLL files
20
+ __pycache__/
21
+ *.py[cod]
22
+ *$py.class
23
+
24
+ # C extensions
25
+ *.so
26
+
27
+ # Distribution / packaging
28
+ .Python
29
+ build/
30
+ develop-eggs/
31
+ dist/
32
+ downloads/
33
+ eggs/
34
+ .eggs/
35
+ lib64/
36
+ parts/
37
+ sdist/
38
+ var/
39
+ wheels/
40
+ share/python-wheels/
41
+ *.egg-info/
42
+ .installed.cfg
43
+ *.egg
44
+ MANIFEST
45
+
46
+ # PyInstaller
47
+ # Usually these files are written by a python script from a template
48
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
49
+ *.manifest
50
+ *.spec
51
+
52
+ # Installer logs
53
+ pip-log.txt
54
+ pip-delete-this-directory.txt
55
+
56
+ # Unit test / coverage reports
57
+ htmlcov/
58
+ .tox/
59
+ .nox/
60
+ .coverage
61
+ .coverage.*
62
+ .cache
63
+ nosetests.xml
64
+ coverage.xml
65
+ *.cover
66
+ *.py,cover
67
+ .hypothesis/
68
+ .pytest_cache/
69
+ cover/
70
+
71
+ # Translations
72
+ *.mo
73
+ *.pot
74
+
75
+ # Django stuff:
76
+ *.log
77
+ local_settings.py
78
+ db.sqlite3
79
+ db.sqlite3-journal
80
+
81
+ # Flask stuff:
82
+ instance/
83
+ .webassets-cache
84
+
85
+ # Scrapy stuff:
86
+ .scrapy
87
+
88
+ # Sphinx documentation
89
+ docs/_build/
90
+
91
+ # PyBuilder
92
+ .pybuilder/
93
+ target/
94
+
95
+ # Jupyter Notebook
96
+ .ipynb_checkpoints
97
+
98
+ # IPython
99
+ profile_default/
100
+ ipython_config.py
101
+
102
+ # pyenv
103
+ # For a library or package, you might want to ignore these files since the code is
104
+ # intended to run in multiple environments; otherwise, check them in:
105
+ # .python-version
106
+
107
+ # pipenv
108
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
109
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
110
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
111
+ # install all needed dependencies.
112
+ #Pipfile.lock
113
+
114
+ # poetry
115
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
116
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
117
+ # commonly ignored for libraries.
118
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
119
+ #poetry.lock
120
+
121
+ # pdm
122
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
123
+ #pdm.lock
124
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
125
+ # in version control.
126
+ # https://pdm.fming.dev/#use-with-ide
127
+ .pdm.toml
128
+
129
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
130
+ __pypackages__/
131
+
132
+ # Celery stuff
133
+ celerybeat-schedule
134
+ celerybeat.pid
135
+
136
+ # SageMath parsed files
137
+ *.sage.py
138
+
139
+ # Environments
140
+ .env
141
+ .venv
142
+ env/
143
+ venv/
144
+ ENV/
145
+ env.bak/
146
+ venv.bak/
147
+
148
+ # Spyder project settings
149
+ .spyderproject
150
+ .spyproject
151
+
152
+ # Rope project settings
153
+ .ropeproject
154
+
155
+ # mkdocs documentation
156
+ /site
157
+
158
+ # mypy
159
+ .mypy_cache/
160
+ .dmypy.json
161
+ dmypy.json
162
+
163
+ # Pyre type checker
164
+ .pyre/
165
+
166
+ # pytype static type analyzer
167
+ .pytype/
168
+
169
+ # Cython debug symbols
170
+ cython_debug/
171
+
172
+ # PyCharm
173
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
174
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
175
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
176
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
177
+ .idea/
178
+
179
+ # Logs
180
+ logs
181
+ *.log
182
+ npm-debug.log*
183
+ yarn-debug.log*
184
+ yarn-error.log*
185
+ lerna-debug.log*
186
+ .pnpm-debug.log*
187
+
188
+ # Diagnostic reports (https://nodejs.org/api/report.html)
189
+ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
190
+
191
+ # Runtime data
192
+ pids
193
+ *.pid
194
+ *.seed
195
+ *.pid.lock
196
+
197
+ # Directory for instrumented libs generated by jscoverage/JSCover
198
+ lib-cov
199
+
200
+ # Coverage directory used by tools like istanbul
201
+ coverage
202
+ *.lcov
203
+
204
+ # nyc test coverage
205
+ .nyc_output
206
+
207
+ # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
208
+ .grunt
209
+
210
+ # Bower dependency directory (https://bower.io/)
211
+ bower_components
212
+
213
+ # node-waf configuration
214
+ .lock-wscript
215
+
216
+ # Compiled binary addons (https://nodejs.org/api/addons.html)
217
+ build/Release
218
+
219
+ # Dependency directories
220
+ node_modules/
221
+ jspm_packages/
222
+
223
+ # Snowpack dependency directory (https://snowpack.dev/)
224
+ web_modules/
225
+
226
+ # TypeScript cache
227
+ *.tsbuildinfo
228
+
229
+ # Optional npm cache directory
230
+ .npm
231
+
232
+ # Optional eslint cache
233
+ .eslintcache
234
+
235
+ # Optional stylelint cache
236
+ .stylelintcache
237
+
238
+ # Microbundle cache
239
+ .rpt2_cache/
240
+ .rts2_cache_cjs/
241
+ .rts2_cache_es/
242
+ .rts2_cache_umd/
243
+
244
+ # Optional REPL history
245
+ .node_repl_history
246
+
247
+ # Output of 'npm pack'
248
+ *.tgz
249
+
250
+ # Yarn Integrity file
251
+ .yarn-integrity
252
+
253
+ # dotenv environment variable files
254
+ .env
255
+ .env.development.local
256
+ .env.test.local
257
+ .env.production.local
258
+ .env.local
259
+
260
+ # parcel-bundler cache (https://parceljs.org/)
261
+ .cache
262
+ .parcel-cache
263
+
264
+ # Next.js build output
265
+ .next
266
+ out
267
+
268
+ # Nuxt.js build / generate output
269
+ .nuxt
270
+ dist
271
+
272
+ # Gatsby files
273
+ .cache/
274
+ # Comment in the public line in if your project uses Gatsby and not Next.js
275
+ # https://nextjs.org/blog/next-9-1#public-directory-support
276
+ # public
277
+
278
+ # vuepress build output
279
+ .vuepress/dist
280
+
281
+ # vuepress v2.x temp and cache directory
282
+ .temp
283
+ .cache
284
+
285
+ # Docusaurus cache and generated files
286
+ .docusaurus
287
+
288
+ # Serverless directories
289
+ .serverless/
290
+
291
+ # FuseBox cache
292
+ .fusebox/
293
+
294
+ # DynamoDB Local files
295
+ .dynamodb/
296
+
297
+ # TernJS port file
298
+ .tern-port
299
+
300
+ # Stores VSCode versions used for testing VSCode extensions
301
+ .vscode-test
302
+
303
+ # yarn v2
304
+ .yarn/cache
305
+ .yarn/unplugged
306
+ .yarn/build-state.yml
307
+ .yarn/install-state.gz
308
+ .pnp.*
309
+
310
+ # cypress artifacts
311
+ cypress/videos
312
+ cypress/screenshots
313
+
314
+
315
+
316
+ /static/*
.prettierrc ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "useTabs": true,
3
+ "singleQuote": true,
4
+ "trailingComma": "none",
5
+ "printWidth": 100,
6
+ "plugins": ["prettier-plugin-svelte"],
7
+ "pluginSearchDirs": ["."],
8
+ "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }],
9
+ "endOfLine": "lf"
10
+ }
CHANGELOG.md ADDED
The diff for this file is too large to render. See raw diff
 
CODE_OF_CONDUCT.md ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ As members, contributors, and leaders of this community, we pledge to make participation in our project a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socioeconomic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6
+
7
+ We are committed to creating and maintaining an open, respectful, and professional environment where positive contributions and meaningful discussions can flourish. By participating in this project, you agree to uphold these values and align your behavior to the standards outlined in this Code of Conduct.
8
+
9
+ ## Why These Standards Are Important
10
+
11
+ Projects rely on a community of volunteers dedicating their time, expertise, and effort toward a shared goal. These projects are inherently collaborative but also fragile, as the success of the project depends on the goodwill, energy, and productivity of those involved.
12
+
13
+ Maintaining a positive and respectful environment is essential to safeguarding the integrity of this project and protecting contributors' efforts. Behavior that disrupts this atmosphere—whether through hostility, entitlement, or unprofessional conduct—can severely harm the morale and productivity of the community. **Strict enforcement of these standards ensures a safe and supportive space for meaningful collaboration.**
14
+
15
+ This is a community where **respect and professionalism are mandatory.** Violations of these standards will result in **zero tolerance** and immediate enforcement to prevent disruption and ensure the well-being of all participants.
16
+
17
+ ## Our Standards
18
+
19
+ Examples of behavior that contribute to a positive and professional community include:
20
+
21
+ - **Respecting others.** Be considerate, listen actively, and engage with empathy toward others' viewpoints and experiences.
22
+ - **Constructive feedback.** Provide actionable, thoughtful, and respectful feedback that helps improve the project and encourages collaboration. Avoid unproductive negativity or hypercriticism.
23
+ - **Recognizing volunteer contributions.** Appreciate that contributors dedicate their free time and resources selflessly. Approach them with gratitude and patience.
24
+ - **Focusing on shared goals.** Collaborate in ways that prioritize the health, success, and sustainability of the community over individual agendas.
25
+
26
+ Examples of unacceptable behavior include:
27
+
28
+ - The use of discriminatory, demeaning, or sexualized language or behavior.
29
+ - Personal attacks, derogatory comments, trolling, or inflammatory political or ideological arguments.
30
+ - Harassment, intimidation, or any behavior intended to create a hostile, uncomfortable, or unsafe environment.
31
+ - Publishing others' private information (e.g., physical or email addresses) without explicit permission.
32
+ - **Entitlement, demand, or aggression toward contributors.** Volunteers are under no obligation to provide immediate or personalized support. Rude or dismissive behavior will not be tolerated.
33
+ - **Unproductive or destructive behavior.** This includes venting frustration as hostility ("tantrums"), hypercriticism, attention-seeking negativity, or anything that distracts from the project's goals.
34
+ - **Spamming and promotional exploitation.** Sharing irrelevant product promotions or self-promotion in the community is not allowed unless it directly contributes value to the discussion.
35
+
36
+ ### Feedback and Community Engagement
37
+
38
+ - **Constructive feedback is encouraged, but hostile or entitled behavior will result in immediate action.** If you disagree with elements of the project, we encourage you to offer meaningful improvements or fork the project if necessary. Healthy discussions and technical disagreements are welcome only when handled with professionalism.
39
+ - **Respect contributors' time and efforts.** No one is entitled to personalized or on-demand assistance. This is a community built on collaboration and shared effort; demanding or demeaning behavior undermines that trust and will not be allowed.
40
+
41
+ ### Zero Tolerance: No Warnings, Immediate Action
42
+
43
+ This community operates under a **zero-tolerance policy.** Any behavior deemed unacceptable under this Code of Conduct will result in **immediate enforcement, without prior warning.**
44
+
45
+ We employ this approach to ensure that unproductive or disruptive behavior does not escalate further or cause unnecessary harm to other contributors. The standards are clear, and violations of any kind—whether mild or severe—will be addressed decisively to protect the community.
46
+
47
+ ## Enforcement Responsibilities
48
+
49
+ Community leaders are responsible for upholding and enforcing these standards. They are empowered to take **immediate and appropriate action** to address any behaviors they deem unacceptable under this Code of Conduct. These actions are taken with the goal of protecting the community and preserving its safe, positive, and productive environment.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies to all community spaces, including forums, repositories, social media accounts, and in-person events. It also applies when an individual represents the community in public settings, such as conferences or official communications.
54
+
55
+ Additionally, any behavior outside of these defined spaces that negatively impacts the community or its members may fall within the scope of this Code of Conduct.
56
+
57
+ ## Reporting Violations
58
+
59
+ Instances of unacceptable behavior can be reported to the leadership team at **[email protected]**. Reports will be handled promptly, confidentially, and with consideration for the safety and well-being of the reporter.
60
+
61
+ All community leaders are required to uphold confidentiality and impartiality when addressing reports of violations.
62
+
63
+ ## Enforcement Guidelines
64
+
65
+ ### Ban
66
+
67
+ **Community Impact**: Community leaders will issue a ban to any participant whose behavior is deemed unacceptable according to this Code of Conduct. Bans are enforced immediately and without prior notice.
68
+
69
+ A ban may be temporary or permanent, depending on the severity of the violation. This includes—but is not limited to—behavior such as:
70
+
71
+ - Harassment or abusive behavior toward contributors.
72
+ - Persistent negativity or hostility that disrupts the collaborative environment.
73
+ - Disrespectful, demanding, or aggressive interactions with others.
74
+ - Attempts to cause harm or sabotage the community.
75
+
76
+ **Consequence**: A banned individual is immediately removed from access to all community spaces, communication channels, and events. Community leaders reserve the right to enforce either a time-limited suspension or a permanent ban based on the specific circumstances of the violation.
77
+
78
+ This approach ensures that disruptive behaviors are addressed swiftly and decisively in order to maintain the integrity and productivity of the community.
79
+
80
+ ## Why Zero Tolerance Is Necessary
81
+
82
+ Projects thrive on collaboration, goodwill, and mutual respect. Toxic behaviors—such as entitlement, hostility, or persistent negativity—threaten not just individual contributors but the health of the project as a whole. Allowing such behaviors to persist robs contributors of their time, energy, and enthusiasm for the work they do.
83
+
84
+ By enforcing a zero-tolerance policy, we ensure that the community remains a safe, welcoming space for all participants. These measures are not about harshness—they are about protecting contributors and fostering a productive environment where innovation can thrive.
85
+
86
+ Our expectations are clear, and our enforcement reflects our commitment to this project's long-term success.
87
+
88
+ ## Attribution
89
+
90
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at
91
+ https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
92
+
93
+ Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
94
+
95
+ [homepage]: https://www.contributor-covenant.org
96
+
97
+ For answers to common questions about this code of conduct, see the FAQ at
98
+ https://www.contributor-covenant.org/faq. Translations are available at
99
+ https://www.contributor-covenant.org/translations.
CONTRIBUTOR_LICENSE_AGREEMENT ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # Open WebUI Contributor License Agreement
2
+
3
+ By submitting my contributions to Open WebUI, I grant Open WebUI full freedom to use my work in any way they choose, under any terms they like, both now and in the future. This approach helps ensure the project remains unified, flexible, and easy to maintain, while empowering Open WebUI to respond quickly to the needs of its users and the wider community.
4
+
5
+ Taking part in this process means my work can be seamlessly integrated and combined with others, ensuring longevity and adaptability for everyone who benefits from the Open WebUI project. This collaborative approach strengthens the project’s future and helps guarantee that improvements can always be shared and distributed in the most effective way possible.
6
+
7
+ **_To the fullest extent permitted by law, my contributions are provided on an “as is” basis, with no warranties or guarantees of any kind, and I disclaim any liability for any issues or damages arising from their use or incorporation into the project, regardless of the type of legal claim._**
Dockerfile ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # syntax=docker/dockerfile:1
2
+ # Initialize device type args
3
+ # use build args in the docker build command with --build-arg="BUILDARG=true"
4
+ ARG USE_CUDA=false
5
+ ARG USE_OLLAMA=false
6
+ # Tested with cu117 for CUDA 11 and cu121 for CUDA 12 (default)
7
+ ARG USE_CUDA_VER=cu128
8
+ # any sentence transformer model; models to use can be found at https://huggingface.co/models?library=sentence-transformers
9
+ # Leaderboard: https://huggingface.co/spaces/mteb/leaderboard
10
+ # for better performance and multilangauge support use "intfloat/multilingual-e5-large" (~2.5GB) or "intfloat/multilingual-e5-base" (~1.5GB)
11
+ # IMPORTANT: If you change the embedding model (sentence-transformers/all-MiniLM-L6-v2) and vice versa, you aren't able to use RAG Chat with your previous documents loaded in the WebUI! You need to re-embed them.
12
+ ARG USE_EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
13
+ ARG USE_RERANKING_MODEL=""
14
+
15
+ # Tiktoken encoding name; models to use can be found at https://huggingface.co/models?library=tiktoken
16
+ ARG USE_TIKTOKEN_ENCODING_NAME="cl100k_base"
17
+
18
+ ARG BUILD_HASH=dev-build
19
+ # Override at your own risk - non-root configurations are untested
20
+ ARG UID=0
21
+ ARG GID=0
22
+
23
+ ######## WebUI frontend ########
24
+ FROM --platform=$BUILDPLATFORM node:22-alpine3.20 AS build
25
+ ARG BUILD_HASH
26
+
27
+ WORKDIR /app
28
+
29
+ # to store git revision in build
30
+ RUN apk add --no-cache git
31
+
32
+ COPY package.json package-lock.json ./
33
+ RUN npm ci --force
34
+
35
+ COPY . .
36
+ ENV APP_BUILD_HASH=${BUILD_HASH}
37
+ RUN npm run build
38
+
39
+ ######## WebUI backend ########
40
+ FROM python:3.11-slim-bookworm AS base
41
+
42
+ # Use args
43
+ ARG USE_CUDA
44
+ ARG USE_OLLAMA
45
+ ARG USE_CUDA_VER
46
+ ARG USE_EMBEDDING_MODEL
47
+ ARG USE_RERANKING_MODEL
48
+ ARG UID
49
+ ARG GID
50
+
51
+ ## Basis ##
52
+ ENV ENV=prod \
53
+ PORT=8080 \
54
+ # pass build args to the build
55
+ USE_OLLAMA_DOCKER=${USE_OLLAMA} \
56
+ USE_CUDA_DOCKER=${USE_CUDA} \
57
+ USE_CUDA_DOCKER_VER=${USE_CUDA_VER} \
58
+ USE_EMBEDDING_MODEL_DOCKER=${USE_EMBEDDING_MODEL} \
59
+ USE_RERANKING_MODEL_DOCKER=${USE_RERANKING_MODEL}
60
+
61
+ ## Basis URL Config ##
62
+ ENV OLLAMA_BASE_URL="/ollama" \
63
+ OPENAI_API_BASE_URL=""
64
+
65
+ ## API Key and Security Config ##
66
+ ENV OPENAI_API_KEY="" \
67
+ WEBUI_SECRET_KEY="" \
68
+ SCARF_NO_ANALYTICS=true \
69
+ DO_NOT_TRACK=true \
70
+ ANONYMIZED_TELEMETRY=false
71
+
72
+ #### Other models #########################################################
73
+ ## whisper TTS model settings ##
74
+ ENV WHISPER_MODEL="base" \
75
+ WHISPER_MODEL_DIR="/app/backend/data/cache/whisper/models"
76
+
77
+ ## RAG Embedding model settings ##
78
+ ENV RAG_EMBEDDING_MODEL="$USE_EMBEDDING_MODEL_DOCKER" \
79
+ RAG_RERANKING_MODEL="$USE_RERANKING_MODEL_DOCKER" \
80
+ SENTENCE_TRANSFORMERS_HOME="/app/backend/data/cache/embedding/models"
81
+
82
+ ## Tiktoken model settings ##
83
+ ENV TIKTOKEN_ENCODING_NAME="cl100k_base" \
84
+ TIKTOKEN_CACHE_DIR="/app/backend/data/cache/tiktoken"
85
+
86
+ ## Hugging Face download cache ##
87
+ ENV HF_HOME="/app/backend/data/cache/embedding/models"
88
+
89
+ ## Torch Extensions ##
90
+ # ENV TORCH_EXTENSIONS_DIR="/.cache/torch_extensions"
91
+
92
+ #### Other models ##########################################################
93
+
94
+ WORKDIR /app/backend
95
+
96
+ ENV HOME=/root
97
+ # Create user and group if not root
98
+ RUN if [ $UID -ne 0 ]; then \
99
+ if [ $GID -ne 0 ]; then \
100
+ addgroup --gid $GID app; \
101
+ fi; \
102
+ adduser --uid $UID --gid $GID --home $HOME --disabled-password --no-create-home app; \
103
+ fi
104
+
105
+ RUN mkdir -p $HOME/.cache/chroma
106
+ RUN echo -n 00000000-0000-0000-0000-000000000000 > $HOME/.cache/chroma/telemetry_user_id
107
+
108
+ # Make sure the user has access to the app and root directory
109
+ RUN chown -R $UID:$GID /app $HOME
110
+
111
+ # Install common system dependencies
112
+ RUN apt-get update && \
113
+ apt-get install -y --no-install-recommends \
114
+ git build-essential pandoc gcc netcat-openbsd curl jq \
115
+ python3-dev \
116
+ ffmpeg libsm6 libxext6 \
117
+ && rm -rf /var/lib/apt/lists/*
118
+
119
+ # install python dependencies
120
+ COPY --chown=$UID:$GID ./backend/requirements.txt ./requirements.txt
121
+
122
+ RUN pip3 install --no-cache-dir uv && \
123
+ if [ "$USE_CUDA" = "true" ]; then \
124
+ # If you use CUDA the whisper and embedding model will be downloaded on first use
125
+ pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir && \
126
+ uv pip install --system -r requirements.txt --no-cache-dir && \
127
+ python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
128
+ python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \
129
+ python -c "import os; import tiktoken; tiktoken.get_encoding(os.environ['TIKTOKEN_ENCODING_NAME'])"; \
130
+ else \
131
+ pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \
132
+ uv pip install --system -r requirements.txt --no-cache-dir && \
133
+ python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
134
+ python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \
135
+ python -c "import os; import tiktoken; tiktoken.get_encoding(os.environ['TIKTOKEN_ENCODING_NAME'])"; \
136
+ fi; \
137
+ chown -R $UID:$GID /app/backend/data/
138
+
139
+ # Install Ollama if requested
140
+ RUN if [ "$USE_OLLAMA" = "true" ]; then \
141
+ date +%s > /tmp/ollama_build_hash && \
142
+ echo "Cache broken at timestamp: `cat /tmp/ollama_build_hash`" && \
143
+ curl -fsSL https://ollama.com/install.sh | sh && \
144
+ rm -rf /var/lib/apt/lists/*; \
145
+ fi
146
+
147
+ # copy embedding weight from build
148
+ # RUN mkdir -p /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2
149
+ # COPY --from=build /app/onnx /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2/onnx
150
+
151
+ # copy built frontend files
152
+ COPY --chown=$UID:$GID --from=build /app/build /app/build
153
+ COPY --chown=$UID:$GID --from=build /app/CHANGELOG.md /app/CHANGELOG.md
154
+ COPY --chown=$UID:$GID --from=build /app/package.json /app/package.json
155
+
156
+ # copy backend files
157
+ COPY --chown=$UID:$GID ./backend .
158
+
159
+ EXPOSE 8080
160
+
161
+ HEALTHCHECK CMD curl --silent --fail http://localhost:${PORT:-8080}/health | jq -ne 'input.status == true' || exit 1
162
+
163
+ # Minimal, atomic permission hardening for OpenShift (arbitrary UID):
164
+ # - Group 0 owns /app and /root
165
+ # - Directories are group-writable and have SGID so new files inherit GID 0
166
+ RUN set -eux; \
167
+ chgrp -R 0 /app /root || true; \
168
+ chmod -R g+rwX /app /root || true; \
169
+ find /app -type d -exec chmod g+s {} + || true; \
170
+ find /root -type d -exec chmod g+s {} + || true
171
+
172
+ USER $UID:$GID
173
+
174
+ ARG BUILD_HASH
175
+ ENV WEBUI_BUILD_VERSION=${BUILD_HASH}
176
+ ENV DOCKER=true
177
+
178
+ CMD [ "bash", "start.sh"]
INSTALLATION.md ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ### Installing Both Ollama and Open WebUI Using Kustomize
2
+
3
+ For cpu-only pod
4
+
5
+ ```bash
6
+ kubectl apply -f ./kubernetes/manifest/base
7
+ ```
8
+
9
+ For gpu-enabled pod
10
+
11
+ ```bash
12
+ kubectl apply -k ./kubernetes/manifest
13
+ ```
14
+
15
+ ### Installing Both Ollama and Open WebUI Using Helm
16
+
17
+ Package Helm file first
18
+
19
+ ```bash
20
+ helm package ./kubernetes/helm/
21
+ ```
22
+
23
+ For cpu-only pod
24
+
25
+ ```bash
26
+ helm install ollama-webui ./ollama-webui-*.tgz
27
+ ```
28
+
29
+ For gpu-enabled pod
30
+
31
+ ```bash
32
+ helm install ollama-webui ./ollama-webui-*.tgz --set ollama.resources.limits.nvidia.com/gpu="1"
33
+ ```
34
+
35
+ Check the `kubernetes/helm/values.yaml` file to know which parameters are available for customization
LICENSE ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Copyright (c) 2023-2025 Timothy Jaeryang Baek (Open WebUI)
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ 1. Redistributions of source code must retain the above copyright notice, this
8
+ list of conditions and the following disclaimer.
9
+
10
+ 2. Redistributions in binary form must reproduce the above copyright notice,
11
+ this list of conditions and the following disclaimer in the documentation
12
+ and/or other materials provided with the distribution.
13
+
14
+ 3. Neither the name of the copyright holder nor the names of its
15
+ contributors may be used to endorse or promote products derived from
16
+ this software without specific prior written permission.
17
+
18
+ 4. Notwithstanding any other provision of this License, and as a material condition of the rights granted herein, licensees are strictly prohibited from altering, removing, obscuring, or replacing any "Open WebUI" branding, including but not limited to the name, logo, or any visual, textual, or symbolic identifiers that distinguish the software and its interfaces, in any deployment or distribution, regardless of the number of users, except as explicitly set forth in Clauses 5 and 6 below.
19
+
20
+ 5. The branding restriction enumerated in Clause 4 shall not apply in the following limited circumstances: (i) deployments or distributions where the total number of end users (defined as individual natural persons with direct access to the application) does not exceed fifty (50) within any rolling thirty (30) day period; (ii) cases in which the licensee is an official contributor to the codebase—with a substantive code change successfully merged into the main branch of the official codebase maintained by the copyright holder—who has obtained specific prior written permission for branding adjustment from the copyright holder; or (iii) where the licensee has obtained a duly executed enterprise license expressly permitting such modification. For all other cases, any removal or alteration of the "Open WebUI" branding shall constitute a material breach of license.
21
+
22
+ 6. All code, modifications, or derivative works incorporated into this project prior to the incorporation of this branding clause remain licensed under the BSD 3-Clause License, and prior contributors retain all BSD-3 rights therein; if any such contributor requests the removal of their BSD-3-licensed code, the copyright holder will do so, and any replacement code will be licensed under the project's primary license then in effect. By contributing after this clause's adoption, you agree to the project's Contributor License Agreement (CLA) and to these updated terms for all new contributions.
23
+
24
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
25
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
26
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
28
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
29
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
30
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
31
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
32
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
33
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
LICENSE_HISTORY ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ All code and materials created before commit `60d84a3aae9802339705826e9095e272e3c83623` are subject to the following copyright and license:
2
+
3
+ Copyright (c) 2023-2025 Timothy Jaeryang Baek
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+
31
+ All code and materials created before commit `a76068d69cd59568b920dfab85dc573dbbb8f131` are subject to the following copyright and license:
32
+
33
+ MIT License
34
+
35
+ Copyright (c) 2023 Timothy Jaeryang Baek
36
+
37
+ Permission is hereby granted, free of charge, to any person obtaining a copy
38
+ of this software and associated documentation files (the "Software"), to deal
39
+ in the Software without restriction, including without limitation the rights
40
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
41
+ copies of the Software, and to permit persons to whom the Software is
42
+ furnished to do so, subject to the following conditions:
43
+
44
+ The above copyright notice and this permission notice shall be included in all
45
+ copies or substantial portions of the Software.
46
+
47
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
48
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
49
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
50
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
51
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
52
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
53
+ SOFTWARE.
Makefile ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ ifneq ($(shell which docker-compose 2>/dev/null),)
3
+ DOCKER_COMPOSE := docker-compose
4
+ else
5
+ DOCKER_COMPOSE := docker compose
6
+ endif
7
+
8
+ install:
9
+ $(DOCKER_COMPOSE) up -d
10
+
11
+ remove:
12
+ @chmod +x confirm_remove.sh
13
+ @./confirm_remove.sh
14
+
15
+ start:
16
+ $(DOCKER_COMPOSE) start
17
+ startAndBuild:
18
+ $(DOCKER_COMPOSE) up -d --build
19
+
20
+ stop:
21
+ $(DOCKER_COMPOSE) stop
22
+
23
+ update:
24
+ # Calls the LLM update script
25
+ chmod +x update_ollama_models.sh
26
+ @./update_ollama_models.sh
27
+ @git pull
28
+ $(DOCKER_COMPOSE) down
29
+ # Make sure the ollama-webui container is stopped before rebuilding
30
+ @docker stop open-webui || true
31
+ $(DOCKER_COMPOSE) up --build -d
32
+ $(DOCKER_COMPOSE) start
33
+
README.md ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Open WebUI
3
+ emoji: 🐳
4
+ colorFrom: purple
5
+ colorTo: gray
6
+ sdk: docker
7
+ app_port: 8080
8
+ ---
9
+ # Open WebUI 👋
10
+
11
+ ![GitHub stars](https://img.shields.io/github/stars/open-webui/open-webui?style=social)
12
+ ![GitHub forks](https://img.shields.io/github/forks/open-webui/open-webui?style=social)
13
+ ![GitHub watchers](https://img.shields.io/github/watchers/open-webui/open-webui?style=social)
14
+ ![GitHub repo size](https://img.shields.io/github/repo-size/open-webui/open-webui)
15
+ ![GitHub language count](https://img.shields.io/github/languages/count/open-webui/open-webui)
16
+ ![GitHub top language](https://img.shields.io/github/languages/top/open-webui/open-webui)
17
+ ![GitHub last commit](https://img.shields.io/github/last-commit/open-webui/open-webui?color=red)
18
+ [![Discord](https://img.shields.io/badge/Discord-Open_WebUI-blue?logo=discord&logoColor=white)](https://discord.gg/5rJgQTnV4s)
19
+ [![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/tjbck)
20
+
21
+ **Open WebUI is an [extensible](https://docs.openwebui.com/features/plugin/), feature-rich, and user-friendly self-hosted AI platform designed to operate entirely offline.** It supports various LLM runners like **Ollama** and **OpenAI-compatible APIs**, with **built-in inference engine** for RAG, making it a **powerful AI deployment solution**.
22
+
23
+ Passionate about open-source AI? [Join our team →](https://careers.openwebui.com/)
24
+
25
+ ![Open WebUI Demo](./demo.gif)
26
+
27
+ > [!TIP]
28
+ > **Looking for an [Enterprise Plan](https://docs.openwebui.com/enterprise)?** – **[Speak with Our Sales Team Today!](mailto:[email protected])**
29
+ >
30
+ > Get **enhanced capabilities**, including **custom theming and branding**, **Service Level Agreement (SLA) support**, **Long-Term Support (LTS) versions**, and **more!**
31
+
32
+ For more information, be sure to check out our [Open WebUI Documentation](https://docs.openwebui.com/).
33
+
34
+ ## Key Features of Open WebUI ⭐
35
+
36
+ - 🚀 **Effortless Setup**: Install seamlessly using Docker or Kubernetes (kubectl, kustomize or helm) for a hassle-free experience with support for both `:ollama` and `:cuda` tagged images.
37
+
38
+ - 🤝 **Ollama/OpenAI API Integration**: Effortlessly integrate OpenAI-compatible APIs for versatile conversations alongside Ollama models. Customize the OpenAI API URL to link with **LMStudio, GroqCloud, Mistral, OpenRouter, and more**.
39
+
40
+ - 🛡️ **Granular Permissions and User Groups**: By allowing administrators to create detailed user roles and permissions, we ensure a secure user environment. This granularity not only enhances security but also allows for customized user experiences, fostering a sense of ownership and responsibility amongst users.
41
+
42
+ - 🔄 **SCIM 2.0 Support**: Enterprise-grade user and group provisioning through SCIM 2.0 protocol, enabling seamless integration with identity providers like Okta, Azure AD, and Google Workspace for automated user lifecycle management.
43
+
44
+ - 📱 **Responsive Design**: Enjoy a seamless experience across Desktop PC, Laptop, and Mobile devices.
45
+
46
+ - 📱 **Progressive Web App (PWA) for Mobile**: Enjoy a native app-like experience on your mobile device with our PWA, providing offline access on localhost and a seamless user interface.
47
+
48
+ - ✒️🔢 **Full Markdown and LaTeX Support**: Elevate your LLM experience with comprehensive Markdown and LaTeX capabilities for enriched interaction.
49
+
50
+ - 🎤📹 **Hands-Free Voice/Video Call**: Experience seamless communication with integrated hands-free voice and video call features, allowing for a more dynamic and interactive chat environment.
51
+
52
+ - 🛠️ **Model Builder**: Easily create Ollama models via the Web UI. Create and add custom characters/agents, customize chat elements, and import models effortlessly through [Open WebUI Community](https://openwebui.com/) integration.
53
+
54
+ - 🐍 **Native Python Function Calling Tool**: Enhance your LLMs with built-in code editor support in the tools workspace. Bring Your Own Function (BYOF) by simply adding your pure Python functions, enabling seamless integration with LLMs.
55
+
56
+ - 📚 **Local RAG Integration**: Dive into the future of chat interactions with groundbreaking Retrieval Augmented Generation (RAG) support. This feature seamlessly integrates document interactions into your chat experience. You can load documents directly into the chat or add files to your document library, effortlessly accessing them using the `#` command before a query.
57
+
58
+ - 🔍 **Web Search for RAG**: Perform web searches using providers like `SearXNG`, `Google PSE`, `Brave Search`, `serpstack`, `serper`, `Serply`, `DuckDuckGo`, `TavilySearch`, `SearchApi` and `Bing` and inject the results directly into your chat experience.
59
+
60
+ - 🌐 **Web Browsing Capability**: Seamlessly integrate websites into your chat experience using the `#` command followed by a URL. This feature allows you to incorporate web content directly into your conversations, enhancing the richness and depth of your interactions.
61
+
62
+ - 🎨 **Image Generation Integration**: Seamlessly incorporate image generation capabilities using options such as AUTOMATIC1111 API or ComfyUI (local), and OpenAI's DALL-E (external), enriching your chat experience with dynamic visual content.
63
+
64
+ - ⚙️ **Many Models Conversations**: Effortlessly engage with various models simultaneously, harnessing their unique strengths for optimal responses. Enhance your experience by leveraging a diverse set of models in parallel.
65
+
66
+ - 🔐 **Role-Based Access Control (RBAC)**: Ensure secure access with restricted permissions; only authorized individuals can access your Ollama, and exclusive model creation/pulling rights are reserved for administrators.
67
+
68
+ - 🌐🌍 **Multilingual Support**: Experience Open WebUI in your preferred language with our internationalization (i18n) support. Join us in expanding our supported languages! We're actively seeking contributors!
69
+
70
+ - 🧩 **Pipelines, Open WebUI Plugin Support**: Seamlessly integrate custom logic and Python libraries into Open WebUI using [Pipelines Plugin Framework](https://github.com/open-webui/pipelines). Launch your Pipelines instance, set the OpenAI URL to the Pipelines URL, and explore endless possibilities. [Examples](https://github.com/open-webui/pipelines/tree/main/examples) include **Function Calling**, User **Rate Limiting** to control access, **Usage Monitoring** with tools like Langfuse, **Live Translation with LibreTranslate** for multilingual support, **Toxic Message Filtering** and much more.
71
+
72
+ - 🌟 **Continuous Updates**: We are committed to improving Open WebUI with regular updates, fixes, and new features.
73
+
74
+ Want to learn more about Open WebUI's features? Check out our [Open WebUI documentation](https://docs.openwebui.com/features) for a comprehensive overview!
75
+
76
+ ## Sponsors 🙌
77
+
78
+ #### Emerald
79
+
80
+ <table>
81
+ <!-- <tr>
82
+ <td>
83
+ <a href="https://n8n.io/" target="_blank">
84
+ <img src="https://docs.openwebui.com/sponsors/logos/n8n.png" alt="n8n" style="width: 8rem; height: 8rem; border-radius: .75rem;" />
85
+ </a>
86
+ </td>
87
+ <td>
88
+ <a href="https://n8n.io/">n8n</a> • Does your interface have a backend yet?<br>Try <a href="https://n8n.io/">n8n</a>
89
+ </td>
90
+ </tr> -->
91
+ <tr>
92
+ <td>
93
+ <a href="https://tailscale.com/blog/self-host-a-local-ai-stack/?utm_source=OpenWebUI&utm_medium=paid-ad-placement&utm_campaign=OpenWebUI-Docs" target="_blank">
94
+ <img src="https://docs.openwebui.com/sponsors/logos/tailscale.png" alt="Tailscale" style="width: 8rem; height: 8rem; border-radius: .75rem;" />
95
+ </a>
96
+ </td>
97
+ <td>
98
+ <a href="https://tailscale.com/blog/self-host-a-local-ai-stack/?utm_source=OpenWebUI&utm_medium=paid-ad-placement&utm_campaign=OpenWebUI-Docs">Tailscale</a> • Connect self-hosted AI to any device with Tailscale
99
+ </td>
100
+ </tr>
101
+ <tr>
102
+ <td>
103
+ <a href="https://warp.dev/open-webui" target="_blank">
104
+ <img src="https://docs.openwebui.com/sponsors/logos/warp.png" alt="Warp" style="width: 8rem; height: 8rem; border-radius: .75rem;" />
105
+ </a>
106
+ </td>
107
+ <td>
108
+ <a href="https://warp.dev/open-webui">Warp</a> • The intelligent terminal for developers
109
+ </td>
110
+ </tr>
111
+ </table>
112
+
113
+ ---
114
+
115
+ We are incredibly grateful for the generous support of our sponsors. Their contributions help us to maintain and improve our project, ensuring we can continue to deliver quality work to our community. Thank you!
116
+
117
+ ## How to Install 🚀
118
+
119
+ ### Installation via Python pip 🐍
120
+
121
+ Open WebUI can be installed using pip, the Python package installer. Before proceeding, ensure you're using **Python 3.11** to avoid compatibility issues.
122
+
123
+ 1. **Install Open WebUI**:
124
+ Open your terminal and run the following command to install Open WebUI:
125
+
126
+ ```bash
127
+ pip install open-webui
128
+ ```
129
+
130
+ 2. **Running Open WebUI**:
131
+ After installation, you can start Open WebUI by executing:
132
+
133
+ ```bash
134
+ open-webui serve
135
+ ```
136
+
137
+ This will start the Open WebUI server, which you can access at [http://localhost:8080](http://localhost:8080)
138
+
139
+ ### Quick Start with Docker 🐳
140
+
141
+ > [!NOTE]
142
+ > Please note that for certain Docker environments, additional configurations might be needed. If you encounter any connection issues, our detailed guide on [Open WebUI Documentation](https://docs.openwebui.com/) is ready to assist you.
143
+
144
+ > [!WARNING]
145
+ > When using Docker to install Open WebUI, make sure to include the `-v open-webui:/app/backend/data` in your Docker command. This step is crucial as it ensures your database is properly mounted and prevents any loss of data.
146
+
147
+ > [!TIP]
148
+ > If you wish to utilize Open WebUI with Ollama included or CUDA acceleration, we recommend utilizing our official images tagged with either `:cuda` or `:ollama`. To enable CUDA, you must install the [Nvidia CUDA container toolkit](https://docs.nvidia.com/dgx/nvidia-container-runtime-upgrade/) on your Linux/WSL system.
149
+
150
+ ### Installation with Default Configuration
151
+
152
+ - **If Ollama is on your computer**, use this command:
153
+
154
+ ```bash
155
+ docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
156
+ ```
157
+
158
+ - **If Ollama is on a Different Server**, use this command:
159
+
160
+ To connect to Ollama on another server, change the `OLLAMA_BASE_URL` to the server's URL:
161
+
162
+ ```bash
163
+ docker run -d -p 3000:8080 -e OLLAMA_BASE_URL=https://example.com -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
164
+ ```
165
+
166
+ - **To run Open WebUI with Nvidia GPU support**, use this command:
167
+
168
+ ```bash
169
+ docker run -d -p 3000:8080 --gpus all --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:cuda
170
+ ```
171
+
172
+ ### Installation for OpenAI API Usage Only
173
+
174
+ - **If you're only using OpenAI API**, use this command:
175
+
176
+ ```bash
177
+ docker run -d -p 3000:8080 -e OPENAI_API_KEY=your_secret_key -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
178
+ ```
179
+
180
+ ### Installing Open WebUI with Bundled Ollama Support
181
+
182
+ This installation method uses a single container image that bundles Open WebUI with Ollama, allowing for a streamlined setup via a single command. Choose the appropriate command based on your hardware setup:
183
+
184
+ - **With GPU Support**:
185
+ Utilize GPU resources by running the following command:
186
+
187
+ ```bash
188
+ docker run -d -p 3000:8080 --gpus=all -v ollama:/root/.ollama -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:ollama
189
+ ```
190
+
191
+ - **For CPU Only**:
192
+ If you're not using a GPU, use this command instead:
193
+
194
+ ```bash
195
+ docker run -d -p 3000:8080 -v ollama:/root/.ollama -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:ollama
196
+ ```
197
+
198
+ Both commands facilitate a built-in, hassle-free installation of both Open WebUI and Ollama, ensuring that you can get everything up and running swiftly.
199
+
200
+ After installation, you can access Open WebUI at [http://localhost:3000](http://localhost:3000). Enjoy! 😄
201
+
202
+ ### Other Installation Methods
203
+
204
+ We offer various installation alternatives, including non-Docker native installation methods, Docker Compose, Kustomize, and Helm. Visit our [Open WebUI Documentation](https://docs.openwebui.com/getting-started/) or join our [Discord community](https://discord.gg/5rJgQTnV4s) for comprehensive guidance.
205
+
206
+ Look at the [Local Development Guide](https://docs.openwebui.com/getting-started/advanced-topics/development) for instructions on setting up a local development environment.
207
+
208
+ ### Troubleshooting
209
+
210
+ Encountering connection issues? Our [Open WebUI Documentation](https://docs.openwebui.com/troubleshooting/) has got you covered. For further assistance and to join our vibrant community, visit the [Open WebUI Discord](https://discord.gg/5rJgQTnV4s).
211
+
212
+ #### Open WebUI: Server Connection Error
213
+
214
+ If you're experiencing connection issues, it’s often due to the WebUI docker container not being able to reach the Ollama server at 127.0.0.1:11434 (host.docker.internal:11434) inside the container . Use the `--network=host` flag in your docker command to resolve this. Note that the port changes from 3000 to 8080, resulting in the link: `http://localhost:8080`.
215
+
216
+ **Example Docker Command**:
217
+
218
+ ```bash
219
+ docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_BASE_URL=http://127.0.0.1:11434 --name open-webui --restart always ghcr.io/open-webui/open-webui:main
220
+ ```
221
+
222
+ ### Keeping Your Docker Installation Up-to-Date
223
+
224
+ In case you want to update your local Docker installation to the latest version, you can do it with [Watchtower](https://containrrr.dev/watchtower/):
225
+
226
+ ```bash
227
+ docker run --rm --volume /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower --run-once open-webui
228
+ ```
229
+
230
+ In the last part of the command, replace `open-webui` with your container name if it is different.
231
+
232
+ Check our Updating Guide available in our [Open WebUI Documentation](https://docs.openwebui.com/getting-started/updating).
233
+
234
+ ### Using the Dev Branch 🌙
235
+
236
+ > [!WARNING]
237
+ > The `:dev` branch contains the latest unstable features and changes. Use it at your own risk as it may have bugs or incomplete features.
238
+
239
+ If you want to try out the latest bleeding-edge features and are okay with occasional instability, you can use the `:dev` tag like this:
240
+
241
+ ```bash
242
+ docker run -d -p 3000:8080 -v open-webui:/app/backend/data --name open-webui --add-host=host.docker.internal:host-gateway --restart always ghcr.io/open-webui/open-webui:dev
243
+ ```
244
+
245
+ ### Offline Mode
246
+
247
+ If you are running Open WebUI in an offline environment, you can set the `HF_HUB_OFFLINE` environment variable to `1` to prevent attempts to download models from the internet.
248
+
249
+ ```bash
250
+ export HF_HUB_OFFLINE=1
251
+ ```
252
+
253
+ ## What's Next? 🌟
254
+
255
+ Discover upcoming features on our roadmap in the [Open WebUI Documentation](https://docs.openwebui.com/roadmap/).
256
+
257
+ ## License 📜
258
+
259
+ This project is licensed under the [Open WebUI License](LICENSE), a revised BSD-3-Clause license. You receive all the same rights as the classic BSD-3 license: you can use, modify, and distribute the software, including in proprietary and commercial products, with minimal restrictions. The only additional requirement is to preserve the "Open WebUI" branding, as detailed in the LICENSE file. For full terms, see the [LICENSE](LICENSE) document. 📄
260
+
261
+ ## Support 💬
262
+
263
+ If you have any questions, suggestions, or need assistance, please open an issue or join our
264
+ [Open WebUI Discord community](https://discord.gg/5rJgQTnV4s) to connect with us! 🤝
265
+
266
+ ## Star History
267
+
268
+ <a href="https://star-history.com/#open-webui/open-webui&Date">
269
+ <picture>
270
+ <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=open-webui/open-webui&type=Date&theme=dark" />
271
+ <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=open-webui/open-webui&type=Date" />
272
+ <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=open-webui/open-webui&type=Date" />
273
+ </picture>
274
+ </a>
275
+
276
+ ---
277
+
278
+ Created by [Timothy Jaeryang Baek](https://github.com/tjbck) - Let's make Open WebUI even more amazing together! 💪
TROUBLESHOOTING.md ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Open WebUI Troubleshooting Guide
2
+
3
+ ## Understanding the Open WebUI Architecture
4
+
5
+ The Open WebUI system is designed to streamline interactions between the client (your browser) and the Ollama API. At the heart of this design is a backend reverse proxy, enhancing security and resolving CORS issues.
6
+
7
+ - **How it Works**: The Open WebUI is designed to interact with the Ollama API through a specific route. When a request is made from the WebUI to Ollama, it is not directly sent to the Ollama API. Initially, the request is sent to the Open WebUI backend via `/ollama` route. From there, the backend is responsible for forwarding the request to the Ollama API. This forwarding is accomplished by using the route specified in the `OLLAMA_BASE_URL` environment variable. Therefore, a request made to `/ollama` in the WebUI is effectively the same as making a request to `OLLAMA_BASE_URL` in the backend. For instance, a request to `/ollama/api/tags` in the WebUI is equivalent to `OLLAMA_BASE_URL/api/tags` in the backend.
8
+
9
+ - **Security Benefits**: This design prevents direct exposure of the Ollama API to the frontend, safeguarding against potential CORS (Cross-Origin Resource Sharing) issues and unauthorized access. Requiring authentication to access the Ollama API further enhances this security layer.
10
+
11
+ ## Open WebUI: Server Connection Error
12
+
13
+ If you're experiencing connection issues, it’s often due to the WebUI docker container not being able to reach the Ollama server at 127.0.0.1:11434 (host.docker.internal:11434) inside the container . Use the `--network=host` flag in your docker command to resolve this. Note that the port changes from 3000 to 8080, resulting in the link: `http://localhost:8080`.
14
+
15
+ **Example Docker Command**:
16
+
17
+ ```bash
18
+ docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_BASE_URL=http://127.0.0.1:11434 --name open-webui --restart always ghcr.io/open-webui/open-webui:main
19
+ ```
20
+
21
+ ### Error on Slow Responses for Ollama
22
+
23
+ Open WebUI has a default timeout of 5 minutes for Ollama to finish generating the response. If needed, this can be adjusted via the environment variable AIOHTTP_CLIENT_TIMEOUT, which sets the timeout in seconds.
24
+
25
+ ### General Connection Errors
26
+
27
+ **Ensure Ollama Version is Up-to-Date**: Always start by checking that you have the latest version of Ollama. Visit [Ollama's official site](https://ollama.com/) for the latest updates.
28
+
29
+ **Troubleshooting Steps**:
30
+
31
+ 1. **Verify Ollama URL Format**:
32
+ - When running the Web UI container, ensure the `OLLAMA_BASE_URL` is correctly set. (e.g., `http://192.168.1.1:11434` for different host setups).
33
+ - In the Open WebUI, navigate to "Settings" > "General".
34
+ - Confirm that the Ollama Server URL is correctly set to `[OLLAMA URL]` (e.g., `http://localhost:11434`).
35
+
36
+ By following these enhanced troubleshooting steps, connection issues should be effectively resolved. For further assistance or queries, feel free to reach out to us on our community Discord.
backend/.dockerignore ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__
2
+ .env
3
+ _old
4
+ uploads
5
+ .ipynb_checkpoints
6
+ *.db
7
+ _test
8
+ !/data
9
+ /data/*
10
+ !/data/litellm
11
+ /data/litellm/*
12
+ !data/litellm/config.yaml
13
+
14
+ !data/config.json
backend/.gitignore ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__
2
+ .env
3
+ _old
4
+ uploads
5
+ .ipynb_checkpoints
6
+ *.db
7
+ _test
8
+ Pipfile
9
+ !/data
10
+ /data/*
11
+ /open_webui/data/*
12
+ .webui_secret_key
backend/dev.sh ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export CORS_ALLOW_ORIGIN="http://localhost:5173"
2
+ PORT="${PORT:-8080}"
3
+ uvicorn open_webui.main:app --port $PORT --host 0.0.0.0 --forwarded-allow-ips '*' --reload
backend/open_webui/__init__.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import os
3
+ import random
4
+ from pathlib import Path
5
+
6
+ import typer
7
+ import uvicorn
8
+ from typing import Optional
9
+ from typing_extensions import Annotated
10
+
11
+ app = typer.Typer()
12
+
13
+ KEY_FILE = Path.cwd() / ".webui_secret_key"
14
+
15
+
16
+ def version_callback(value: bool):
17
+ if value:
18
+ from open_webui.env import VERSION
19
+
20
+ typer.echo(f"Open WebUI version: {VERSION}")
21
+ raise typer.Exit()
22
+
23
+
24
+ @app.command()
25
+ def main(
26
+ version: Annotated[
27
+ Optional[bool], typer.Option("--version", callback=version_callback)
28
+ ] = None,
29
+ ):
30
+ pass
31
+
32
+
33
+ @app.command()
34
+ def serve(
35
+ host: str = "0.0.0.0",
36
+ port: int = 8080,
37
+ ):
38
+ os.environ["FROM_INIT_PY"] = "true"
39
+ if os.getenv("WEBUI_SECRET_KEY") is None:
40
+ typer.echo(
41
+ "Loading WEBUI_SECRET_KEY from file, not provided as an environment variable."
42
+ )
43
+ if not KEY_FILE.exists():
44
+ typer.echo(f"Generating a new secret key and saving it to {KEY_FILE}")
45
+ KEY_FILE.write_bytes(base64.b64encode(random.randbytes(12)))
46
+ typer.echo(f"Loading WEBUI_SECRET_KEY from {KEY_FILE}")
47
+ os.environ["WEBUI_SECRET_KEY"] = KEY_FILE.read_text()
48
+
49
+ if os.getenv("USE_CUDA_DOCKER", "false") == "true":
50
+ typer.echo(
51
+ "CUDA is enabled, appending LD_LIBRARY_PATH to include torch/cudnn & cublas libraries."
52
+ )
53
+ LD_LIBRARY_PATH = os.getenv("LD_LIBRARY_PATH", "").split(":")
54
+ os.environ["LD_LIBRARY_PATH"] = ":".join(
55
+ LD_LIBRARY_PATH
56
+ + [
57
+ "/usr/local/lib/python3.11/site-packages/torch/lib",
58
+ "/usr/local/lib/python3.11/site-packages/nvidia/cudnn/lib",
59
+ ]
60
+ )
61
+ try:
62
+ import torch
63
+
64
+ assert torch.cuda.is_available(), "CUDA not available"
65
+ typer.echo("CUDA seems to be working")
66
+ except Exception as e:
67
+ typer.echo(
68
+ "Error when testing CUDA but USE_CUDA_DOCKER is true. "
69
+ "Resetting USE_CUDA_DOCKER to false and removing "
70
+ f"LD_LIBRARY_PATH modifications: {e}"
71
+ )
72
+ os.environ["USE_CUDA_DOCKER"] = "false"
73
+ os.environ["LD_LIBRARY_PATH"] = ":".join(LD_LIBRARY_PATH)
74
+
75
+ import open_webui.main # we need set environment variables before importing main
76
+ from open_webui.env import UVICORN_WORKERS # Import the workers setting
77
+
78
+ uvicorn.run(
79
+ "open_webui.main:app",
80
+ host=host,
81
+ port=port,
82
+ forwarded_allow_ips="*",
83
+ workers=UVICORN_WORKERS,
84
+ )
85
+
86
+
87
+ @app.command()
88
+ def dev(
89
+ host: str = "0.0.0.0",
90
+ port: int = 8080,
91
+ reload: bool = True,
92
+ ):
93
+ uvicorn.run(
94
+ "open_webui.main:app",
95
+ host=host,
96
+ port=port,
97
+ reload=reload,
98
+ forwarded_allow_ips="*",
99
+ )
100
+
101
+
102
+ if __name__ == "__main__":
103
+ app()
backend/open_webui/alembic.ini ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # A generic, single database configuration.
2
+
3
+ [alembic]
4
+ # path to migration scripts
5
+ script_location = migrations
6
+
7
+ # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
8
+ # Uncomment the line below if you want the files to be prepended with date and time
9
+ # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
10
+
11
+ # sys.path path, will be prepended to sys.path if present.
12
+ # defaults to the current working directory.
13
+ prepend_sys_path = ..
14
+
15
+ # timezone to use when rendering the date within the migration file
16
+ # as well as the filename.
17
+ # If specified, requires the python>=3.9 or backports.zoneinfo library.
18
+ # Any required deps can installed by adding `alembic[tz]` to the pip requirements
19
+ # string value is passed to ZoneInfo()
20
+ # leave blank for localtime
21
+ # timezone =
22
+
23
+ # max length of characters to apply to the
24
+ # "slug" field
25
+ # truncate_slug_length = 40
26
+
27
+ # set to 'true' to run the environment during
28
+ # the 'revision' command, regardless of autogenerate
29
+ # revision_environment = false
30
+
31
+ # set to 'true' to allow .pyc and .pyo files without
32
+ # a source .py file to be detected as revisions in the
33
+ # versions/ directory
34
+ # sourceless = false
35
+
36
+ # version location specification; This defaults
37
+ # to migrations/versions. When using multiple version
38
+ # directories, initial revisions must be specified with --version-path.
39
+ # The path separator used here should be the separator specified by "version_path_separator" below.
40
+ # version_locations = %(here)s/bar:%(here)s/bat:migrations/versions
41
+
42
+ # version path separator; As mentioned above, this is the character used to split
43
+ # version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
44
+ # If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
45
+ # Valid values for version_path_separator are:
46
+ #
47
+ # version_path_separator = :
48
+ # version_path_separator = ;
49
+ # version_path_separator = space
50
+ version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
51
+
52
+ # set to 'true' to search source files recursively
53
+ # in each "version_locations" directory
54
+ # new in Alembic version 1.10
55
+ # recursive_version_locations = false
56
+
57
+ # the output encoding used when revision files
58
+ # are written from script.py.mako
59
+ # output_encoding = utf-8
60
+
61
+ # sqlalchemy.url = REPLACE_WITH_DATABASE_URL
62
+
63
+
64
+ [post_write_hooks]
65
+ # post_write_hooks defines scripts or Python functions that are run
66
+ # on newly generated revision scripts. See the documentation for further
67
+ # detail and examples
68
+
69
+ # format using "black" - use the console_scripts runner, against the "black" entrypoint
70
+ # hooks = black
71
+ # black.type = console_scripts
72
+ # black.entrypoint = black
73
+ # black.options = -l 79 REVISION_SCRIPT_FILENAME
74
+
75
+ # lint with attempts to fix using "ruff" - use the exec runner, execute a binary
76
+ # hooks = ruff
77
+ # ruff.type = exec
78
+ # ruff.executable = %(here)s/.venv/bin/ruff
79
+ # ruff.options = --fix REVISION_SCRIPT_FILENAME
80
+
81
+ # Logging configuration
82
+ [loggers]
83
+ keys = root,sqlalchemy,alembic
84
+
85
+ [handlers]
86
+ keys = console
87
+
88
+ [formatters]
89
+ keys = generic
90
+
91
+ [logger_root]
92
+ level = WARN
93
+ handlers = console
94
+ qualname =
95
+
96
+ [logger_sqlalchemy]
97
+ level = WARN
98
+ handlers =
99
+ qualname = sqlalchemy.engine
100
+
101
+ [logger_alembic]
102
+ level = INFO
103
+ handlers =
104
+ qualname = alembic
105
+
106
+ [handler_console]
107
+ class = StreamHandler
108
+ args = (sys.stderr,)
109
+ level = NOTSET
110
+ formatter = generic
111
+
112
+ [formatter_generic]
113
+ format = %(levelname)-5.5s [%(name)s] %(message)s
114
+ datefmt = %H:%M:%S
backend/open_webui/config.py ADDED
The diff for this file is too large to render. See raw diff
 
backend/open_webui/constants.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from enum import Enum
2
+
3
+
4
+ class MESSAGES(str, Enum):
5
+ DEFAULT = lambda msg="": f"{msg if msg else ''}"
6
+ MODEL_ADDED = lambda model="": f"The model '{model}' has been added successfully."
7
+ MODEL_DELETED = (
8
+ lambda model="": f"The model '{model}' has been deleted successfully."
9
+ )
10
+
11
+
12
+ class WEBHOOK_MESSAGES(str, Enum):
13
+ DEFAULT = lambda msg="": f"{msg if msg else ''}"
14
+ USER_SIGNUP = lambda username="": (
15
+ f"New user signed up: {username}" if username else "New user signed up"
16
+ )
17
+
18
+
19
+ class ERROR_MESSAGES(str, Enum):
20
+ def __str__(self) -> str:
21
+ return super().__str__()
22
+
23
+ DEFAULT = (
24
+ lambda err="": f'{"Something went wrong :/" if err == "" else "[ERROR: " + str(err) + "]"}'
25
+ )
26
+ ENV_VAR_NOT_FOUND = "Required environment variable not found. Terminating now."
27
+ CREATE_USER_ERROR = "Oops! Something went wrong while creating your account. Please try again later. If the issue persists, contact support for assistance."
28
+ DELETE_USER_ERROR = "Oops! Something went wrong. We encountered an issue while trying to delete the user. Please give it another shot."
29
+ EMAIL_MISMATCH = "Uh-oh! This email does not match the email your provider is registered with. Please check your email and try again."
30
+ EMAIL_TAKEN = "Uh-oh! This email is already registered. Sign in with your existing account or choose another email to start anew."
31
+ USERNAME_TAKEN = (
32
+ "Uh-oh! This username is already registered. Please choose another username."
33
+ )
34
+ PASSWORD_TOO_LONG = "Uh-oh! The password you entered is too long. Please make sure your password is less than 72 bytes long."
35
+ COMMAND_TAKEN = "Uh-oh! This command is already registered. Please choose another command string."
36
+ FILE_EXISTS = "Uh-oh! This file is already registered. Please choose another file."
37
+
38
+ ID_TAKEN = "Uh-oh! This id is already registered. Please choose another id string."
39
+ MODEL_ID_TAKEN = "Uh-oh! This model id is already registered. Please choose another model id string."
40
+ NAME_TAG_TAKEN = "Uh-oh! This name tag is already registered. Please choose another name tag string."
41
+
42
+ INVALID_TOKEN = (
43
+ "Your session has expired or the token is invalid. Please sign in again."
44
+ )
45
+ INVALID_CRED = "The email or password provided is incorrect. Please check for typos and try logging in again."
46
+ INVALID_EMAIL_FORMAT = "The email format you entered is invalid. Please double-check and make sure you're using a valid email address (e.g., [email protected])."
47
+ INVALID_PASSWORD = (
48
+ "The password provided is incorrect. Please check for typos and try again."
49
+ )
50
+ INVALID_TRUSTED_HEADER = "Your provider has not provided a trusted header. Please contact your administrator for assistance."
51
+
52
+ EXISTING_USERS = "You can't turn off authentication because there are existing users. If you want to disable WEBUI_AUTH, make sure your web interface doesn't have any existing users and is a fresh installation."
53
+
54
+ UNAUTHORIZED = "401 Unauthorized"
55
+ ACCESS_PROHIBITED = "You do not have permission to access this resource. Please contact your administrator for assistance."
56
+ ACTION_PROHIBITED = (
57
+ "The requested action has been restricted as a security measure."
58
+ )
59
+
60
+ FILE_NOT_SENT = "FILE_NOT_SENT"
61
+ FILE_NOT_SUPPORTED = "Oops! It seems like the file format you're trying to upload is not supported. Please upload a file with a supported format and try again."
62
+
63
+ NOT_FOUND = "We could not find what you're looking for :/"
64
+ USER_NOT_FOUND = "We could not find what you're looking for :/"
65
+ API_KEY_NOT_FOUND = "Oops! It looks like there's a hiccup. The API key is missing. Please make sure to provide a valid API key to access this feature."
66
+ API_KEY_NOT_ALLOWED = "Use of API key is not enabled in the environment."
67
+
68
+ MALICIOUS = "Unusual activities detected, please try again in a few minutes."
69
+
70
+ PANDOC_NOT_INSTALLED = "Pandoc is not installed on the server. Please contact your administrator for assistance."
71
+ INCORRECT_FORMAT = (
72
+ lambda err="": f"Invalid format. Please use the correct format{err}"
73
+ )
74
+ RATE_LIMIT_EXCEEDED = "API rate limit exceeded"
75
+
76
+ MODEL_NOT_FOUND = lambda name="": f"Model '{name}' was not found"
77
+ OPENAI_NOT_FOUND = lambda name="": "OpenAI API was not found"
78
+ OLLAMA_NOT_FOUND = "WebUI could not connect to Ollama"
79
+ CREATE_API_KEY_ERROR = "Oops! Something went wrong while creating your API key. Please try again later. If the issue persists, contact support for assistance."
80
+ API_KEY_CREATION_NOT_ALLOWED = "API key creation is not allowed in the environment."
81
+
82
+ EMPTY_CONTENT = "The content provided is empty. Please ensure that there is text or data present before proceeding."
83
+
84
+ DB_NOT_SQLITE = "This feature is only available when running with SQLite databases."
85
+
86
+ INVALID_URL = (
87
+ "Oops! The URL you provided is invalid. Please double-check and try again."
88
+ )
89
+
90
+ WEB_SEARCH_ERROR = (
91
+ lambda err="": f"{err if err else 'Oops! Something went wrong while searching the web.'}"
92
+ )
93
+
94
+ OLLAMA_API_DISABLED = (
95
+ "The Ollama API is disabled. Please enable it to use this feature."
96
+ )
97
+
98
+ FILE_TOO_LARGE = (
99
+ lambda size="": f"Oops! The file you're trying to upload is too large. Please upload a file that is less than {size}."
100
+ )
101
+
102
+ DUPLICATE_CONTENT = (
103
+ "Duplicate content detected. Please provide unique content to proceed."
104
+ )
105
+ FILE_NOT_PROCESSED = "Extracted content is not available for this file. Please ensure that the file is processed before proceeding."
106
+
107
+
108
+ class TASKS(str, Enum):
109
+ def __str__(self) -> str:
110
+ return super().__str__()
111
+
112
+ DEFAULT = lambda task="": f"{task if task else 'generation'}"
113
+ TITLE_GENERATION = "title_generation"
114
+ FOLLOW_UP_GENERATION = "follow_up_generation"
115
+ TAGS_GENERATION = "tags_generation"
116
+ EMOJI_GENERATION = "emoji_generation"
117
+ QUERY_GENERATION = "query_generation"
118
+ IMAGE_PROMPT_GENERATION = "image_prompt_generation"
119
+ AUTOCOMPLETE_GENERATION = "autocomplete_generation"
120
+ FUNCTION_CALLING = "function_calling"
121
+ MOA_RESPONSE_GENERATION = "moa_response_generation"
backend/open_webui/env.py ADDED
@@ -0,0 +1,773 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import importlib.metadata
2
+ import json
3
+ import logging
4
+ import os
5
+ import pkgutil
6
+ import sys
7
+ import shutil
8
+ from uuid import uuid4
9
+ from pathlib import Path
10
+ from cryptography.hazmat.primitives import serialization
11
+
12
+ import markdown
13
+ from bs4 import BeautifulSoup
14
+ from open_webui.constants import ERROR_MESSAGES
15
+
16
+ ####################################
17
+ # Load .env file
18
+ ####################################
19
+
20
+ # Use .resolve() to get the canonical path, removing any '..' or '.' components
21
+ ENV_FILE_PATH = Path(__file__).resolve()
22
+
23
+ # OPEN_WEBUI_DIR should be the directory where env.py resides (open_webui/)
24
+ OPEN_WEBUI_DIR = ENV_FILE_PATH.parent
25
+
26
+ # BACKEND_DIR is the parent of OPEN_WEBUI_DIR (backend/)
27
+ BACKEND_DIR = OPEN_WEBUI_DIR.parent
28
+
29
+ # BASE_DIR is the parent of BACKEND_DIR (open-webui-dev/)
30
+ BASE_DIR = BACKEND_DIR.parent
31
+
32
+ try:
33
+ from dotenv import find_dotenv, load_dotenv
34
+
35
+ load_dotenv(find_dotenv(str(BASE_DIR / ".env")))
36
+ except ImportError:
37
+ print("dotenv not installed, skipping...")
38
+
39
+ DOCKER = os.environ.get("DOCKER", "False").lower() == "true"
40
+
41
+ # device type embedding models - "cpu" (default), "cuda" (nvidia gpu required) or "mps" (apple silicon) - choosing this right can lead to better performance
42
+ USE_CUDA = os.environ.get("USE_CUDA_DOCKER", "false")
43
+
44
+ if USE_CUDA.lower() == "true":
45
+ try:
46
+ import torch
47
+
48
+ assert torch.cuda.is_available(), "CUDA not available"
49
+ DEVICE_TYPE = "cuda"
50
+ except Exception as e:
51
+ cuda_error = (
52
+ "Error when testing CUDA but USE_CUDA_DOCKER is true. "
53
+ f"Resetting USE_CUDA_DOCKER to false: {e}"
54
+ )
55
+ os.environ["USE_CUDA_DOCKER"] = "false"
56
+ USE_CUDA = "false"
57
+ DEVICE_TYPE = "cpu"
58
+ else:
59
+ DEVICE_TYPE = "cpu"
60
+
61
+ try:
62
+ import torch
63
+
64
+ if torch.backends.mps.is_available() and torch.backends.mps.is_built():
65
+ DEVICE_TYPE = "mps"
66
+ except Exception:
67
+ pass
68
+
69
+ ####################################
70
+ # LOGGING
71
+ ####################################
72
+
73
+ GLOBAL_LOG_LEVEL = os.environ.get("GLOBAL_LOG_LEVEL", "").upper()
74
+ if GLOBAL_LOG_LEVEL in logging.getLevelNamesMapping():
75
+ logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL, force=True)
76
+ else:
77
+ GLOBAL_LOG_LEVEL = "INFO"
78
+
79
+ log = logging.getLogger(__name__)
80
+ log.info(f"GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}")
81
+
82
+ if "cuda_error" in locals():
83
+ log.exception(cuda_error)
84
+ del cuda_error
85
+
86
+ log_sources = [
87
+ "AUDIO",
88
+ "COMFYUI",
89
+ "CONFIG",
90
+ "DB",
91
+ "IMAGES",
92
+ "MAIN",
93
+ "MODELS",
94
+ "OLLAMA",
95
+ "OPENAI",
96
+ "RAG",
97
+ "WEBHOOK",
98
+ "SOCKET",
99
+ "OAUTH",
100
+ ]
101
+
102
+ SRC_LOG_LEVELS = {}
103
+
104
+ for source in log_sources:
105
+ log_env_var = source + "_LOG_LEVEL"
106
+ SRC_LOG_LEVELS[source] = os.environ.get(log_env_var, "").upper()
107
+ if SRC_LOG_LEVELS[source] not in logging.getLevelNamesMapping():
108
+ SRC_LOG_LEVELS[source] = GLOBAL_LOG_LEVEL
109
+ log.info(f"{log_env_var}: {SRC_LOG_LEVELS[source]}")
110
+
111
+ log.setLevel(SRC_LOG_LEVELS["CONFIG"])
112
+
113
+ WEBUI_NAME = os.environ.get("WEBUI_NAME", "Open WebUI")
114
+ if WEBUI_NAME != "Open WebUI":
115
+ WEBUI_NAME += " (Open WebUI)"
116
+
117
+ WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png"
118
+
119
+ TRUSTED_SIGNATURE_KEY = os.environ.get("TRUSTED_SIGNATURE_KEY", "")
120
+
121
+ ####################################
122
+ # ENV (dev,test,prod)
123
+ ####################################
124
+
125
+ ENV = os.environ.get("ENV", "dev")
126
+
127
+ FROM_INIT_PY = os.environ.get("FROM_INIT_PY", "False").lower() == "true"
128
+
129
+ if FROM_INIT_PY:
130
+ PACKAGE_DATA = {"version": importlib.metadata.version("open-webui")}
131
+ else:
132
+ try:
133
+ PACKAGE_DATA = json.loads((BASE_DIR / "package.json").read_text())
134
+ except Exception:
135
+ PACKAGE_DATA = {"version": "0.0.0"}
136
+
137
+ VERSION = PACKAGE_DATA["version"]
138
+ INSTANCE_ID = os.environ.get("INSTANCE_ID", str(uuid4()))
139
+
140
+
141
+ # Function to parse each section
142
+ def parse_section(section):
143
+ items = []
144
+ for li in section.find_all("li"):
145
+ # Extract raw HTML string
146
+ raw_html = str(li)
147
+
148
+ # Extract text without HTML tags
149
+ text = li.get_text(separator=" ", strip=True)
150
+
151
+ # Split into title and content
152
+ parts = text.split(": ", 1)
153
+ title = parts[0].strip() if len(parts) > 1 else ""
154
+ content = parts[1].strip() if len(parts) > 1 else text
155
+
156
+ items.append({"title": title, "content": content, "raw": raw_html})
157
+ return items
158
+
159
+
160
+ try:
161
+ changelog_path = BASE_DIR / "CHANGELOG.md"
162
+ with open(str(changelog_path.absolute()), "r", encoding="utf8") as file:
163
+ changelog_content = file.read()
164
+
165
+ except Exception:
166
+ changelog_content = (pkgutil.get_data("open_webui", "CHANGELOG.md") or b"").decode()
167
+
168
+ # Convert markdown content to HTML
169
+ html_content = markdown.markdown(changelog_content)
170
+
171
+ # Parse the HTML content
172
+ soup = BeautifulSoup(html_content, "html.parser")
173
+
174
+ # Initialize JSON structure
175
+ changelog_json = {}
176
+
177
+ # Iterate over each version
178
+ for version in soup.find_all("h2"):
179
+ version_number = version.get_text().strip().split(" - ")[0][1:-1] # Remove brackets
180
+ date = version.get_text().strip().split(" - ")[1]
181
+
182
+ version_data = {"date": date}
183
+
184
+ # Find the next sibling that is a h3 tag (section title)
185
+ current = version.find_next_sibling()
186
+
187
+ while current and current.name != "h2":
188
+ if current.name == "h3":
189
+ section_title = current.get_text().lower() # e.g., "added", "fixed"
190
+ section_items = parse_section(current.find_next_sibling("ul"))
191
+ version_data[section_title] = section_items
192
+
193
+ # Move to the next element
194
+ current = current.find_next_sibling()
195
+
196
+ changelog_json[version_number] = version_data
197
+
198
+ CHANGELOG = changelog_json
199
+
200
+ ####################################
201
+ # SAFE_MODE
202
+ ####################################
203
+
204
+ SAFE_MODE = os.environ.get("SAFE_MODE", "false").lower() == "true"
205
+
206
+
207
+ ####################################
208
+ # ENABLE_FORWARD_USER_INFO_HEADERS
209
+ ####################################
210
+
211
+ ENABLE_FORWARD_USER_INFO_HEADERS = (
212
+ os.environ.get("ENABLE_FORWARD_USER_INFO_HEADERS", "False").lower() == "true"
213
+ )
214
+
215
+ ####################################
216
+ # WEBUI_BUILD_HASH
217
+ ####################################
218
+
219
+ WEBUI_BUILD_HASH = os.environ.get("WEBUI_BUILD_HASH", "dev-build")
220
+
221
+ ####################################
222
+ # DATA/FRONTEND BUILD DIR
223
+ ####################################
224
+
225
+ DATA_DIR = Path(os.getenv("DATA_DIR", BACKEND_DIR / "data")).resolve()
226
+
227
+ if FROM_INIT_PY:
228
+ NEW_DATA_DIR = Path(os.getenv("DATA_DIR", OPEN_WEBUI_DIR / "data")).resolve()
229
+ NEW_DATA_DIR.mkdir(parents=True, exist_ok=True)
230
+
231
+ # Check if the data directory exists in the package directory
232
+ if DATA_DIR.exists() and DATA_DIR != NEW_DATA_DIR:
233
+ log.info(f"Moving {DATA_DIR} to {NEW_DATA_DIR}")
234
+ for item in DATA_DIR.iterdir():
235
+ dest = NEW_DATA_DIR / item.name
236
+ if item.is_dir():
237
+ shutil.copytree(item, dest, dirs_exist_ok=True)
238
+ else:
239
+ shutil.copy2(item, dest)
240
+
241
+ # Zip the data directory
242
+ shutil.make_archive(DATA_DIR.parent / "open_webui_data", "zip", DATA_DIR)
243
+
244
+ # Remove the old data directory
245
+ shutil.rmtree(DATA_DIR)
246
+
247
+ DATA_DIR = Path(os.getenv("DATA_DIR", OPEN_WEBUI_DIR / "data"))
248
+
249
+ STATIC_DIR = Path(os.getenv("STATIC_DIR", OPEN_WEBUI_DIR / "static"))
250
+
251
+ FONTS_DIR = Path(os.getenv("FONTS_DIR", OPEN_WEBUI_DIR / "static" / "fonts"))
252
+
253
+ FRONTEND_BUILD_DIR = Path(os.getenv("FRONTEND_BUILD_DIR", BASE_DIR / "build")).resolve()
254
+
255
+ if FROM_INIT_PY:
256
+ FRONTEND_BUILD_DIR = Path(
257
+ os.getenv("FRONTEND_BUILD_DIR", OPEN_WEBUI_DIR / "frontend")
258
+ ).resolve()
259
+
260
+ ####################################
261
+ # Database
262
+ ####################################
263
+
264
+ # Check if the file exists
265
+ if os.path.exists(f"{DATA_DIR}/ollama.db"):
266
+ # Rename the file
267
+ os.rename(f"{DATA_DIR}/ollama.db", f"{DATA_DIR}/webui.db")
268
+ log.info("Database migrated from Ollama-WebUI successfully.")
269
+ else:
270
+ pass
271
+
272
+ DATABASE_URL = os.environ.get("DATABASE_URL", f"sqlite:///{DATA_DIR}/webui.db")
273
+
274
+ DATABASE_TYPE = os.environ.get("DATABASE_TYPE")
275
+ DATABASE_USER = os.environ.get("DATABASE_USER")
276
+ DATABASE_PASSWORD = os.environ.get("DATABASE_PASSWORD")
277
+
278
+ DATABASE_CRED = ""
279
+ if DATABASE_USER:
280
+ DATABASE_CRED += f"{DATABASE_USER}"
281
+ if DATABASE_PASSWORD:
282
+ DATABASE_CRED += f":{DATABASE_PASSWORD}"
283
+
284
+ DB_VARS = {
285
+ "db_type": DATABASE_TYPE,
286
+ "db_cred": DATABASE_CRED,
287
+ "db_host": os.environ.get("DATABASE_HOST"),
288
+ "db_port": os.environ.get("DATABASE_PORT"),
289
+ "db_name": os.environ.get("DATABASE_NAME"),
290
+ }
291
+
292
+ if all(DB_VARS.values()):
293
+ DATABASE_URL = f"{DB_VARS['db_type']}://{DB_VARS['db_cred']}@{DB_VARS['db_host']}:{DB_VARS['db_port']}/{DB_VARS['db_name']}"
294
+ elif DATABASE_TYPE == "sqlite+sqlcipher" and not os.environ.get("DATABASE_URL"):
295
+ # Handle SQLCipher with local file when DATABASE_URL wasn't explicitly set
296
+ DATABASE_URL = f"sqlite+sqlcipher:///{DATA_DIR}/webui.db"
297
+
298
+ # Replace the postgres:// with postgresql://
299
+ if "postgres://" in DATABASE_URL:
300
+ DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql://")
301
+
302
+ DATABASE_SCHEMA = os.environ.get("DATABASE_SCHEMA", None)
303
+
304
+ DATABASE_POOL_SIZE = os.environ.get("DATABASE_POOL_SIZE", None)
305
+
306
+ if DATABASE_POOL_SIZE != None:
307
+ try:
308
+ DATABASE_POOL_SIZE = int(DATABASE_POOL_SIZE)
309
+ except Exception:
310
+ DATABASE_POOL_SIZE = None
311
+
312
+ DATABASE_POOL_MAX_OVERFLOW = os.environ.get("DATABASE_POOL_MAX_OVERFLOW", 0)
313
+
314
+ if DATABASE_POOL_MAX_OVERFLOW == "":
315
+ DATABASE_POOL_MAX_OVERFLOW = 0
316
+ else:
317
+ try:
318
+ DATABASE_POOL_MAX_OVERFLOW = int(DATABASE_POOL_MAX_OVERFLOW)
319
+ except Exception:
320
+ DATABASE_POOL_MAX_OVERFLOW = 0
321
+
322
+ DATABASE_POOL_TIMEOUT = os.environ.get("DATABASE_POOL_TIMEOUT", 30)
323
+
324
+ if DATABASE_POOL_TIMEOUT == "":
325
+ DATABASE_POOL_TIMEOUT = 30
326
+ else:
327
+ try:
328
+ DATABASE_POOL_TIMEOUT = int(DATABASE_POOL_TIMEOUT)
329
+ except Exception:
330
+ DATABASE_POOL_TIMEOUT = 30
331
+
332
+ DATABASE_POOL_RECYCLE = os.environ.get("DATABASE_POOL_RECYCLE", 3600)
333
+
334
+ if DATABASE_POOL_RECYCLE == "":
335
+ DATABASE_POOL_RECYCLE = 3600
336
+ else:
337
+ try:
338
+ DATABASE_POOL_RECYCLE = int(DATABASE_POOL_RECYCLE)
339
+ except Exception:
340
+ DATABASE_POOL_RECYCLE = 3600
341
+
342
+ DATABASE_ENABLE_SQLITE_WAL = (
343
+ os.environ.get("DATABASE_ENABLE_SQLITE_WAL", "False").lower() == "true"
344
+ )
345
+
346
+ DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL = os.environ.get(
347
+ "DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL", None
348
+ )
349
+ if DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL is not None:
350
+ try:
351
+ DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL = float(
352
+ DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL
353
+ )
354
+ except Exception:
355
+ DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL = 0.0
356
+
357
+ RESET_CONFIG_ON_START = (
358
+ os.environ.get("RESET_CONFIG_ON_START", "False").lower() == "true"
359
+ )
360
+
361
+ ENABLE_REALTIME_CHAT_SAVE = (
362
+ os.environ.get("ENABLE_REALTIME_CHAT_SAVE", "False").lower() == "true"
363
+ )
364
+
365
+ ####################################
366
+ # REDIS
367
+ ####################################
368
+
369
+ REDIS_URL = os.environ.get("REDIS_URL", "")
370
+ REDIS_CLUSTER = os.environ.get("REDIS_CLUSTER", "False").lower() == "true"
371
+
372
+ REDIS_KEY_PREFIX = os.environ.get("REDIS_KEY_PREFIX", "open-webui")
373
+
374
+ REDIS_SENTINEL_HOSTS = os.environ.get("REDIS_SENTINEL_HOSTS", "")
375
+ REDIS_SENTINEL_PORT = os.environ.get("REDIS_SENTINEL_PORT", "26379")
376
+
377
+ # Maximum number of retries for Redis operations when using Sentinel fail-over
378
+ REDIS_SENTINEL_MAX_RETRY_COUNT = os.environ.get("REDIS_SENTINEL_MAX_RETRY_COUNT", "2")
379
+ try:
380
+ REDIS_SENTINEL_MAX_RETRY_COUNT = int(REDIS_SENTINEL_MAX_RETRY_COUNT)
381
+ if REDIS_SENTINEL_MAX_RETRY_COUNT < 1:
382
+ REDIS_SENTINEL_MAX_RETRY_COUNT = 2
383
+ except ValueError:
384
+ REDIS_SENTINEL_MAX_RETRY_COUNT = 2
385
+
386
+ ####################################
387
+ # UVICORN WORKERS
388
+ ####################################
389
+
390
+ # Number of uvicorn worker processes for handling requests
391
+ UVICORN_WORKERS = os.environ.get("UVICORN_WORKERS", "1")
392
+ try:
393
+ UVICORN_WORKERS = int(UVICORN_WORKERS)
394
+ if UVICORN_WORKERS < 1:
395
+ UVICORN_WORKERS = 1
396
+ except ValueError:
397
+ UVICORN_WORKERS = 1
398
+ log.info(f"Invalid UVICORN_WORKERS value, defaulting to {UVICORN_WORKERS}")
399
+
400
+ ####################################
401
+ # WEBUI_AUTH (Required for security)
402
+ ####################################
403
+
404
+ WEBUI_AUTH = os.environ.get("WEBUI_AUTH", "True").lower() == "true"
405
+ ENABLE_SIGNUP_PASSWORD_CONFIRMATION = (
406
+ os.environ.get("ENABLE_SIGNUP_PASSWORD_CONFIRMATION", "False").lower() == "true"
407
+ )
408
+
409
+ WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get(
410
+ "WEBUI_AUTH_TRUSTED_EMAIL_HEADER", None
411
+ )
412
+ WEBUI_AUTH_TRUSTED_NAME_HEADER = os.environ.get("WEBUI_AUTH_TRUSTED_NAME_HEADER", None)
413
+ WEBUI_AUTH_TRUSTED_GROUPS_HEADER = os.environ.get(
414
+ "WEBUI_AUTH_TRUSTED_GROUPS_HEADER", None
415
+ )
416
+
417
+
418
+ BYPASS_MODEL_ACCESS_CONTROL = (
419
+ os.environ.get("BYPASS_MODEL_ACCESS_CONTROL", "False").lower() == "true"
420
+ )
421
+
422
+ WEBUI_AUTH_SIGNOUT_REDIRECT_URL = os.environ.get(
423
+ "WEBUI_AUTH_SIGNOUT_REDIRECT_URL", None
424
+ )
425
+
426
+ ####################################
427
+ # WEBUI_SECRET_KEY
428
+ ####################################
429
+
430
+ WEBUI_SECRET_KEY = os.environ.get(
431
+ "WEBUI_SECRET_KEY",
432
+ os.environ.get(
433
+ "WEBUI_JWT_SECRET_KEY", "t0p-s3cr3t"
434
+ ), # DEPRECATED: remove at next major version
435
+ )
436
+
437
+ WEBUI_SESSION_COOKIE_SAME_SITE = os.environ.get("WEBUI_SESSION_COOKIE_SAME_SITE", "lax")
438
+
439
+ WEBUI_SESSION_COOKIE_SECURE = (
440
+ os.environ.get("WEBUI_SESSION_COOKIE_SECURE", "false").lower() == "true"
441
+ )
442
+
443
+ WEBUI_AUTH_COOKIE_SAME_SITE = os.environ.get(
444
+ "WEBUI_AUTH_COOKIE_SAME_SITE", WEBUI_SESSION_COOKIE_SAME_SITE
445
+ )
446
+
447
+ WEBUI_AUTH_COOKIE_SECURE = (
448
+ os.environ.get(
449
+ "WEBUI_AUTH_COOKIE_SECURE",
450
+ os.environ.get("WEBUI_SESSION_COOKIE_SECURE", "false"),
451
+ ).lower()
452
+ == "true"
453
+ )
454
+
455
+ if WEBUI_AUTH and WEBUI_SECRET_KEY == "":
456
+ raise ValueError(ERROR_MESSAGES.ENV_VAR_NOT_FOUND)
457
+
458
+ ENABLE_COMPRESSION_MIDDLEWARE = (
459
+ os.environ.get("ENABLE_COMPRESSION_MIDDLEWARE", "True").lower() == "true"
460
+ )
461
+
462
+
463
+ ####################################
464
+ # SCIM Configuration
465
+ ####################################
466
+
467
+ SCIM_ENABLED = os.environ.get("SCIM_ENABLED", "False").lower() == "true"
468
+ SCIM_TOKEN = os.environ.get("SCIM_TOKEN", "")
469
+
470
+ ####################################
471
+ # LICENSE_KEY
472
+ ####################################
473
+
474
+ LICENSE_KEY = os.environ.get("LICENSE_KEY", "")
475
+
476
+ LICENSE_BLOB = None
477
+ LICENSE_BLOB_PATH = os.environ.get("LICENSE_BLOB_PATH", DATA_DIR / "l.data")
478
+ if LICENSE_BLOB_PATH and os.path.exists(LICENSE_BLOB_PATH):
479
+ with open(LICENSE_BLOB_PATH, "rb") as f:
480
+ LICENSE_BLOB = f.read()
481
+
482
+ LICENSE_PUBLIC_KEY = os.environ.get("LICENSE_PUBLIC_KEY", "")
483
+
484
+ pk = None
485
+ if LICENSE_PUBLIC_KEY:
486
+ pk = serialization.load_pem_public_key(
487
+ f"""
488
+ -----BEGIN PUBLIC KEY-----
489
+ {LICENSE_PUBLIC_KEY}
490
+ -----END PUBLIC KEY-----
491
+ """.encode(
492
+ "utf-8"
493
+ )
494
+ )
495
+
496
+
497
+ ####################################
498
+ # MODELS
499
+ ####################################
500
+
501
+ MODELS_CACHE_TTL = os.environ.get("MODELS_CACHE_TTL", "1")
502
+ if MODELS_CACHE_TTL == "":
503
+ MODELS_CACHE_TTL = None
504
+ else:
505
+ try:
506
+ MODELS_CACHE_TTL = int(MODELS_CACHE_TTL)
507
+ except Exception:
508
+ MODELS_CACHE_TTL = 1
509
+
510
+
511
+ ####################################
512
+ # CHAT
513
+ ####################################
514
+
515
+ CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = os.environ.get(
516
+ "CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE", "1"
517
+ )
518
+
519
+ if CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE == "":
520
+ CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = 1
521
+ else:
522
+ try:
523
+ CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = int(
524
+ CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE
525
+ )
526
+ except Exception:
527
+ CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = 1
528
+
529
+
530
+ ####################################
531
+ # WEBSOCKET SUPPORT
532
+ ####################################
533
+
534
+ ENABLE_WEBSOCKET_SUPPORT = (
535
+ os.environ.get("ENABLE_WEBSOCKET_SUPPORT", "True").lower() == "true"
536
+ )
537
+
538
+
539
+ WEBSOCKET_MANAGER = os.environ.get("WEBSOCKET_MANAGER", "")
540
+
541
+ WEBSOCKET_REDIS_URL = os.environ.get("WEBSOCKET_REDIS_URL", REDIS_URL)
542
+ WEBSOCKET_REDIS_CLUSTER = (
543
+ os.environ.get("WEBSOCKET_REDIS_CLUSTER", str(REDIS_CLUSTER)).lower() == "true"
544
+ )
545
+
546
+ websocket_redis_lock_timeout = os.environ.get("WEBSOCKET_REDIS_LOCK_TIMEOUT", "60")
547
+
548
+ try:
549
+ WEBSOCKET_REDIS_LOCK_TIMEOUT = int(websocket_redis_lock_timeout)
550
+ except ValueError:
551
+ WEBSOCKET_REDIS_LOCK_TIMEOUT = 60
552
+
553
+ WEBSOCKET_SENTINEL_HOSTS = os.environ.get("WEBSOCKET_SENTINEL_HOSTS", "")
554
+ WEBSOCKET_SENTINEL_PORT = os.environ.get("WEBSOCKET_SENTINEL_PORT", "26379")
555
+
556
+
557
+ AIOHTTP_CLIENT_TIMEOUT = os.environ.get("AIOHTTP_CLIENT_TIMEOUT", "")
558
+
559
+ if AIOHTTP_CLIENT_TIMEOUT == "":
560
+ AIOHTTP_CLIENT_TIMEOUT = None
561
+ else:
562
+ try:
563
+ AIOHTTP_CLIENT_TIMEOUT = int(AIOHTTP_CLIENT_TIMEOUT)
564
+ except Exception:
565
+ AIOHTTP_CLIENT_TIMEOUT = 300
566
+
567
+
568
+ AIOHTTP_CLIENT_SESSION_SSL = (
569
+ os.environ.get("AIOHTTP_CLIENT_SESSION_SSL", "True").lower() == "true"
570
+ )
571
+
572
+ AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = os.environ.get(
573
+ "AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST",
574
+ os.environ.get("AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST", "10"),
575
+ )
576
+
577
+ if AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST == "":
578
+ AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = None
579
+ else:
580
+ try:
581
+ AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = int(AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
582
+ except Exception:
583
+ AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = 10
584
+
585
+
586
+ AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = os.environ.get(
587
+ "AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA", "10"
588
+ )
589
+
590
+ if AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA == "":
591
+ AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = None
592
+ else:
593
+ try:
594
+ AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = int(
595
+ AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA
596
+ )
597
+ except Exception:
598
+ AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = 10
599
+
600
+
601
+ AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL = (
602
+ os.environ.get("AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL", "True").lower() == "true"
603
+ )
604
+
605
+
606
+ ####################################
607
+ # SENTENCE TRANSFORMERS
608
+ ####################################
609
+
610
+
611
+ SENTENCE_TRANSFORMERS_BACKEND = os.environ.get("SENTENCE_TRANSFORMERS_BACKEND", "")
612
+ if SENTENCE_TRANSFORMERS_BACKEND == "":
613
+ SENTENCE_TRANSFORMERS_BACKEND = "torch"
614
+
615
+
616
+ SENTENCE_TRANSFORMERS_MODEL_KWARGS = os.environ.get(
617
+ "SENTENCE_TRANSFORMERS_MODEL_KWARGS", ""
618
+ )
619
+ if SENTENCE_TRANSFORMERS_MODEL_KWARGS == "":
620
+ SENTENCE_TRANSFORMERS_MODEL_KWARGS = None
621
+ else:
622
+ try:
623
+ SENTENCE_TRANSFORMERS_MODEL_KWARGS = json.loads(
624
+ SENTENCE_TRANSFORMERS_MODEL_KWARGS
625
+ )
626
+ except Exception:
627
+ SENTENCE_TRANSFORMERS_MODEL_KWARGS = None
628
+
629
+
630
+ SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND = os.environ.get(
631
+ "SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND", ""
632
+ )
633
+ if SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND == "":
634
+ SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND = "torch"
635
+
636
+
637
+ SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = os.environ.get(
638
+ "SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS", ""
639
+ )
640
+ if SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS == "":
641
+ SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = None
642
+ else:
643
+ try:
644
+ SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = json.loads(
645
+ SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS
646
+ )
647
+ except Exception:
648
+ SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = None
649
+
650
+ ####################################
651
+ # OFFLINE_MODE
652
+ ####################################
653
+
654
+ ENABLE_VERSION_UPDATE_CHECK = (
655
+ os.environ.get("ENABLE_VERSION_UPDATE_CHECK", "true").lower() == "true"
656
+ )
657
+ OFFLINE_MODE = os.environ.get("OFFLINE_MODE", "false").lower() == "true"
658
+
659
+ if OFFLINE_MODE:
660
+ os.environ["HF_HUB_OFFLINE"] = "1"
661
+ ENABLE_VERSION_UPDATE_CHECK = False
662
+
663
+ ####################################
664
+ # AUDIT LOGGING
665
+ ####################################
666
+ # Where to store log file
667
+ AUDIT_LOGS_FILE_PATH = f"{DATA_DIR}/audit.log"
668
+ # Maximum size of a file before rotating into a new log file
669
+ AUDIT_LOG_FILE_ROTATION_SIZE = os.getenv("AUDIT_LOG_FILE_ROTATION_SIZE", "10MB")
670
+
671
+ # Comma separated list of logger names to use for audit logging
672
+ # Default is "uvicorn.access" which is the access log for Uvicorn
673
+ # You can add more logger names to this list if you want to capture more logs
674
+ AUDIT_UVICORN_LOGGER_NAMES = os.getenv(
675
+ "AUDIT_UVICORN_LOGGER_NAMES", "uvicorn.access"
676
+ ).split(",")
677
+
678
+ # METADATA | REQUEST | REQUEST_RESPONSE
679
+ AUDIT_LOG_LEVEL = os.getenv("AUDIT_LOG_LEVEL", "NONE").upper()
680
+ try:
681
+ MAX_BODY_LOG_SIZE = int(os.environ.get("MAX_BODY_LOG_SIZE") or 2048)
682
+ except ValueError:
683
+ MAX_BODY_LOG_SIZE = 2048
684
+
685
+ # Comma separated list for urls to exclude from audit
686
+ AUDIT_EXCLUDED_PATHS = os.getenv("AUDIT_EXCLUDED_PATHS", "/chats,/chat,/folders").split(
687
+ ","
688
+ )
689
+ AUDIT_EXCLUDED_PATHS = [path.strip() for path in AUDIT_EXCLUDED_PATHS]
690
+ AUDIT_EXCLUDED_PATHS = [path.lstrip("/") for path in AUDIT_EXCLUDED_PATHS]
691
+
692
+
693
+ ####################################
694
+ # OPENTELEMETRY
695
+ ####################################
696
+
697
+ ENABLE_OTEL = os.environ.get("ENABLE_OTEL", "False").lower() == "true"
698
+ ENABLE_OTEL_TRACES = os.environ.get("ENABLE_OTEL_TRACES", "False").lower() == "true"
699
+ ENABLE_OTEL_METRICS = os.environ.get("ENABLE_OTEL_METRICS", "False").lower() == "true"
700
+ ENABLE_OTEL_LOGS = os.environ.get("ENABLE_OTEL_LOGS", "False").lower() == "true"
701
+
702
+ OTEL_EXPORTER_OTLP_ENDPOINT = os.environ.get(
703
+ "OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317"
704
+ )
705
+ OTEL_METRICS_EXPORTER_OTLP_ENDPOINT = os.environ.get(
706
+ "OTEL_METRICS_EXPORTER_OTLP_ENDPOINT", OTEL_EXPORTER_OTLP_ENDPOINT
707
+ )
708
+ OTEL_LOGS_EXPORTER_OTLP_ENDPOINT = os.environ.get(
709
+ "OTEL_LOGS_EXPORTER_OTLP_ENDPOINT", OTEL_EXPORTER_OTLP_ENDPOINT
710
+ )
711
+ OTEL_EXPORTER_OTLP_INSECURE = (
712
+ os.environ.get("OTEL_EXPORTER_OTLP_INSECURE", "False").lower() == "true"
713
+ )
714
+ OTEL_METRICS_EXPORTER_OTLP_INSECURE = (
715
+ os.environ.get(
716
+ "OTEL_METRICS_EXPORTER_OTLP_INSECURE", str(OTEL_EXPORTER_OTLP_INSECURE)
717
+ ).lower()
718
+ == "true"
719
+ )
720
+ OTEL_LOGS_EXPORTER_OTLP_INSECURE = (
721
+ os.environ.get(
722
+ "OTEL_LOGS_EXPORTER_OTLP_INSECURE", str(OTEL_EXPORTER_OTLP_INSECURE)
723
+ ).lower()
724
+ == "true"
725
+ )
726
+ OTEL_SERVICE_NAME = os.environ.get("OTEL_SERVICE_NAME", "open-webui")
727
+ OTEL_RESOURCE_ATTRIBUTES = os.environ.get(
728
+ "OTEL_RESOURCE_ATTRIBUTES", ""
729
+ ) # e.g. key1=val1,key2=val2
730
+ OTEL_TRACES_SAMPLER = os.environ.get(
731
+ "OTEL_TRACES_SAMPLER", "parentbased_always_on"
732
+ ).lower()
733
+ OTEL_BASIC_AUTH_USERNAME = os.environ.get("OTEL_BASIC_AUTH_USERNAME", "")
734
+ OTEL_BASIC_AUTH_PASSWORD = os.environ.get("OTEL_BASIC_AUTH_PASSWORD", "")
735
+
736
+ OTEL_METRICS_BASIC_AUTH_USERNAME = os.environ.get(
737
+ "OTEL_METRICS_BASIC_AUTH_USERNAME", OTEL_BASIC_AUTH_USERNAME
738
+ )
739
+ OTEL_METRICS_BASIC_AUTH_PASSWORD = os.environ.get(
740
+ "OTEL_METRICS_BASIC_AUTH_PASSWORD", OTEL_BASIC_AUTH_PASSWORD
741
+ )
742
+ OTEL_LOGS_BASIC_AUTH_USERNAME = os.environ.get(
743
+ "OTEL_LOGS_BASIC_AUTH_USERNAME", OTEL_BASIC_AUTH_USERNAME
744
+ )
745
+ OTEL_LOGS_BASIC_AUTH_PASSWORD = os.environ.get(
746
+ "OTEL_LOGS_BASIC_AUTH_PASSWORD", OTEL_BASIC_AUTH_PASSWORD
747
+ )
748
+
749
+ OTEL_OTLP_SPAN_EXPORTER = os.environ.get(
750
+ "OTEL_OTLP_SPAN_EXPORTER", "grpc"
751
+ ).lower() # grpc or http
752
+
753
+ OTEL_METRICS_OTLP_SPAN_EXPORTER = os.environ.get(
754
+ "OTEL_METRICS_OTLP_SPAN_EXPORTER", OTEL_OTLP_SPAN_EXPORTER
755
+ ).lower() # grpc or http
756
+
757
+ OTEL_LOGS_OTLP_SPAN_EXPORTER = os.environ.get(
758
+ "OTEL_LOGS_OTLP_SPAN_EXPORTER", OTEL_OTLP_SPAN_EXPORTER
759
+ ).lower() # grpc or http
760
+
761
+ ####################################
762
+ # TOOLS/FUNCTIONS PIP OPTIONS
763
+ ####################################
764
+
765
+ PIP_OPTIONS = os.getenv("PIP_OPTIONS", "").split()
766
+ PIP_PACKAGE_INDEX_OPTIONS = os.getenv("PIP_PACKAGE_INDEX_OPTIONS", "").split()
767
+
768
+
769
+ ####################################
770
+ # PROGRESSIVE WEB APP OPTIONS
771
+ ####################################
772
+
773
+ EXTERNAL_PWA_MANIFEST_URL = os.environ.get("EXTERNAL_PWA_MANIFEST_URL")
backend/open_webui/functions.py ADDED
@@ -0,0 +1,319 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import sys
3
+ import inspect
4
+ import json
5
+ import asyncio
6
+
7
+ from pydantic import BaseModel
8
+ from typing import AsyncGenerator, Generator, Iterator
9
+ from fastapi import (
10
+ Depends,
11
+ FastAPI,
12
+ File,
13
+ Form,
14
+ HTTPException,
15
+ Request,
16
+ UploadFile,
17
+ status,
18
+ )
19
+ from starlette.responses import Response, StreamingResponse
20
+
21
+
22
+ from open_webui.socket.main import (
23
+ get_event_call,
24
+ get_event_emitter,
25
+ )
26
+
27
+
28
+ from open_webui.models.users import UserModel
29
+ from open_webui.models.functions import Functions
30
+ from open_webui.models.models import Models
31
+
32
+ from open_webui.utils.plugin import (
33
+ load_function_module_by_id,
34
+ get_function_module_from_cache,
35
+ )
36
+ from open_webui.utils.tools import get_tools
37
+ from open_webui.utils.access_control import has_access
38
+
39
+ from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL
40
+
41
+ from open_webui.utils.misc import (
42
+ add_or_update_system_message,
43
+ get_last_user_message,
44
+ prepend_to_first_user_message_content,
45
+ openai_chat_chunk_message_template,
46
+ openai_chat_completion_message_template,
47
+ )
48
+ from open_webui.utils.payload import (
49
+ apply_model_params_to_body_openai,
50
+ apply_system_prompt_to_body,
51
+ )
52
+
53
+
54
+ logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
55
+ log = logging.getLogger(__name__)
56
+ log.setLevel(SRC_LOG_LEVELS["MAIN"])
57
+
58
+
59
+ def get_function_module_by_id(request: Request, pipe_id: str):
60
+ function_module, _, _ = get_function_module_from_cache(request, pipe_id)
61
+
62
+ if hasattr(function_module, "valves") and hasattr(function_module, "Valves"):
63
+ valves = Functions.get_function_valves_by_id(pipe_id)
64
+ function_module.valves = function_module.Valves(**(valves if valves else {}))
65
+ return function_module
66
+
67
+
68
+ async def get_function_models(request):
69
+ pipes = Functions.get_functions_by_type("pipe", active_only=True)
70
+ pipe_models = []
71
+
72
+ for pipe in pipes:
73
+ function_module = get_function_module_by_id(request, pipe.id)
74
+
75
+ # Check if function is a manifold
76
+ if hasattr(function_module, "pipes"):
77
+ sub_pipes = []
78
+
79
+ # Handle pipes being a list, sync function, or async function
80
+ try:
81
+ if callable(function_module.pipes):
82
+ if asyncio.iscoroutinefunction(function_module.pipes):
83
+ sub_pipes = await function_module.pipes()
84
+ else:
85
+ sub_pipes = function_module.pipes()
86
+ else:
87
+ sub_pipes = function_module.pipes
88
+ except Exception as e:
89
+ log.exception(e)
90
+ sub_pipes = []
91
+
92
+ log.debug(
93
+ f"get_function_models: function '{pipe.id}' is a manifold of {sub_pipes}"
94
+ )
95
+
96
+ for p in sub_pipes:
97
+ sub_pipe_id = f'{pipe.id}.{p["id"]}'
98
+ sub_pipe_name = p["name"]
99
+
100
+ if hasattr(function_module, "name"):
101
+ sub_pipe_name = f"{function_module.name}{sub_pipe_name}"
102
+
103
+ pipe_flag = {"type": pipe.type}
104
+
105
+ pipe_models.append(
106
+ {
107
+ "id": sub_pipe_id,
108
+ "name": sub_pipe_name,
109
+ "object": "model",
110
+ "created": pipe.created_at,
111
+ "owned_by": "openai",
112
+ "pipe": pipe_flag,
113
+ }
114
+ )
115
+ else:
116
+ pipe_flag = {"type": "pipe"}
117
+
118
+ log.debug(
119
+ f"get_function_models: function '{pipe.id}' is a single pipe {{ 'id': {pipe.id}, 'name': {pipe.name} }}"
120
+ )
121
+
122
+ pipe_models.append(
123
+ {
124
+ "id": pipe.id,
125
+ "name": pipe.name,
126
+ "object": "model",
127
+ "created": pipe.created_at,
128
+ "owned_by": "openai",
129
+ "pipe": pipe_flag,
130
+ }
131
+ )
132
+
133
+ return pipe_models
134
+
135
+
136
+ async def generate_function_chat_completion(
137
+ request, form_data, user, models: dict = {}
138
+ ):
139
+ async def execute_pipe(pipe, params):
140
+ if inspect.iscoroutinefunction(pipe):
141
+ return await pipe(**params)
142
+ else:
143
+ return pipe(**params)
144
+
145
+ async def get_message_content(res: str | Generator | AsyncGenerator) -> str:
146
+ if isinstance(res, str):
147
+ return res
148
+ if isinstance(res, Generator):
149
+ return "".join(map(str, res))
150
+ if isinstance(res, AsyncGenerator):
151
+ return "".join([str(stream) async for stream in res])
152
+
153
+ def process_line(form_data: dict, line):
154
+ if isinstance(line, BaseModel):
155
+ line = line.model_dump_json()
156
+ line = f"data: {line}"
157
+ if isinstance(line, dict):
158
+ line = f"data: {json.dumps(line)}"
159
+
160
+ try:
161
+ line = line.decode("utf-8")
162
+ except Exception:
163
+ pass
164
+
165
+ if line.startswith("data:"):
166
+ return f"{line}\n\n"
167
+ else:
168
+ line = openai_chat_chunk_message_template(form_data["model"], line)
169
+ return f"data: {json.dumps(line)}\n\n"
170
+
171
+ def get_pipe_id(form_data: dict) -> str:
172
+ pipe_id = form_data["model"]
173
+ if "." in pipe_id:
174
+ pipe_id, _ = pipe_id.split(".", 1)
175
+ return pipe_id
176
+
177
+ def get_function_params(function_module, form_data, user, extra_params=None):
178
+ if extra_params is None:
179
+ extra_params = {}
180
+
181
+ pipe_id = get_pipe_id(form_data)
182
+
183
+ # Get the signature of the function
184
+ sig = inspect.signature(function_module.pipe)
185
+ params = {"body": form_data} | {
186
+ k: v for k, v in extra_params.items() if k in sig.parameters
187
+ }
188
+
189
+ if "__user__" in params and hasattr(function_module, "UserValves"):
190
+ user_valves = Functions.get_user_valves_by_id_and_user_id(pipe_id, user.id)
191
+ try:
192
+ params["__user__"]["valves"] = function_module.UserValves(**user_valves)
193
+ except Exception as e:
194
+ log.exception(e)
195
+ params["__user__"]["valves"] = function_module.UserValves()
196
+
197
+ return params
198
+
199
+ model_id = form_data.get("model")
200
+ model_info = Models.get_model_by_id(model_id)
201
+
202
+ metadata = form_data.pop("metadata", {})
203
+
204
+ files = metadata.get("files", [])
205
+ tool_ids = metadata.get("tool_ids", [])
206
+ # Check if tool_ids is None
207
+ if tool_ids is None:
208
+ tool_ids = []
209
+
210
+ __event_emitter__ = None
211
+ __event_call__ = None
212
+ __task__ = None
213
+ __task_body__ = None
214
+
215
+ if metadata:
216
+ if all(k in metadata for k in ("session_id", "chat_id", "message_id")):
217
+ __event_emitter__ = get_event_emitter(metadata)
218
+ __event_call__ = get_event_call(metadata)
219
+ __task__ = metadata.get("task", None)
220
+ __task_body__ = metadata.get("task_body", None)
221
+
222
+ extra_params = {
223
+ "__event_emitter__": __event_emitter__,
224
+ "__event_call__": __event_call__,
225
+ "__chat_id__": metadata.get("chat_id", None),
226
+ "__session_id__": metadata.get("session_id", None),
227
+ "__message_id__": metadata.get("message_id", None),
228
+ "__task__": __task__,
229
+ "__task_body__": __task_body__,
230
+ "__files__": files,
231
+ "__user__": user.model_dump() if isinstance(user, UserModel) else {},
232
+ "__metadata__": metadata,
233
+ "__request__": request,
234
+ }
235
+ extra_params["__tools__"] = get_tools(
236
+ request,
237
+ tool_ids,
238
+ user,
239
+ {
240
+ **extra_params,
241
+ "__model__": models.get(form_data["model"], None),
242
+ "__messages__": form_data["messages"],
243
+ "__files__": files,
244
+ },
245
+ )
246
+
247
+ if model_info:
248
+ if model_info.base_model_id:
249
+ form_data["model"] = model_info.base_model_id
250
+
251
+ params = model_info.params.model_dump()
252
+
253
+ if params:
254
+ system = params.pop("system", None)
255
+ form_data = apply_model_params_to_body_openai(params, form_data)
256
+ form_data = apply_system_prompt_to_body(system, form_data, metadata, user)
257
+
258
+ pipe_id = get_pipe_id(form_data)
259
+ function_module = get_function_module_by_id(request, pipe_id)
260
+
261
+ pipe = function_module.pipe
262
+ params = get_function_params(function_module, form_data, user, extra_params)
263
+
264
+ if form_data.get("stream", False):
265
+
266
+ async def stream_content():
267
+ try:
268
+ res = await execute_pipe(pipe, params)
269
+
270
+ # Directly return if the response is a StreamingResponse
271
+ if isinstance(res, StreamingResponse):
272
+ async for data in res.body_iterator:
273
+ yield data
274
+ return
275
+ if isinstance(res, dict):
276
+ yield f"data: {json.dumps(res)}\n\n"
277
+ return
278
+
279
+ except Exception as e:
280
+ log.error(f"Error: {e}")
281
+ yield f"data: {json.dumps({'error': {'detail':str(e)}})}\n\n"
282
+ return
283
+
284
+ if isinstance(res, str):
285
+ message = openai_chat_chunk_message_template(form_data["model"], res)
286
+ yield f"data: {json.dumps(message)}\n\n"
287
+
288
+ if isinstance(res, Iterator):
289
+ for line in res:
290
+ yield process_line(form_data, line)
291
+
292
+ if isinstance(res, AsyncGenerator):
293
+ async for line in res:
294
+ yield process_line(form_data, line)
295
+
296
+ if isinstance(res, str) or isinstance(res, Generator):
297
+ finish_message = openai_chat_chunk_message_template(
298
+ form_data["model"], ""
299
+ )
300
+ finish_message["choices"][0]["finish_reason"] = "stop"
301
+ yield f"data: {json.dumps(finish_message)}\n\n"
302
+ yield "data: [DONE]"
303
+
304
+ return StreamingResponse(stream_content(), media_type="text/event-stream")
305
+ else:
306
+ try:
307
+ res = await execute_pipe(pipe, params)
308
+
309
+ except Exception as e:
310
+ log.error(f"Error: {e}")
311
+ return {"error": {"detail": str(e)}}
312
+
313
+ if isinstance(res, StreamingResponse) or isinstance(res, dict):
314
+ return res
315
+ if isinstance(res, BaseModel):
316
+ return res.model_dump()
317
+
318
+ message = await get_message_content(res)
319
+ return openai_chat_completion_message_template(form_data["model"], message)
backend/open_webui/internal/db.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import logging
4
+ from contextlib import contextmanager
5
+ from typing import Any, Optional
6
+
7
+ from open_webui.internal.wrappers import register_connection
8
+ from open_webui.env import (
9
+ OPEN_WEBUI_DIR,
10
+ DATABASE_URL,
11
+ DATABASE_SCHEMA,
12
+ SRC_LOG_LEVELS,
13
+ DATABASE_POOL_MAX_OVERFLOW,
14
+ DATABASE_POOL_RECYCLE,
15
+ DATABASE_POOL_SIZE,
16
+ DATABASE_POOL_TIMEOUT,
17
+ DATABASE_ENABLE_SQLITE_WAL,
18
+ )
19
+ from peewee_migrate import Router
20
+ from sqlalchemy import Dialect, create_engine, MetaData, event, types
21
+ from sqlalchemy.ext.declarative import declarative_base
22
+ from sqlalchemy.orm import scoped_session, sessionmaker
23
+ from sqlalchemy.pool import QueuePool, NullPool
24
+ from sqlalchemy.sql.type_api import _T
25
+ from typing_extensions import Self
26
+
27
+ log = logging.getLogger(__name__)
28
+ log.setLevel(SRC_LOG_LEVELS["DB"])
29
+
30
+
31
+ class JSONField(types.TypeDecorator):
32
+ impl = types.Text
33
+ cache_ok = True
34
+
35
+ def process_bind_param(self, value: Optional[_T], dialect: Dialect) -> Any:
36
+ return json.dumps(value)
37
+
38
+ def process_result_value(self, value: Optional[_T], dialect: Dialect) -> Any:
39
+ if value is not None:
40
+ return json.loads(value)
41
+
42
+ def copy(self, **kw: Any) -> Self:
43
+ return JSONField(self.impl.length)
44
+
45
+ def db_value(self, value):
46
+ return json.dumps(value)
47
+
48
+ def python_value(self, value):
49
+ if value is not None:
50
+ return json.loads(value)
51
+
52
+
53
+ # Workaround to handle the peewee migration
54
+ # This is required to ensure the peewee migration is handled before the alembic migration
55
+ def handle_peewee_migration(DATABASE_URL):
56
+ # db = None
57
+ try:
58
+ # Replace the postgresql:// with postgres:// to handle the peewee migration
59
+ db = register_connection(DATABASE_URL.replace("postgresql://", "postgres://"))
60
+ migrate_dir = OPEN_WEBUI_DIR / "internal" / "migrations"
61
+ router = Router(db, logger=log, migrate_dir=migrate_dir)
62
+ router.run()
63
+ db.close()
64
+
65
+ except Exception as e:
66
+ log.error(f"Failed to initialize the database connection: {e}")
67
+ log.warning(
68
+ "Hint: If your database password contains special characters, you may need to URL-encode it."
69
+ )
70
+ raise
71
+ finally:
72
+ # Properly closing the database connection
73
+ if db and not db.is_closed():
74
+ db.close()
75
+
76
+ # Assert if db connection has been closed
77
+ assert db.is_closed(), "Database connection is still open."
78
+
79
+
80
+ handle_peewee_migration(DATABASE_URL)
81
+
82
+
83
+ SQLALCHEMY_DATABASE_URL = DATABASE_URL
84
+
85
+ # Handle SQLCipher URLs
86
+ if SQLALCHEMY_DATABASE_URL.startswith("sqlite+sqlcipher://"):
87
+ database_password = os.environ.get("DATABASE_PASSWORD")
88
+ if not database_password or database_password.strip() == "":
89
+ raise ValueError(
90
+ "DATABASE_PASSWORD is required when using sqlite+sqlcipher:// URLs"
91
+ )
92
+
93
+ # Extract database path from SQLCipher URL
94
+ db_path = SQLALCHEMY_DATABASE_URL.replace("sqlite+sqlcipher://", "")
95
+ if db_path.startswith("/"):
96
+ db_path = db_path[1:] # Remove leading slash for relative paths
97
+
98
+ # Create a custom creator function that uses sqlcipher3
99
+ def create_sqlcipher_connection():
100
+ import sqlcipher3
101
+
102
+ conn = sqlcipher3.connect(db_path, check_same_thread=False)
103
+ conn.execute(f"PRAGMA key = '{database_password}'")
104
+ return conn
105
+
106
+ engine = create_engine(
107
+ "sqlite://", # Dummy URL since we're using creator
108
+ creator=create_sqlcipher_connection,
109
+ echo=False,
110
+ )
111
+
112
+ log.info("Connected to encrypted SQLite database using SQLCipher")
113
+
114
+ elif "sqlite" in SQLALCHEMY_DATABASE_URL:
115
+ engine = create_engine(
116
+ SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
117
+ )
118
+
119
+ def on_connect(dbapi_connection, connection_record):
120
+ cursor = dbapi_connection.cursor()
121
+ if DATABASE_ENABLE_SQLITE_WAL:
122
+ cursor.execute("PRAGMA journal_mode=WAL")
123
+ else:
124
+ cursor.execute("PRAGMA journal_mode=DELETE")
125
+ cursor.close()
126
+
127
+ event.listen(engine, "connect", on_connect)
128
+ else:
129
+ if isinstance(DATABASE_POOL_SIZE, int):
130
+ if DATABASE_POOL_SIZE > 0:
131
+ engine = create_engine(
132
+ SQLALCHEMY_DATABASE_URL,
133
+ pool_size=DATABASE_POOL_SIZE,
134
+ max_overflow=DATABASE_POOL_MAX_OVERFLOW,
135
+ pool_timeout=DATABASE_POOL_TIMEOUT,
136
+ pool_recycle=DATABASE_POOL_RECYCLE,
137
+ pool_pre_ping=True,
138
+ poolclass=QueuePool,
139
+ )
140
+ else:
141
+ engine = create_engine(
142
+ SQLALCHEMY_DATABASE_URL, pool_pre_ping=True, poolclass=NullPool
143
+ )
144
+ else:
145
+ engine = create_engine(SQLALCHEMY_DATABASE_URL, pool_pre_ping=True)
146
+
147
+
148
+ SessionLocal = sessionmaker(
149
+ autocommit=False, autoflush=False, bind=engine, expire_on_commit=False
150
+ )
151
+ metadata_obj = MetaData(schema=DATABASE_SCHEMA)
152
+ Base = declarative_base(metadata=metadata_obj)
153
+ Session = scoped_session(SessionLocal)
154
+
155
+
156
+ def get_session():
157
+ db = SessionLocal()
158
+ try:
159
+ yield db
160
+ finally:
161
+ db.close()
162
+
163
+
164
+ get_db = contextmanager(get_session)
backend/open_webui/internal/migrations/001_initial_schema.py ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peewee migrations -- 001_initial_schema.py.
2
+
3
+ Some examples (model - class or model name)::
4
+
5
+ > Model = migrator.orm['table_name'] # Return model in current state by name
6
+ > Model = migrator.ModelClass # Return model in current state by name
7
+
8
+ > migrator.sql(sql) # Run custom SQL
9
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
10
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
11
+ > migrator.remove_model(model, cascade=True) # Remove a model
12
+ > migrator.add_fields(model, **fields) # Add fields to a model
13
+ > migrator.change_fields(model, **fields) # Change fields
14
+ > migrator.remove_fields(model, *field_names, cascade=True)
15
+ > migrator.rename_field(model, old_field_name, new_field_name)
16
+ > migrator.rename_table(model, new_table_name)
17
+ > migrator.add_index(model, *col_names, unique=False)
18
+ > migrator.add_not_null(model, *field_names)
19
+ > migrator.add_default(model, field_name, default)
20
+ > migrator.add_constraint(model, name, sql)
21
+ > migrator.drop_index(model, *col_names)
22
+ > migrator.drop_not_null(model, *field_names)
23
+ > migrator.drop_constraints(model, *constraints)
24
+
25
+ """
26
+
27
+ from contextlib import suppress
28
+
29
+ import peewee as pw
30
+ from peewee_migrate import Migrator
31
+
32
+
33
+ with suppress(ImportError):
34
+ import playhouse.postgres_ext as pw_pext
35
+
36
+
37
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
38
+ """Write your migrations here."""
39
+
40
+ # We perform different migrations for SQLite and other databases
41
+ # This is because SQLite is very loose with enforcing its schema, and trying to migrate other databases like SQLite
42
+ # will require per-database SQL queries.
43
+ # Instead, we assume that because external DB support was added at a later date, it is safe to assume a newer base
44
+ # schema instead of trying to migrate from an older schema.
45
+ if isinstance(database, pw.SqliteDatabase):
46
+ migrate_sqlite(migrator, database, fake=fake)
47
+ else:
48
+ migrate_external(migrator, database, fake=fake)
49
+
50
+
51
+ def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
52
+ @migrator.create_model
53
+ class Auth(pw.Model):
54
+ id = pw.CharField(max_length=255, unique=True)
55
+ email = pw.CharField(max_length=255)
56
+ password = pw.CharField(max_length=255)
57
+ active = pw.BooleanField()
58
+
59
+ class Meta:
60
+ table_name = "auth"
61
+
62
+ @migrator.create_model
63
+ class Chat(pw.Model):
64
+ id = pw.CharField(max_length=255, unique=True)
65
+ user_id = pw.CharField(max_length=255)
66
+ title = pw.CharField()
67
+ chat = pw.TextField()
68
+ timestamp = pw.BigIntegerField()
69
+
70
+ class Meta:
71
+ table_name = "chat"
72
+
73
+ @migrator.create_model
74
+ class ChatIdTag(pw.Model):
75
+ id = pw.CharField(max_length=255, unique=True)
76
+ tag_name = pw.CharField(max_length=255)
77
+ chat_id = pw.CharField(max_length=255)
78
+ user_id = pw.CharField(max_length=255)
79
+ timestamp = pw.BigIntegerField()
80
+
81
+ class Meta:
82
+ table_name = "chatidtag"
83
+
84
+ @migrator.create_model
85
+ class Document(pw.Model):
86
+ id = pw.AutoField()
87
+ collection_name = pw.CharField(max_length=255, unique=True)
88
+ name = pw.CharField(max_length=255, unique=True)
89
+ title = pw.CharField()
90
+ filename = pw.CharField()
91
+ content = pw.TextField(null=True)
92
+ user_id = pw.CharField(max_length=255)
93
+ timestamp = pw.BigIntegerField()
94
+
95
+ class Meta:
96
+ table_name = "document"
97
+
98
+ @migrator.create_model
99
+ class Modelfile(pw.Model):
100
+ id = pw.AutoField()
101
+ tag_name = pw.CharField(max_length=255, unique=True)
102
+ user_id = pw.CharField(max_length=255)
103
+ modelfile = pw.TextField()
104
+ timestamp = pw.BigIntegerField()
105
+
106
+ class Meta:
107
+ table_name = "modelfile"
108
+
109
+ @migrator.create_model
110
+ class Prompt(pw.Model):
111
+ id = pw.AutoField()
112
+ command = pw.CharField(max_length=255, unique=True)
113
+ user_id = pw.CharField(max_length=255)
114
+ title = pw.CharField()
115
+ content = pw.TextField()
116
+ timestamp = pw.BigIntegerField()
117
+
118
+ class Meta:
119
+ table_name = "prompt"
120
+
121
+ @migrator.create_model
122
+ class Tag(pw.Model):
123
+ id = pw.CharField(max_length=255, unique=True)
124
+ name = pw.CharField(max_length=255)
125
+ user_id = pw.CharField(max_length=255)
126
+ data = pw.TextField(null=True)
127
+
128
+ class Meta:
129
+ table_name = "tag"
130
+
131
+ @migrator.create_model
132
+ class User(pw.Model):
133
+ id = pw.CharField(max_length=255, unique=True)
134
+ name = pw.CharField(max_length=255)
135
+ email = pw.CharField(max_length=255)
136
+ role = pw.CharField(max_length=255)
137
+ profile_image_url = pw.CharField(max_length=255)
138
+ timestamp = pw.BigIntegerField()
139
+
140
+ class Meta:
141
+ table_name = "user"
142
+
143
+
144
+ def migrate_external(migrator: Migrator, database: pw.Database, *, fake=False):
145
+ @migrator.create_model
146
+ class Auth(pw.Model):
147
+ id = pw.CharField(max_length=255, unique=True)
148
+ email = pw.CharField(max_length=255)
149
+ password = pw.TextField()
150
+ active = pw.BooleanField()
151
+
152
+ class Meta:
153
+ table_name = "auth"
154
+
155
+ @migrator.create_model
156
+ class Chat(pw.Model):
157
+ id = pw.CharField(max_length=255, unique=True)
158
+ user_id = pw.CharField(max_length=255)
159
+ title = pw.TextField()
160
+ chat = pw.TextField()
161
+ timestamp = pw.BigIntegerField()
162
+
163
+ class Meta:
164
+ table_name = "chat"
165
+
166
+ @migrator.create_model
167
+ class ChatIdTag(pw.Model):
168
+ id = pw.CharField(max_length=255, unique=True)
169
+ tag_name = pw.CharField(max_length=255)
170
+ chat_id = pw.CharField(max_length=255)
171
+ user_id = pw.CharField(max_length=255)
172
+ timestamp = pw.BigIntegerField()
173
+
174
+ class Meta:
175
+ table_name = "chatidtag"
176
+
177
+ @migrator.create_model
178
+ class Document(pw.Model):
179
+ id = pw.AutoField()
180
+ collection_name = pw.CharField(max_length=255, unique=True)
181
+ name = pw.CharField(max_length=255, unique=True)
182
+ title = pw.TextField()
183
+ filename = pw.TextField()
184
+ content = pw.TextField(null=True)
185
+ user_id = pw.CharField(max_length=255)
186
+ timestamp = pw.BigIntegerField()
187
+
188
+ class Meta:
189
+ table_name = "document"
190
+
191
+ @migrator.create_model
192
+ class Modelfile(pw.Model):
193
+ id = pw.AutoField()
194
+ tag_name = pw.CharField(max_length=255, unique=True)
195
+ user_id = pw.CharField(max_length=255)
196
+ modelfile = pw.TextField()
197
+ timestamp = pw.BigIntegerField()
198
+
199
+ class Meta:
200
+ table_name = "modelfile"
201
+
202
+ @migrator.create_model
203
+ class Prompt(pw.Model):
204
+ id = pw.AutoField()
205
+ command = pw.CharField(max_length=255, unique=True)
206
+ user_id = pw.CharField(max_length=255)
207
+ title = pw.TextField()
208
+ content = pw.TextField()
209
+ timestamp = pw.BigIntegerField()
210
+
211
+ class Meta:
212
+ table_name = "prompt"
213
+
214
+ @migrator.create_model
215
+ class Tag(pw.Model):
216
+ id = pw.CharField(max_length=255, unique=True)
217
+ name = pw.CharField(max_length=255)
218
+ user_id = pw.CharField(max_length=255)
219
+ data = pw.TextField(null=True)
220
+
221
+ class Meta:
222
+ table_name = "tag"
223
+
224
+ @migrator.create_model
225
+ class User(pw.Model):
226
+ id = pw.CharField(max_length=255, unique=True)
227
+ name = pw.CharField(max_length=255)
228
+ email = pw.CharField(max_length=255)
229
+ role = pw.CharField(max_length=255)
230
+ profile_image_url = pw.TextField()
231
+ timestamp = pw.BigIntegerField()
232
+
233
+ class Meta:
234
+ table_name = "user"
235
+
236
+
237
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
238
+ """Write your rollback migrations here."""
239
+
240
+ migrator.remove_model("user")
241
+
242
+ migrator.remove_model("tag")
243
+
244
+ migrator.remove_model("prompt")
245
+
246
+ migrator.remove_model("modelfile")
247
+
248
+ migrator.remove_model("document")
249
+
250
+ migrator.remove_model("chatidtag")
251
+
252
+ migrator.remove_model("chat")
253
+
254
+ migrator.remove_model("auth")
backend/open_webui/internal/migrations/002_add_local_sharing.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peewee migrations -- 002_add_local_sharing.py.
2
+
3
+ Some examples (model - class or model name)::
4
+
5
+ > Model = migrator.orm['table_name'] # Return model in current state by name
6
+ > Model = migrator.ModelClass # Return model in current state by name
7
+
8
+ > migrator.sql(sql) # Run custom SQL
9
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
10
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
11
+ > migrator.remove_model(model, cascade=True) # Remove a model
12
+ > migrator.add_fields(model, **fields) # Add fields to a model
13
+ > migrator.change_fields(model, **fields) # Change fields
14
+ > migrator.remove_fields(model, *field_names, cascade=True)
15
+ > migrator.rename_field(model, old_field_name, new_field_name)
16
+ > migrator.rename_table(model, new_table_name)
17
+ > migrator.add_index(model, *col_names, unique=False)
18
+ > migrator.add_not_null(model, *field_names)
19
+ > migrator.add_default(model, field_name, default)
20
+ > migrator.add_constraint(model, name, sql)
21
+ > migrator.drop_index(model, *col_names)
22
+ > migrator.drop_not_null(model, *field_names)
23
+ > migrator.drop_constraints(model, *constraints)
24
+
25
+ """
26
+
27
+ from contextlib import suppress
28
+
29
+ import peewee as pw
30
+ from peewee_migrate import Migrator
31
+
32
+
33
+ with suppress(ImportError):
34
+ import playhouse.postgres_ext as pw_pext
35
+
36
+
37
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
38
+ """Write your migrations here."""
39
+
40
+ migrator.add_fields(
41
+ "chat", share_id=pw.CharField(max_length=255, null=True, unique=True)
42
+ )
43
+
44
+
45
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
46
+ """Write your rollback migrations here."""
47
+
48
+ migrator.remove_fields("chat", "share_id")
backend/open_webui/internal/migrations/003_add_auth_api_key.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peewee migrations -- 002_add_local_sharing.py.
2
+
3
+ Some examples (model - class or model name)::
4
+
5
+ > Model = migrator.orm['table_name'] # Return model in current state by name
6
+ > Model = migrator.ModelClass # Return model in current state by name
7
+
8
+ > migrator.sql(sql) # Run custom SQL
9
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
10
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
11
+ > migrator.remove_model(model, cascade=True) # Remove a model
12
+ > migrator.add_fields(model, **fields) # Add fields to a model
13
+ > migrator.change_fields(model, **fields) # Change fields
14
+ > migrator.remove_fields(model, *field_names, cascade=True)
15
+ > migrator.rename_field(model, old_field_name, new_field_name)
16
+ > migrator.rename_table(model, new_table_name)
17
+ > migrator.add_index(model, *col_names, unique=False)
18
+ > migrator.add_not_null(model, *field_names)
19
+ > migrator.add_default(model, field_name, default)
20
+ > migrator.add_constraint(model, name, sql)
21
+ > migrator.drop_index(model, *col_names)
22
+ > migrator.drop_not_null(model, *field_names)
23
+ > migrator.drop_constraints(model, *constraints)
24
+
25
+ """
26
+
27
+ from contextlib import suppress
28
+
29
+ import peewee as pw
30
+ from peewee_migrate import Migrator
31
+
32
+
33
+ with suppress(ImportError):
34
+ import playhouse.postgres_ext as pw_pext
35
+
36
+
37
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
38
+ """Write your migrations here."""
39
+
40
+ migrator.add_fields(
41
+ "user", api_key=pw.CharField(max_length=255, null=True, unique=True)
42
+ )
43
+
44
+
45
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
46
+ """Write your rollback migrations here."""
47
+
48
+ migrator.remove_fields("user", "api_key")
backend/open_webui/internal/migrations/004_add_archived.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peewee migrations -- 002_add_local_sharing.py.
2
+
3
+ Some examples (model - class or model name)::
4
+
5
+ > Model = migrator.orm['table_name'] # Return model in current state by name
6
+ > Model = migrator.ModelClass # Return model in current state by name
7
+
8
+ > migrator.sql(sql) # Run custom SQL
9
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
10
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
11
+ > migrator.remove_model(model, cascade=True) # Remove a model
12
+ > migrator.add_fields(model, **fields) # Add fields to a model
13
+ > migrator.change_fields(model, **fields) # Change fields
14
+ > migrator.remove_fields(model, *field_names, cascade=True)
15
+ > migrator.rename_field(model, old_field_name, new_field_name)
16
+ > migrator.rename_table(model, new_table_name)
17
+ > migrator.add_index(model, *col_names, unique=False)
18
+ > migrator.add_not_null(model, *field_names)
19
+ > migrator.add_default(model, field_name, default)
20
+ > migrator.add_constraint(model, name, sql)
21
+ > migrator.drop_index(model, *col_names)
22
+ > migrator.drop_not_null(model, *field_names)
23
+ > migrator.drop_constraints(model, *constraints)
24
+
25
+ """
26
+
27
+ from contextlib import suppress
28
+
29
+ import peewee as pw
30
+ from peewee_migrate import Migrator
31
+
32
+
33
+ with suppress(ImportError):
34
+ import playhouse.postgres_ext as pw_pext
35
+
36
+
37
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
38
+ """Write your migrations here."""
39
+
40
+ migrator.add_fields("chat", archived=pw.BooleanField(default=False))
41
+
42
+
43
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
44
+ """Write your rollback migrations here."""
45
+
46
+ migrator.remove_fields("chat", "archived")
backend/open_webui/internal/migrations/005_add_updated_at.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peewee migrations -- 002_add_local_sharing.py.
2
+
3
+ Some examples (model - class or model name)::
4
+
5
+ > Model = migrator.orm['table_name'] # Return model in current state by name
6
+ > Model = migrator.ModelClass # Return model in current state by name
7
+
8
+ > migrator.sql(sql) # Run custom SQL
9
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
10
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
11
+ > migrator.remove_model(model, cascade=True) # Remove a model
12
+ > migrator.add_fields(model, **fields) # Add fields to a model
13
+ > migrator.change_fields(model, **fields) # Change fields
14
+ > migrator.remove_fields(model, *field_names, cascade=True)
15
+ > migrator.rename_field(model, old_field_name, new_field_name)
16
+ > migrator.rename_table(model, new_table_name)
17
+ > migrator.add_index(model, *col_names, unique=False)
18
+ > migrator.add_not_null(model, *field_names)
19
+ > migrator.add_default(model, field_name, default)
20
+ > migrator.add_constraint(model, name, sql)
21
+ > migrator.drop_index(model, *col_names)
22
+ > migrator.drop_not_null(model, *field_names)
23
+ > migrator.drop_constraints(model, *constraints)
24
+
25
+ """
26
+
27
+ from contextlib import suppress
28
+
29
+ import peewee as pw
30
+ from peewee_migrate import Migrator
31
+
32
+
33
+ with suppress(ImportError):
34
+ import playhouse.postgres_ext as pw_pext
35
+
36
+
37
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
38
+ """Write your migrations here."""
39
+
40
+ if isinstance(database, pw.SqliteDatabase):
41
+ migrate_sqlite(migrator, database, fake=fake)
42
+ else:
43
+ migrate_external(migrator, database, fake=fake)
44
+
45
+
46
+ def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
47
+ # Adding fields created_at and updated_at to the 'chat' table
48
+ migrator.add_fields(
49
+ "chat",
50
+ created_at=pw.DateTimeField(null=True), # Allow null for transition
51
+ updated_at=pw.DateTimeField(null=True), # Allow null for transition
52
+ )
53
+
54
+ # Populate the new fields from an existing 'timestamp' field
55
+ migrator.sql(
56
+ "UPDATE chat SET created_at = timestamp, updated_at = timestamp WHERE timestamp IS NOT NULL"
57
+ )
58
+
59
+ # Now that the data has been copied, remove the original 'timestamp' field
60
+ migrator.remove_fields("chat", "timestamp")
61
+
62
+ # Update the fields to be not null now that they are populated
63
+ migrator.change_fields(
64
+ "chat",
65
+ created_at=pw.DateTimeField(null=False),
66
+ updated_at=pw.DateTimeField(null=False),
67
+ )
68
+
69
+
70
+ def migrate_external(migrator: Migrator, database: pw.Database, *, fake=False):
71
+ # Adding fields created_at and updated_at to the 'chat' table
72
+ migrator.add_fields(
73
+ "chat",
74
+ created_at=pw.BigIntegerField(null=True), # Allow null for transition
75
+ updated_at=pw.BigIntegerField(null=True), # Allow null for transition
76
+ )
77
+
78
+ # Populate the new fields from an existing 'timestamp' field
79
+ migrator.sql(
80
+ "UPDATE chat SET created_at = timestamp, updated_at = timestamp WHERE timestamp IS NOT NULL"
81
+ )
82
+
83
+ # Now that the data has been copied, remove the original 'timestamp' field
84
+ migrator.remove_fields("chat", "timestamp")
85
+
86
+ # Update the fields to be not null now that they are populated
87
+ migrator.change_fields(
88
+ "chat",
89
+ created_at=pw.BigIntegerField(null=False),
90
+ updated_at=pw.BigIntegerField(null=False),
91
+ )
92
+
93
+
94
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
95
+ """Write your rollback migrations here."""
96
+
97
+ if isinstance(database, pw.SqliteDatabase):
98
+ rollback_sqlite(migrator, database, fake=fake)
99
+ else:
100
+ rollback_external(migrator, database, fake=fake)
101
+
102
+
103
+ def rollback_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
104
+ # Recreate the timestamp field initially allowing null values for safe transition
105
+ migrator.add_fields("chat", timestamp=pw.DateTimeField(null=True))
106
+
107
+ # Copy the earliest created_at date back into the new timestamp field
108
+ # This assumes created_at was originally a copy of timestamp
109
+ migrator.sql("UPDATE chat SET timestamp = created_at")
110
+
111
+ # Remove the created_at and updated_at fields
112
+ migrator.remove_fields("chat", "created_at", "updated_at")
113
+
114
+ # Finally, alter the timestamp field to not allow nulls if that was the original setting
115
+ migrator.change_fields("chat", timestamp=pw.DateTimeField(null=False))
116
+
117
+
118
+ def rollback_external(migrator: Migrator, database: pw.Database, *, fake=False):
119
+ # Recreate the timestamp field initially allowing null values for safe transition
120
+ migrator.add_fields("chat", timestamp=pw.BigIntegerField(null=True))
121
+
122
+ # Copy the earliest created_at date back into the new timestamp field
123
+ # This assumes created_at was originally a copy of timestamp
124
+ migrator.sql("UPDATE chat SET timestamp = created_at")
125
+
126
+ # Remove the created_at and updated_at fields
127
+ migrator.remove_fields("chat", "created_at", "updated_at")
128
+
129
+ # Finally, alter the timestamp field to not allow nulls if that was the original setting
130
+ migrator.change_fields("chat", timestamp=pw.BigIntegerField(null=False))
backend/open_webui/internal/migrations/006_migrate_timestamps_and_charfields.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peewee migrations -- 006_migrate_timestamps_and_charfields.py.
2
+
3
+ Some examples (model - class or model name)::
4
+
5
+ > Model = migrator.orm['table_name'] # Return model in current state by name
6
+ > Model = migrator.ModelClass # Return model in current state by name
7
+
8
+ > migrator.sql(sql) # Run custom SQL
9
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
10
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
11
+ > migrator.remove_model(model, cascade=True) # Remove a model
12
+ > migrator.add_fields(model, **fields) # Add fields to a model
13
+ > migrator.change_fields(model, **fields) # Change fields
14
+ > migrator.remove_fields(model, *field_names, cascade=True)
15
+ > migrator.rename_field(model, old_field_name, new_field_name)
16
+ > migrator.rename_table(model, new_table_name)
17
+ > migrator.add_index(model, *col_names, unique=False)
18
+ > migrator.add_not_null(model, *field_names)
19
+ > migrator.add_default(model, field_name, default)
20
+ > migrator.add_constraint(model, name, sql)
21
+ > migrator.drop_index(model, *col_names)
22
+ > migrator.drop_not_null(model, *field_names)
23
+ > migrator.drop_constraints(model, *constraints)
24
+
25
+ """
26
+
27
+ from contextlib import suppress
28
+
29
+ import peewee as pw
30
+ from peewee_migrate import Migrator
31
+
32
+
33
+ with suppress(ImportError):
34
+ import playhouse.postgres_ext as pw_pext
35
+
36
+
37
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
38
+ """Write your migrations here."""
39
+
40
+ # Alter the tables with timestamps
41
+ migrator.change_fields(
42
+ "chatidtag",
43
+ timestamp=pw.BigIntegerField(),
44
+ )
45
+ migrator.change_fields(
46
+ "document",
47
+ timestamp=pw.BigIntegerField(),
48
+ )
49
+ migrator.change_fields(
50
+ "modelfile",
51
+ timestamp=pw.BigIntegerField(),
52
+ )
53
+ migrator.change_fields(
54
+ "prompt",
55
+ timestamp=pw.BigIntegerField(),
56
+ )
57
+ migrator.change_fields(
58
+ "user",
59
+ timestamp=pw.BigIntegerField(),
60
+ )
61
+ # Alter the tables with varchar to text where necessary
62
+ migrator.change_fields(
63
+ "auth",
64
+ password=pw.TextField(),
65
+ )
66
+ migrator.change_fields(
67
+ "chat",
68
+ title=pw.TextField(),
69
+ )
70
+ migrator.change_fields(
71
+ "document",
72
+ title=pw.TextField(),
73
+ filename=pw.TextField(),
74
+ )
75
+ migrator.change_fields(
76
+ "prompt",
77
+ title=pw.TextField(),
78
+ )
79
+ migrator.change_fields(
80
+ "user",
81
+ profile_image_url=pw.TextField(),
82
+ )
83
+
84
+
85
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
86
+ """Write your rollback migrations here."""
87
+
88
+ if isinstance(database, pw.SqliteDatabase):
89
+ # Alter the tables with timestamps
90
+ migrator.change_fields(
91
+ "chatidtag",
92
+ timestamp=pw.DateField(),
93
+ )
94
+ migrator.change_fields(
95
+ "document",
96
+ timestamp=pw.DateField(),
97
+ )
98
+ migrator.change_fields(
99
+ "modelfile",
100
+ timestamp=pw.DateField(),
101
+ )
102
+ migrator.change_fields(
103
+ "prompt",
104
+ timestamp=pw.DateField(),
105
+ )
106
+ migrator.change_fields(
107
+ "user",
108
+ timestamp=pw.DateField(),
109
+ )
110
+ migrator.change_fields(
111
+ "auth",
112
+ password=pw.CharField(max_length=255),
113
+ )
114
+ migrator.change_fields(
115
+ "chat",
116
+ title=pw.CharField(),
117
+ )
118
+ migrator.change_fields(
119
+ "document",
120
+ title=pw.CharField(),
121
+ filename=pw.CharField(),
122
+ )
123
+ migrator.change_fields(
124
+ "prompt",
125
+ title=pw.CharField(),
126
+ )
127
+ migrator.change_fields(
128
+ "user",
129
+ profile_image_url=pw.CharField(),
130
+ )
backend/open_webui/internal/migrations/007_add_user_last_active_at.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peewee migrations -- 002_add_local_sharing.py.
2
+
3
+ Some examples (model - class or model name)::
4
+
5
+ > Model = migrator.orm['table_name'] # Return model in current state by name
6
+ > Model = migrator.ModelClass # Return model in current state by name
7
+
8
+ > migrator.sql(sql) # Run custom SQL
9
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
10
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
11
+ > migrator.remove_model(model, cascade=True) # Remove a model
12
+ > migrator.add_fields(model, **fields) # Add fields to a model
13
+ > migrator.change_fields(model, **fields) # Change fields
14
+ > migrator.remove_fields(model, *field_names, cascade=True)
15
+ > migrator.rename_field(model, old_field_name, new_field_name)
16
+ > migrator.rename_table(model, new_table_name)
17
+ > migrator.add_index(model, *col_names, unique=False)
18
+ > migrator.add_not_null(model, *field_names)
19
+ > migrator.add_default(model, field_name, default)
20
+ > migrator.add_constraint(model, name, sql)
21
+ > migrator.drop_index(model, *col_names)
22
+ > migrator.drop_not_null(model, *field_names)
23
+ > migrator.drop_constraints(model, *constraints)
24
+
25
+ """
26
+
27
+ from contextlib import suppress
28
+
29
+ import peewee as pw
30
+ from peewee_migrate import Migrator
31
+
32
+
33
+ with suppress(ImportError):
34
+ import playhouse.postgres_ext as pw_pext
35
+
36
+
37
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
38
+ """Write your migrations here."""
39
+
40
+ # Adding fields created_at and updated_at to the 'user' table
41
+ migrator.add_fields(
42
+ "user",
43
+ created_at=pw.BigIntegerField(null=True), # Allow null for transition
44
+ updated_at=pw.BigIntegerField(null=True), # Allow null for transition
45
+ last_active_at=pw.BigIntegerField(null=True), # Allow null for transition
46
+ )
47
+
48
+ # Populate the new fields from an existing 'timestamp' field
49
+ migrator.sql(
50
+ 'UPDATE "user" SET created_at = timestamp, updated_at = timestamp, last_active_at = timestamp WHERE timestamp IS NOT NULL'
51
+ )
52
+
53
+ # Now that the data has been copied, remove the original 'timestamp' field
54
+ migrator.remove_fields("user", "timestamp")
55
+
56
+ # Update the fields to be not null now that they are populated
57
+ migrator.change_fields(
58
+ "user",
59
+ created_at=pw.BigIntegerField(null=False),
60
+ updated_at=pw.BigIntegerField(null=False),
61
+ last_active_at=pw.BigIntegerField(null=False),
62
+ )
63
+
64
+
65
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
66
+ """Write your rollback migrations here."""
67
+
68
+ # Recreate the timestamp field initially allowing null values for safe transition
69
+ migrator.add_fields("user", timestamp=pw.BigIntegerField(null=True))
70
+
71
+ # Copy the earliest created_at date back into the new timestamp field
72
+ # This assumes created_at was originally a copy of timestamp
73
+ migrator.sql('UPDATE "user" SET timestamp = created_at')
74
+
75
+ # Remove the created_at and updated_at fields
76
+ migrator.remove_fields("user", "created_at", "updated_at", "last_active_at")
77
+
78
+ # Finally, alter the timestamp field to not allow nulls if that was the original setting
79
+ migrator.change_fields("user", timestamp=pw.BigIntegerField(null=False))
backend/open_webui/internal/migrations/008_add_memory.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peewee migrations -- 002_add_local_sharing.py.
2
+
3
+ Some examples (model - class or model name)::
4
+
5
+ > Model = migrator.orm['table_name'] # Return model in current state by name
6
+ > Model = migrator.ModelClass # Return model in current state by name
7
+
8
+ > migrator.sql(sql) # Run custom SQL
9
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
10
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
11
+ > migrator.remove_model(model, cascade=True) # Remove a model
12
+ > migrator.add_fields(model, **fields) # Add fields to a model
13
+ > migrator.change_fields(model, **fields) # Change fields
14
+ > migrator.remove_fields(model, *field_names, cascade=True)
15
+ > migrator.rename_field(model, old_field_name, new_field_name)
16
+ > migrator.rename_table(model, new_table_name)
17
+ > migrator.add_index(model, *col_names, unique=False)
18
+ > migrator.add_not_null(model, *field_names)
19
+ > migrator.add_default(model, field_name, default)
20
+ > migrator.add_constraint(model, name, sql)
21
+ > migrator.drop_index(model, *col_names)
22
+ > migrator.drop_not_null(model, *field_names)
23
+ > migrator.drop_constraints(model, *constraints)
24
+
25
+ """
26
+
27
+ from contextlib import suppress
28
+
29
+ import peewee as pw
30
+ from peewee_migrate import Migrator
31
+
32
+
33
+ with suppress(ImportError):
34
+ import playhouse.postgres_ext as pw_pext
35
+
36
+
37
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
38
+ @migrator.create_model
39
+ class Memory(pw.Model):
40
+ id = pw.CharField(max_length=255, unique=True)
41
+ user_id = pw.CharField(max_length=255)
42
+ content = pw.TextField(null=False)
43
+ updated_at = pw.BigIntegerField(null=False)
44
+ created_at = pw.BigIntegerField(null=False)
45
+
46
+ class Meta:
47
+ table_name = "memory"
48
+
49
+
50
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
51
+ """Write your rollback migrations here."""
52
+
53
+ migrator.remove_model("memory")
backend/open_webui/internal/migrations/009_add_models.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peewee migrations -- 009_add_models.py.
2
+
3
+ Some examples (model - class or model name)::
4
+
5
+ > Model = migrator.orm['table_name'] # Return model in current state by name
6
+ > Model = migrator.ModelClass # Return model in current state by name
7
+
8
+ > migrator.sql(sql) # Run custom SQL
9
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
10
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
11
+ > migrator.remove_model(model, cascade=True) # Remove a model
12
+ > migrator.add_fields(model, **fields) # Add fields to a model
13
+ > migrator.change_fields(model, **fields) # Change fields
14
+ > migrator.remove_fields(model, *field_names, cascade=True)
15
+ > migrator.rename_field(model, old_field_name, new_field_name)
16
+ > migrator.rename_table(model, new_table_name)
17
+ > migrator.add_index(model, *col_names, unique=False)
18
+ > migrator.add_not_null(model, *field_names)
19
+ > migrator.add_default(model, field_name, default)
20
+ > migrator.add_constraint(model, name, sql)
21
+ > migrator.drop_index(model, *col_names)
22
+ > migrator.drop_not_null(model, *field_names)
23
+ > migrator.drop_constraints(model, *constraints)
24
+
25
+ """
26
+
27
+ from contextlib import suppress
28
+
29
+ import peewee as pw
30
+ from peewee_migrate import Migrator
31
+
32
+
33
+ with suppress(ImportError):
34
+ import playhouse.postgres_ext as pw_pext
35
+
36
+
37
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
38
+ """Write your migrations here."""
39
+
40
+ @migrator.create_model
41
+ class Model(pw.Model):
42
+ id = pw.TextField(unique=True)
43
+ user_id = pw.TextField()
44
+ base_model_id = pw.TextField(null=True)
45
+
46
+ name = pw.TextField()
47
+
48
+ meta = pw.TextField()
49
+ params = pw.TextField()
50
+
51
+ created_at = pw.BigIntegerField(null=False)
52
+ updated_at = pw.BigIntegerField(null=False)
53
+
54
+ class Meta:
55
+ table_name = "model"
56
+
57
+
58
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
59
+ """Write your rollback migrations here."""
60
+
61
+ migrator.remove_model("model")
backend/open_webui/internal/migrations/010_migrate_modelfiles_to_models.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peewee migrations -- 009_add_models.py.
2
+
3
+ Some examples (model - class or model name)::
4
+
5
+ > Model = migrator.orm['table_name'] # Return model in current state by name
6
+ > Model = migrator.ModelClass # Return model in current state by name
7
+
8
+ > migrator.sql(sql) # Run custom SQL
9
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
10
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
11
+ > migrator.remove_model(model, cascade=True) # Remove a model
12
+ > migrator.add_fields(model, **fields) # Add fields to a model
13
+ > migrator.change_fields(model, **fields) # Change fields
14
+ > migrator.remove_fields(model, *field_names, cascade=True)
15
+ > migrator.rename_field(model, old_field_name, new_field_name)
16
+ > migrator.rename_table(model, new_table_name)
17
+ > migrator.add_index(model, *col_names, unique=False)
18
+ > migrator.add_not_null(model, *field_names)
19
+ > migrator.add_default(model, field_name, default)
20
+ > migrator.add_constraint(model, name, sql)
21
+ > migrator.drop_index(model, *col_names)
22
+ > migrator.drop_not_null(model, *field_names)
23
+ > migrator.drop_constraints(model, *constraints)
24
+
25
+ """
26
+
27
+ from contextlib import suppress
28
+
29
+ import peewee as pw
30
+ from peewee_migrate import Migrator
31
+ import json
32
+
33
+ from open_webui.utils.misc import parse_ollama_modelfile
34
+
35
+ with suppress(ImportError):
36
+ import playhouse.postgres_ext as pw_pext
37
+
38
+
39
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
40
+ """Write your migrations here."""
41
+
42
+ # Fetch data from 'modelfile' table and insert into 'model' table
43
+ migrate_modelfile_to_model(migrator, database)
44
+ # Drop the 'modelfile' table
45
+ migrator.remove_model("modelfile")
46
+
47
+
48
+ def migrate_modelfile_to_model(migrator: Migrator, database: pw.Database):
49
+ ModelFile = migrator.orm["modelfile"]
50
+ Model = migrator.orm["model"]
51
+
52
+ modelfiles = ModelFile.select()
53
+
54
+ for modelfile in modelfiles:
55
+ # Extract and transform data in Python
56
+
57
+ modelfile.modelfile = json.loads(modelfile.modelfile)
58
+ meta = json.dumps(
59
+ {
60
+ "description": modelfile.modelfile.get("desc"),
61
+ "profile_image_url": modelfile.modelfile.get("imageUrl"),
62
+ "ollama": {"modelfile": modelfile.modelfile.get("content")},
63
+ "suggestion_prompts": modelfile.modelfile.get("suggestionPrompts"),
64
+ "categories": modelfile.modelfile.get("categories"),
65
+ "user": {**modelfile.modelfile.get("user", {}), "community": True},
66
+ }
67
+ )
68
+
69
+ info = parse_ollama_modelfile(modelfile.modelfile.get("content"))
70
+
71
+ # Insert the processed data into the 'model' table
72
+ Model.create(
73
+ id=f"ollama-{modelfile.tag_name}",
74
+ user_id=modelfile.user_id,
75
+ base_model_id=info.get("base_model_id"),
76
+ name=modelfile.modelfile.get("title"),
77
+ meta=meta,
78
+ params=json.dumps(info.get("params", {})),
79
+ created_at=modelfile.timestamp,
80
+ updated_at=modelfile.timestamp,
81
+ )
82
+
83
+
84
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
85
+ """Write your rollback migrations here."""
86
+
87
+ recreate_modelfile_table(migrator, database)
88
+ move_data_back_to_modelfile(migrator, database)
89
+ migrator.remove_model("model")
90
+
91
+
92
+ def recreate_modelfile_table(migrator: Migrator, database: pw.Database):
93
+ query = """
94
+ CREATE TABLE IF NOT EXISTS modelfile (
95
+ user_id TEXT,
96
+ tag_name TEXT,
97
+ modelfile JSON,
98
+ timestamp BIGINT
99
+ )
100
+ """
101
+ migrator.sql(query)
102
+
103
+
104
+ def move_data_back_to_modelfile(migrator: Migrator, database: pw.Database):
105
+ Model = migrator.orm["model"]
106
+ Modelfile = migrator.orm["modelfile"]
107
+
108
+ models = Model.select()
109
+
110
+ for model in models:
111
+ # Extract and transform data in Python
112
+ meta = json.loads(model.meta)
113
+
114
+ modelfile_data = {
115
+ "title": model.name,
116
+ "desc": meta.get("description"),
117
+ "imageUrl": meta.get("profile_image_url"),
118
+ "content": meta.get("ollama", {}).get("modelfile"),
119
+ "suggestionPrompts": meta.get("suggestion_prompts"),
120
+ "categories": meta.get("categories"),
121
+ "user": {k: v for k, v in meta.get("user", {}).items() if k != "community"},
122
+ }
123
+
124
+ # Insert the processed data back into the 'modelfile' table
125
+ Modelfile.create(
126
+ user_id=model.user_id,
127
+ tag_name=model.id,
128
+ modelfile=modelfile_data,
129
+ timestamp=model.created_at,
130
+ )
backend/open_webui/internal/migrations/011_add_user_settings.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peewee migrations -- 002_add_local_sharing.py.
2
+
3
+ Some examples (model - class or model name)::
4
+
5
+ > Model = migrator.orm['table_name'] # Return model in current state by name
6
+ > Model = migrator.ModelClass # Return model in current state by name
7
+
8
+ > migrator.sql(sql) # Run custom SQL
9
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
10
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
11
+ > migrator.remove_model(model, cascade=True) # Remove a model
12
+ > migrator.add_fields(model, **fields) # Add fields to a model
13
+ > migrator.change_fields(model, **fields) # Change fields
14
+ > migrator.remove_fields(model, *field_names, cascade=True)
15
+ > migrator.rename_field(model, old_field_name, new_field_name)
16
+ > migrator.rename_table(model, new_table_name)
17
+ > migrator.add_index(model, *col_names, unique=False)
18
+ > migrator.add_not_null(model, *field_names)
19
+ > migrator.add_default(model, field_name, default)
20
+ > migrator.add_constraint(model, name, sql)
21
+ > migrator.drop_index(model, *col_names)
22
+ > migrator.drop_not_null(model, *field_names)
23
+ > migrator.drop_constraints(model, *constraints)
24
+
25
+ """
26
+
27
+ from contextlib import suppress
28
+
29
+ import peewee as pw
30
+ from peewee_migrate import Migrator
31
+
32
+
33
+ with suppress(ImportError):
34
+ import playhouse.postgres_ext as pw_pext
35
+
36
+
37
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
38
+ """Write your migrations here."""
39
+
40
+ # Adding fields settings to the 'user' table
41
+ migrator.add_fields("user", settings=pw.TextField(null=True))
42
+
43
+
44
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
45
+ """Write your rollback migrations here."""
46
+
47
+ # Remove the settings field
48
+ migrator.remove_fields("user", "settings")
backend/open_webui/internal/migrations/012_add_tools.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peewee migrations -- 009_add_models.py.
2
+
3
+ Some examples (model - class or model name)::
4
+
5
+ > Model = migrator.orm['table_name'] # Return model in current state by name
6
+ > Model = migrator.ModelClass # Return model in current state by name
7
+
8
+ > migrator.sql(sql) # Run custom SQL
9
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
10
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
11
+ > migrator.remove_model(model, cascade=True) # Remove a model
12
+ > migrator.add_fields(model, **fields) # Add fields to a model
13
+ > migrator.change_fields(model, **fields) # Change fields
14
+ > migrator.remove_fields(model, *field_names, cascade=True)
15
+ > migrator.rename_field(model, old_field_name, new_field_name)
16
+ > migrator.rename_table(model, new_table_name)
17
+ > migrator.add_index(model, *col_names, unique=False)
18
+ > migrator.add_not_null(model, *field_names)
19
+ > migrator.add_default(model, field_name, default)
20
+ > migrator.add_constraint(model, name, sql)
21
+ > migrator.drop_index(model, *col_names)
22
+ > migrator.drop_not_null(model, *field_names)
23
+ > migrator.drop_constraints(model, *constraints)
24
+
25
+ """
26
+
27
+ from contextlib import suppress
28
+
29
+ import peewee as pw
30
+ from peewee_migrate import Migrator
31
+
32
+
33
+ with suppress(ImportError):
34
+ import playhouse.postgres_ext as pw_pext
35
+
36
+
37
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
38
+ """Write your migrations here."""
39
+
40
+ @migrator.create_model
41
+ class Tool(pw.Model):
42
+ id = pw.TextField(unique=True)
43
+ user_id = pw.TextField()
44
+
45
+ name = pw.TextField()
46
+ content = pw.TextField()
47
+ specs = pw.TextField()
48
+
49
+ meta = pw.TextField()
50
+
51
+ created_at = pw.BigIntegerField(null=False)
52
+ updated_at = pw.BigIntegerField(null=False)
53
+
54
+ class Meta:
55
+ table_name = "tool"
56
+
57
+
58
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
59
+ """Write your rollback migrations here."""
60
+
61
+ migrator.remove_model("tool")
backend/open_webui/internal/migrations/013_add_user_info.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peewee migrations -- 002_add_local_sharing.py.
2
+
3
+ Some examples (model - class or model name)::
4
+
5
+ > Model = migrator.orm['table_name'] # Return model in current state by name
6
+ > Model = migrator.ModelClass # Return model in current state by name
7
+
8
+ > migrator.sql(sql) # Run custom SQL
9
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
10
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
11
+ > migrator.remove_model(model, cascade=True) # Remove a model
12
+ > migrator.add_fields(model, **fields) # Add fields to a model
13
+ > migrator.change_fields(model, **fields) # Change fields
14
+ > migrator.remove_fields(model, *field_names, cascade=True)
15
+ > migrator.rename_field(model, old_field_name, new_field_name)
16
+ > migrator.rename_table(model, new_table_name)
17
+ > migrator.add_index(model, *col_names, unique=False)
18
+ > migrator.add_not_null(model, *field_names)
19
+ > migrator.add_default(model, field_name, default)
20
+ > migrator.add_constraint(model, name, sql)
21
+ > migrator.drop_index(model, *col_names)
22
+ > migrator.drop_not_null(model, *field_names)
23
+ > migrator.drop_constraints(model, *constraints)
24
+
25
+ """
26
+
27
+ from contextlib import suppress
28
+
29
+ import peewee as pw
30
+ from peewee_migrate import Migrator
31
+
32
+
33
+ with suppress(ImportError):
34
+ import playhouse.postgres_ext as pw_pext
35
+
36
+
37
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
38
+ """Write your migrations here."""
39
+
40
+ # Adding fields info to the 'user' table
41
+ migrator.add_fields("user", info=pw.TextField(null=True))
42
+
43
+
44
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
45
+ """Write your rollback migrations here."""
46
+
47
+ # Remove the settings field
48
+ migrator.remove_fields("user", "info")
backend/open_webui/internal/migrations/014_add_files.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peewee migrations -- 009_add_models.py.
2
+
3
+ Some examples (model - class or model name)::
4
+
5
+ > Model = migrator.orm['table_name'] # Return model in current state by name
6
+ > Model = migrator.ModelClass # Return model in current state by name
7
+
8
+ > migrator.sql(sql) # Run custom SQL
9
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
10
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
11
+ > migrator.remove_model(model, cascade=True) # Remove a model
12
+ > migrator.add_fields(model, **fields) # Add fields to a model
13
+ > migrator.change_fields(model, **fields) # Change fields
14
+ > migrator.remove_fields(model, *field_names, cascade=True)
15
+ > migrator.rename_field(model, old_field_name, new_field_name)
16
+ > migrator.rename_table(model, new_table_name)
17
+ > migrator.add_index(model, *col_names, unique=False)
18
+ > migrator.add_not_null(model, *field_names)
19
+ > migrator.add_default(model, field_name, default)
20
+ > migrator.add_constraint(model, name, sql)
21
+ > migrator.drop_index(model, *col_names)
22
+ > migrator.drop_not_null(model, *field_names)
23
+ > migrator.drop_constraints(model, *constraints)
24
+
25
+ """
26
+
27
+ from contextlib import suppress
28
+
29
+ import peewee as pw
30
+ from peewee_migrate import Migrator
31
+
32
+
33
+ with suppress(ImportError):
34
+ import playhouse.postgres_ext as pw_pext
35
+
36
+
37
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
38
+ """Write your migrations here."""
39
+
40
+ @migrator.create_model
41
+ class File(pw.Model):
42
+ id = pw.TextField(unique=True)
43
+ user_id = pw.TextField()
44
+ filename = pw.TextField()
45
+ meta = pw.TextField()
46
+ created_at = pw.BigIntegerField(null=False)
47
+
48
+ class Meta:
49
+ table_name = "file"
50
+
51
+
52
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
53
+ """Write your rollback migrations here."""
54
+
55
+ migrator.remove_model("file")
backend/open_webui/internal/migrations/015_add_functions.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peewee migrations -- 009_add_models.py.
2
+
3
+ Some examples (model - class or model name)::
4
+
5
+ > Model = migrator.orm['table_name'] # Return model in current state by name
6
+ > Model = migrator.ModelClass # Return model in current state by name
7
+
8
+ > migrator.sql(sql) # Run custom SQL
9
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
10
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
11
+ > migrator.remove_model(model, cascade=True) # Remove a model
12
+ > migrator.add_fields(model, **fields) # Add fields to a model
13
+ > migrator.change_fields(model, **fields) # Change fields
14
+ > migrator.remove_fields(model, *field_names, cascade=True)
15
+ > migrator.rename_field(model, old_field_name, new_field_name)
16
+ > migrator.rename_table(model, new_table_name)
17
+ > migrator.add_index(model, *col_names, unique=False)
18
+ > migrator.add_not_null(model, *field_names)
19
+ > migrator.add_default(model, field_name, default)
20
+ > migrator.add_constraint(model, name, sql)
21
+ > migrator.drop_index(model, *col_names)
22
+ > migrator.drop_not_null(model, *field_names)
23
+ > migrator.drop_constraints(model, *constraints)
24
+
25
+ """
26
+
27
+ from contextlib import suppress
28
+
29
+ import peewee as pw
30
+ from peewee_migrate import Migrator
31
+
32
+
33
+ with suppress(ImportError):
34
+ import playhouse.postgres_ext as pw_pext
35
+
36
+
37
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
38
+ """Write your migrations here."""
39
+
40
+ @migrator.create_model
41
+ class Function(pw.Model):
42
+ id = pw.TextField(unique=True)
43
+ user_id = pw.TextField()
44
+
45
+ name = pw.TextField()
46
+ type = pw.TextField()
47
+
48
+ content = pw.TextField()
49
+ meta = pw.TextField()
50
+
51
+ created_at = pw.BigIntegerField(null=False)
52
+ updated_at = pw.BigIntegerField(null=False)
53
+
54
+ class Meta:
55
+ table_name = "function"
56
+
57
+
58
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
59
+ """Write your rollback migrations here."""
60
+
61
+ migrator.remove_model("function")
backend/open_webui/internal/migrations/016_add_valves_and_is_active.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peewee migrations -- 009_add_models.py.
2
+
3
+ Some examples (model - class or model name)::
4
+
5
+ > Model = migrator.orm['table_name'] # Return model in current state by name
6
+ > Model = migrator.ModelClass # Return model in current state by name
7
+
8
+ > migrator.sql(sql) # Run custom SQL
9
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
10
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
11
+ > migrator.remove_model(model, cascade=True) # Remove a model
12
+ > migrator.add_fields(model, **fields) # Add fields to a model
13
+ > migrator.change_fields(model, **fields) # Change fields
14
+ > migrator.remove_fields(model, *field_names, cascade=True)
15
+ > migrator.rename_field(model, old_field_name, new_field_name)
16
+ > migrator.rename_table(model, new_table_name)
17
+ > migrator.add_index(model, *col_names, unique=False)
18
+ > migrator.add_not_null(model, *field_names)
19
+ > migrator.add_default(model, field_name, default)
20
+ > migrator.add_constraint(model, name, sql)
21
+ > migrator.drop_index(model, *col_names)
22
+ > migrator.drop_not_null(model, *field_names)
23
+ > migrator.drop_constraints(model, *constraints)
24
+
25
+ """
26
+
27
+ from contextlib import suppress
28
+
29
+ import peewee as pw
30
+ from peewee_migrate import Migrator
31
+
32
+
33
+ with suppress(ImportError):
34
+ import playhouse.postgres_ext as pw_pext
35
+
36
+
37
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
38
+ """Write your migrations here."""
39
+
40
+ migrator.add_fields("tool", valves=pw.TextField(null=True))
41
+ migrator.add_fields("function", valves=pw.TextField(null=True))
42
+ migrator.add_fields("function", is_active=pw.BooleanField(default=False))
43
+
44
+
45
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
46
+ """Write your rollback migrations here."""
47
+
48
+ migrator.remove_fields("tool", "valves")
49
+ migrator.remove_fields("function", "valves")
50
+ migrator.remove_fields("function", "is_active")
backend/open_webui/internal/migrations/017_add_user_oauth_sub.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peewee migrations -- 017_add_user_oauth_sub.py.
2
+ Some examples (model - class or model name)::
3
+ > Model = migrator.orm['table_name'] # Return model in current state by name
4
+ > Model = migrator.ModelClass # Return model in current state by name
5
+ > migrator.sql(sql) # Run custom SQL
6
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
7
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
8
+ > migrator.remove_model(model, cascade=True) # Remove a model
9
+ > migrator.add_fields(model, **fields) # Add fields to a model
10
+ > migrator.change_fields(model, **fields) # Change fields
11
+ > migrator.remove_fields(model, *field_names, cascade=True)
12
+ > migrator.rename_field(model, old_field_name, new_field_name)
13
+ > migrator.rename_table(model, new_table_name)
14
+ > migrator.add_index(model, *col_names, unique=False)
15
+ > migrator.add_not_null(model, *field_names)
16
+ > migrator.add_default(model, field_name, default)
17
+ > migrator.add_constraint(model, name, sql)
18
+ > migrator.drop_index(model, *col_names)
19
+ > migrator.drop_not_null(model, *field_names)
20
+ > migrator.drop_constraints(model, *constraints)
21
+ """
22
+
23
+ from contextlib import suppress
24
+
25
+ import peewee as pw
26
+ from peewee_migrate import Migrator
27
+
28
+
29
+ with suppress(ImportError):
30
+ import playhouse.postgres_ext as pw_pext
31
+
32
+
33
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
34
+ """Write your migrations here."""
35
+
36
+ migrator.add_fields(
37
+ "user",
38
+ oauth_sub=pw.TextField(null=True, unique=True),
39
+ )
40
+
41
+
42
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
43
+ """Write your rollback migrations here."""
44
+
45
+ migrator.remove_fields("user", "oauth_sub")
backend/open_webui/internal/migrations/018_add_function_is_global.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peewee migrations -- 017_add_user_oauth_sub.py.
2
+
3
+ Some examples (model - class or model name)::
4
+
5
+ > Model = migrator.orm['table_name'] # Return model in current state by name
6
+ > Model = migrator.ModelClass # Return model in current state by name
7
+
8
+ > migrator.sql(sql) # Run custom SQL
9
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
10
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
11
+ > migrator.remove_model(model, cascade=True) # Remove a model
12
+ > migrator.add_fields(model, **fields) # Add fields to a model
13
+ > migrator.change_fields(model, **fields) # Change fields
14
+ > migrator.remove_fields(model, *field_names, cascade=True)
15
+ > migrator.rename_field(model, old_field_name, new_field_name)
16
+ > migrator.rename_table(model, new_table_name)
17
+ > migrator.add_index(model, *col_names, unique=False)
18
+ > migrator.add_not_null(model, *field_names)
19
+ > migrator.add_default(model, field_name, default)
20
+ > migrator.add_constraint(model, name, sql)
21
+ > migrator.drop_index(model, *col_names)
22
+ > migrator.drop_not_null(model, *field_names)
23
+ > migrator.drop_constraints(model, *constraints)
24
+
25
+ """
26
+
27
+ from contextlib import suppress
28
+
29
+ import peewee as pw
30
+ from peewee_migrate import Migrator
31
+
32
+
33
+ with suppress(ImportError):
34
+ import playhouse.postgres_ext as pw_pext
35
+
36
+
37
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
38
+ """Write your migrations here."""
39
+
40
+ migrator.add_fields(
41
+ "function",
42
+ is_global=pw.BooleanField(default=False),
43
+ )
44
+
45
+
46
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
47
+ """Write your rollback migrations here."""
48
+
49
+ migrator.remove_fields("function", "is_global")
backend/open_webui/internal/wrappers.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ from contextvars import ContextVar
4
+
5
+ from open_webui.env import SRC_LOG_LEVELS
6
+ from peewee import *
7
+ from peewee import InterfaceError as PeeWeeInterfaceError
8
+ from peewee import PostgresqlDatabase
9
+ from playhouse.db_url import connect, parse
10
+ from playhouse.shortcuts import ReconnectMixin
11
+
12
+ log = logging.getLogger(__name__)
13
+ log.setLevel(SRC_LOG_LEVELS["DB"])
14
+
15
+ db_state_default = {"closed": None, "conn": None, "ctx": None, "transactions": None}
16
+ db_state = ContextVar("db_state", default=db_state_default.copy())
17
+
18
+
19
+ class PeeweeConnectionState(object):
20
+ def __init__(self, **kwargs):
21
+ super().__setattr__("_state", db_state)
22
+ super().__init__(**kwargs)
23
+
24
+ def __setattr__(self, name, value):
25
+ self._state.get()[name] = value
26
+
27
+ def __getattr__(self, name):
28
+ value = self._state.get()[name]
29
+ return value
30
+
31
+
32
+ class CustomReconnectMixin(ReconnectMixin):
33
+ reconnect_errors = (
34
+ # psycopg2
35
+ (OperationalError, "termin"),
36
+ (InterfaceError, "closed"),
37
+ # peewee
38
+ (PeeWeeInterfaceError, "closed"),
39
+ )
40
+
41
+
42
+ class ReconnectingPostgresqlDatabase(CustomReconnectMixin, PostgresqlDatabase):
43
+ pass
44
+
45
+
46
+ def register_connection(db_url):
47
+ # Check if using SQLCipher protocol
48
+ if db_url.startswith("sqlite+sqlcipher://"):
49
+ database_password = os.environ.get("DATABASE_PASSWORD")
50
+ if not database_password or database_password.strip() == "":
51
+ raise ValueError(
52
+ "DATABASE_PASSWORD is required when using sqlite+sqlcipher:// URLs"
53
+ )
54
+ from playhouse.sqlcipher_ext import SqlCipherDatabase
55
+
56
+ # Parse the database path from SQLCipher URL
57
+ # Convert sqlite+sqlcipher:///path/to/db.sqlite to /path/to/db.sqlite
58
+ db_path = db_url.replace("sqlite+sqlcipher://", "")
59
+ if db_path.startswith("/"):
60
+ db_path = db_path[1:] # Remove leading slash for relative paths
61
+
62
+ # Use Peewee's native SqlCipherDatabase with encryption
63
+ db = SqlCipherDatabase(db_path, passphrase=database_password)
64
+ db.autoconnect = True
65
+ db.reuse_if_open = True
66
+ log.info("Connected to encrypted SQLite database using SQLCipher")
67
+
68
+ else:
69
+ # Standard database connection (existing logic)
70
+ db = connect(db_url, unquote_user=True, unquote_password=True)
71
+ if isinstance(db, PostgresqlDatabase):
72
+ # Enable autoconnect for SQLite databases, managed by Peewee
73
+ db.autoconnect = True
74
+ db.reuse_if_open = True
75
+ log.info("Connected to PostgreSQL database")
76
+
77
+ # Get the connection details
78
+ connection = parse(db_url, unquote_user=True, unquote_password=True)
79
+
80
+ # Use our custom database class that supports reconnection
81
+ db = ReconnectingPostgresqlDatabase(**connection)
82
+ db.connect(reuse_if_open=True)
83
+ elif isinstance(db, SqliteDatabase):
84
+ # Enable autoconnect for SQLite databases, managed by Peewee
85
+ db.autoconnect = True
86
+ db.reuse_if_open = True
87
+ log.info("Connected to SQLite database")
88
+ else:
89
+ raise ValueError("Unsupported database connection")
90
+ return db
backend/open_webui/main.py ADDED
@@ -0,0 +1,1976 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import inspect
3
+ import json
4
+ import logging
5
+ import mimetypes
6
+ import os
7
+ import shutil
8
+ import sys
9
+ import time
10
+ import random
11
+ from uuid import uuid4
12
+
13
+
14
+ from contextlib import asynccontextmanager
15
+ from urllib.parse import urlencode, parse_qs, urlparse
16
+ from pydantic import BaseModel
17
+ from sqlalchemy import text
18
+
19
+ from typing import Optional
20
+ from aiocache import cached
21
+ import aiohttp
22
+ import anyio.to_thread
23
+ import requests
24
+ from redis import Redis
25
+
26
+
27
+ from fastapi import (
28
+ Depends,
29
+ FastAPI,
30
+ File,
31
+ Form,
32
+ HTTPException,
33
+ Request,
34
+ UploadFile,
35
+ status,
36
+ applications,
37
+ BackgroundTasks,
38
+ )
39
+ from fastapi.openapi.docs import get_swagger_ui_html
40
+
41
+ from fastapi.middleware.cors import CORSMiddleware
42
+ from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
43
+ from fastapi.staticfiles import StaticFiles
44
+
45
+ from starlette_compress import CompressMiddleware
46
+
47
+ from starlette.exceptions import HTTPException as StarletteHTTPException
48
+ from starlette.middleware.base import BaseHTTPMiddleware
49
+ from starlette.middleware.sessions import SessionMiddleware
50
+ from starlette.responses import Response, StreamingResponse
51
+ from starlette.datastructures import Headers
52
+
53
+
54
+ from open_webui.utils import logger
55
+ from open_webui.utils.audit import AuditLevel, AuditLoggingMiddleware
56
+ from open_webui.utils.logger import start_logger
57
+ from open_webui.socket.main import (
58
+ app as socket_app,
59
+ periodic_usage_pool_cleanup,
60
+ get_event_emitter,
61
+ get_models_in_use,
62
+ get_active_user_ids,
63
+ )
64
+ from open_webui.routers import (
65
+ audio,
66
+ images,
67
+ ollama,
68
+ openai,
69
+ retrieval,
70
+ pipelines,
71
+ tasks,
72
+ auths,
73
+ channels,
74
+ chats,
75
+ notes,
76
+ folders,
77
+ configs,
78
+ groups,
79
+ files,
80
+ functions,
81
+ memories,
82
+ models,
83
+ knowledge,
84
+ prompts,
85
+ evaluations,
86
+ tools,
87
+ users,
88
+ utils,
89
+ scim,
90
+ )
91
+
92
+ from open_webui.routers.retrieval import (
93
+ get_embedding_function,
94
+ get_reranking_function,
95
+ get_ef,
96
+ get_rf,
97
+ )
98
+
99
+ from open_webui.internal.db import Session, engine
100
+
101
+ from open_webui.models.functions import Functions
102
+ from open_webui.models.models import Models
103
+ from open_webui.models.users import UserModel, Users
104
+ from open_webui.models.chats import Chats
105
+
106
+ from open_webui.config import (
107
+ # Ollama
108
+ ENABLE_OLLAMA_API,
109
+ OLLAMA_BASE_URLS,
110
+ OLLAMA_API_CONFIGS,
111
+ # OpenAI
112
+ ENABLE_OPENAI_API,
113
+ ONEDRIVE_CLIENT_ID,
114
+ ONEDRIVE_SHAREPOINT_URL,
115
+ ONEDRIVE_SHAREPOINT_TENANT_ID,
116
+ OPENAI_API_BASE_URLS,
117
+ OPENAI_API_KEYS,
118
+ OPENAI_API_CONFIGS,
119
+ # Direct Connections
120
+ ENABLE_DIRECT_CONNECTIONS,
121
+ # Model list
122
+ ENABLE_BASE_MODELS_CACHE,
123
+ # Thread pool size for FastAPI/AnyIO
124
+ THREAD_POOL_SIZE,
125
+ # Tool Server Configs
126
+ TOOL_SERVER_CONNECTIONS,
127
+ # Code Execution
128
+ ENABLE_CODE_EXECUTION,
129
+ CODE_EXECUTION_ENGINE,
130
+ CODE_EXECUTION_JUPYTER_URL,
131
+ CODE_EXECUTION_JUPYTER_AUTH,
132
+ CODE_EXECUTION_JUPYTER_AUTH_TOKEN,
133
+ CODE_EXECUTION_JUPYTER_AUTH_PASSWORD,
134
+ CODE_EXECUTION_JUPYTER_TIMEOUT,
135
+ ENABLE_CODE_INTERPRETER,
136
+ CODE_INTERPRETER_ENGINE,
137
+ CODE_INTERPRETER_PROMPT_TEMPLATE,
138
+ CODE_INTERPRETER_JUPYTER_URL,
139
+ CODE_INTERPRETER_JUPYTER_AUTH,
140
+ CODE_INTERPRETER_JUPYTER_AUTH_TOKEN,
141
+ CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD,
142
+ CODE_INTERPRETER_JUPYTER_TIMEOUT,
143
+ # Image
144
+ AUTOMATIC1111_API_AUTH,
145
+ AUTOMATIC1111_BASE_URL,
146
+ AUTOMATIC1111_CFG_SCALE,
147
+ AUTOMATIC1111_SAMPLER,
148
+ AUTOMATIC1111_SCHEDULER,
149
+ COMFYUI_BASE_URL,
150
+ COMFYUI_API_KEY,
151
+ COMFYUI_WORKFLOW,
152
+ COMFYUI_WORKFLOW_NODES,
153
+ ENABLE_IMAGE_GENERATION,
154
+ ENABLE_IMAGE_PROMPT_GENERATION,
155
+ IMAGE_GENERATION_ENGINE,
156
+ IMAGE_GENERATION_MODEL,
157
+ IMAGE_SIZE,
158
+ IMAGE_STEPS,
159
+ IMAGES_OPENAI_API_BASE_URL,
160
+ IMAGES_OPENAI_API_KEY,
161
+ IMAGES_GEMINI_API_BASE_URL,
162
+ IMAGES_GEMINI_API_KEY,
163
+ # Audio
164
+ AUDIO_STT_ENGINE,
165
+ AUDIO_STT_MODEL,
166
+ AUDIO_STT_SUPPORTED_CONTENT_TYPES,
167
+ AUDIO_STT_OPENAI_API_BASE_URL,
168
+ AUDIO_STT_OPENAI_API_KEY,
169
+ AUDIO_STT_AZURE_API_KEY,
170
+ AUDIO_STT_AZURE_REGION,
171
+ AUDIO_STT_AZURE_LOCALES,
172
+ AUDIO_STT_AZURE_BASE_URL,
173
+ AUDIO_STT_AZURE_MAX_SPEAKERS,
174
+ AUDIO_TTS_API_KEY,
175
+ AUDIO_TTS_ENGINE,
176
+ AUDIO_TTS_MODEL,
177
+ AUDIO_TTS_OPENAI_API_BASE_URL,
178
+ AUDIO_TTS_OPENAI_API_KEY,
179
+ AUDIO_TTS_SPLIT_ON,
180
+ AUDIO_TTS_VOICE,
181
+ AUDIO_TTS_AZURE_SPEECH_REGION,
182
+ AUDIO_TTS_AZURE_SPEECH_BASE_URL,
183
+ AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT,
184
+ PLAYWRIGHT_WS_URL,
185
+ PLAYWRIGHT_TIMEOUT,
186
+ FIRECRAWL_API_BASE_URL,
187
+ FIRECRAWL_API_KEY,
188
+ WEB_LOADER_ENGINE,
189
+ WEB_LOADER_CONCURRENT_REQUESTS,
190
+ WHISPER_MODEL,
191
+ WHISPER_VAD_FILTER,
192
+ WHISPER_LANGUAGE,
193
+ DEEPGRAM_API_KEY,
194
+ WHISPER_MODEL_AUTO_UPDATE,
195
+ WHISPER_MODEL_DIR,
196
+ # Retrieval
197
+ RAG_TEMPLATE,
198
+ DEFAULT_RAG_TEMPLATE,
199
+ RAG_FULL_CONTEXT,
200
+ BYPASS_EMBEDDING_AND_RETRIEVAL,
201
+ RAG_EMBEDDING_MODEL,
202
+ RAG_EMBEDDING_MODEL_AUTO_UPDATE,
203
+ RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
204
+ RAG_RERANKING_ENGINE,
205
+ RAG_RERANKING_MODEL,
206
+ RAG_EXTERNAL_RERANKER_URL,
207
+ RAG_EXTERNAL_RERANKER_API_KEY,
208
+ RAG_RERANKING_MODEL_AUTO_UPDATE,
209
+ RAG_RERANKING_MODEL_TRUST_REMOTE_CODE,
210
+ RAG_EMBEDDING_ENGINE,
211
+ RAG_EMBEDDING_BATCH_SIZE,
212
+ RAG_TOP_K,
213
+ RAG_TOP_K_RERANKER,
214
+ RAG_RELEVANCE_THRESHOLD,
215
+ RAG_HYBRID_BM25_WEIGHT,
216
+ RAG_ALLOWED_FILE_EXTENSIONS,
217
+ RAG_FILE_MAX_COUNT,
218
+ RAG_FILE_MAX_SIZE,
219
+ FILE_IMAGE_COMPRESSION_WIDTH,
220
+ FILE_IMAGE_COMPRESSION_HEIGHT,
221
+ RAG_OPENAI_API_BASE_URL,
222
+ RAG_OPENAI_API_KEY,
223
+ RAG_AZURE_OPENAI_BASE_URL,
224
+ RAG_AZURE_OPENAI_API_KEY,
225
+ RAG_AZURE_OPENAI_API_VERSION,
226
+ RAG_OLLAMA_BASE_URL,
227
+ RAG_OLLAMA_API_KEY,
228
+ CHUNK_OVERLAP,
229
+ CHUNK_SIZE,
230
+ CONTENT_EXTRACTION_ENGINE,
231
+ DATALAB_MARKER_API_KEY,
232
+ DATALAB_MARKER_API_BASE_URL,
233
+ DATALAB_MARKER_ADDITIONAL_CONFIG,
234
+ DATALAB_MARKER_SKIP_CACHE,
235
+ DATALAB_MARKER_FORCE_OCR,
236
+ DATALAB_MARKER_PAGINATE,
237
+ DATALAB_MARKER_STRIP_EXISTING_OCR,
238
+ DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION,
239
+ DATALAB_MARKER_FORMAT_LINES,
240
+ DATALAB_MARKER_OUTPUT_FORMAT,
241
+ DATALAB_MARKER_USE_LLM,
242
+ EXTERNAL_DOCUMENT_LOADER_URL,
243
+ EXTERNAL_DOCUMENT_LOADER_API_KEY,
244
+ TIKA_SERVER_URL,
245
+ DOCLING_SERVER_URL,
246
+ DOCLING_OCR_ENGINE,
247
+ DOCLING_OCR_LANG,
248
+ DOCLING_DO_PICTURE_DESCRIPTION,
249
+ DOCLING_PICTURE_DESCRIPTION_MODE,
250
+ DOCLING_PICTURE_DESCRIPTION_LOCAL,
251
+ DOCLING_PICTURE_DESCRIPTION_API,
252
+ DOCUMENT_INTELLIGENCE_ENDPOINT,
253
+ DOCUMENT_INTELLIGENCE_KEY,
254
+ MISTRAL_OCR_API_KEY,
255
+ RAG_TEXT_SPLITTER,
256
+ TIKTOKEN_ENCODING_NAME,
257
+ PDF_EXTRACT_IMAGES,
258
+ YOUTUBE_LOADER_LANGUAGE,
259
+ YOUTUBE_LOADER_PROXY_URL,
260
+ # Retrieval (Web Search)
261
+ ENABLE_WEB_SEARCH,
262
+ WEB_SEARCH_ENGINE,
263
+ BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL,
264
+ BYPASS_WEB_SEARCH_WEB_LOADER,
265
+ WEB_SEARCH_RESULT_COUNT,
266
+ WEB_SEARCH_CONCURRENT_REQUESTS,
267
+ WEB_SEARCH_TRUST_ENV,
268
+ WEB_SEARCH_DOMAIN_FILTER_LIST,
269
+ JINA_API_KEY,
270
+ SEARCHAPI_API_KEY,
271
+ SEARCHAPI_ENGINE,
272
+ SERPAPI_API_KEY,
273
+ SERPAPI_ENGINE,
274
+ SEARXNG_QUERY_URL,
275
+ YACY_QUERY_URL,
276
+ YACY_USERNAME,
277
+ YACY_PASSWORD,
278
+ SERPER_API_KEY,
279
+ SERPLY_API_KEY,
280
+ SERPSTACK_API_KEY,
281
+ SERPSTACK_HTTPS,
282
+ TAVILY_API_KEY,
283
+ TAVILY_EXTRACT_DEPTH,
284
+ BING_SEARCH_V7_ENDPOINT,
285
+ BING_SEARCH_V7_SUBSCRIPTION_KEY,
286
+ BRAVE_SEARCH_API_KEY,
287
+ EXA_API_KEY,
288
+ PERPLEXITY_API_KEY,
289
+ PERPLEXITY_MODEL,
290
+ PERPLEXITY_SEARCH_CONTEXT_USAGE,
291
+ SOUGOU_API_SID,
292
+ SOUGOU_API_SK,
293
+ KAGI_SEARCH_API_KEY,
294
+ MOJEEK_SEARCH_API_KEY,
295
+ BOCHA_SEARCH_API_KEY,
296
+ GOOGLE_PSE_API_KEY,
297
+ GOOGLE_PSE_ENGINE_ID,
298
+ GOOGLE_DRIVE_CLIENT_ID,
299
+ GOOGLE_DRIVE_API_KEY,
300
+ ONEDRIVE_CLIENT_ID,
301
+ ONEDRIVE_SHAREPOINT_URL,
302
+ ONEDRIVE_SHAREPOINT_TENANT_ID,
303
+ ENABLE_RAG_HYBRID_SEARCH,
304
+ ENABLE_RAG_LOCAL_WEB_FETCH,
305
+ ENABLE_WEB_LOADER_SSL_VERIFICATION,
306
+ ENABLE_GOOGLE_DRIVE_INTEGRATION,
307
+ ENABLE_ONEDRIVE_INTEGRATION,
308
+ UPLOAD_DIR,
309
+ EXTERNAL_WEB_SEARCH_URL,
310
+ EXTERNAL_WEB_SEARCH_API_KEY,
311
+ EXTERNAL_WEB_LOADER_URL,
312
+ EXTERNAL_WEB_LOADER_API_KEY,
313
+ # WebUI
314
+ WEBUI_AUTH,
315
+ WEBUI_NAME,
316
+ WEBUI_BANNERS,
317
+ WEBHOOK_URL,
318
+ ADMIN_EMAIL,
319
+ SHOW_ADMIN_DETAILS,
320
+ JWT_EXPIRES_IN,
321
+ ENABLE_SIGNUP,
322
+ ENABLE_LOGIN_FORM,
323
+ ENABLE_API_KEY,
324
+ ENABLE_API_KEY_ENDPOINT_RESTRICTIONS,
325
+ API_KEY_ALLOWED_ENDPOINTS,
326
+ ENABLE_CHANNELS,
327
+ ENABLE_NOTES,
328
+ ENABLE_COMMUNITY_SHARING,
329
+ ENABLE_MESSAGE_RATING,
330
+ ENABLE_USER_WEBHOOKS,
331
+ ENABLE_EVALUATION_ARENA_MODELS,
332
+ BYPASS_ADMIN_ACCESS_CONTROL,
333
+ USER_PERMISSIONS,
334
+ DEFAULT_USER_ROLE,
335
+ PENDING_USER_OVERLAY_CONTENT,
336
+ PENDING_USER_OVERLAY_TITLE,
337
+ DEFAULT_PROMPT_SUGGESTIONS,
338
+ DEFAULT_MODELS,
339
+ DEFAULT_ARENA_MODEL,
340
+ MODEL_ORDER_LIST,
341
+ EVALUATION_ARENA_MODELS,
342
+ # WebUI (OAuth)
343
+ ENABLE_OAUTH_ROLE_MANAGEMENT,
344
+ OAUTH_ROLES_CLAIM,
345
+ OAUTH_EMAIL_CLAIM,
346
+ OAUTH_PICTURE_CLAIM,
347
+ OAUTH_USERNAME_CLAIM,
348
+ OAUTH_ALLOWED_ROLES,
349
+ OAUTH_ADMIN_ROLES,
350
+ # WebUI (LDAP)
351
+ ENABLE_LDAP,
352
+ LDAP_SERVER_LABEL,
353
+ LDAP_SERVER_HOST,
354
+ LDAP_SERVER_PORT,
355
+ LDAP_ATTRIBUTE_FOR_MAIL,
356
+ LDAP_ATTRIBUTE_FOR_USERNAME,
357
+ LDAP_SEARCH_FILTERS,
358
+ LDAP_SEARCH_BASE,
359
+ LDAP_APP_DN,
360
+ LDAP_APP_PASSWORD,
361
+ LDAP_USE_TLS,
362
+ LDAP_CA_CERT_FILE,
363
+ LDAP_VALIDATE_CERT,
364
+ LDAP_CIPHERS,
365
+ # LDAP Group Management
366
+ ENABLE_LDAP_GROUP_MANAGEMENT,
367
+ ENABLE_LDAP_GROUP_CREATION,
368
+ LDAP_ATTRIBUTE_FOR_GROUPS,
369
+ # Misc
370
+ ENV,
371
+ CACHE_DIR,
372
+ STATIC_DIR,
373
+ FRONTEND_BUILD_DIR,
374
+ CORS_ALLOW_ORIGIN,
375
+ DEFAULT_LOCALE,
376
+ OAUTH_PROVIDERS,
377
+ WEBUI_URL,
378
+ RESPONSE_WATERMARK,
379
+ # Admin
380
+ ENABLE_ADMIN_CHAT_ACCESS,
381
+ BYPASS_ADMIN_ACCESS_CONTROL,
382
+ ENABLE_ADMIN_EXPORT,
383
+ # Tasks
384
+ TASK_MODEL,
385
+ TASK_MODEL_EXTERNAL,
386
+ ENABLE_TAGS_GENERATION,
387
+ ENABLE_TITLE_GENERATION,
388
+ ENABLE_FOLLOW_UP_GENERATION,
389
+ ENABLE_SEARCH_QUERY_GENERATION,
390
+ ENABLE_RETRIEVAL_QUERY_GENERATION,
391
+ ENABLE_AUTOCOMPLETE_GENERATION,
392
+ TITLE_GENERATION_PROMPT_TEMPLATE,
393
+ FOLLOW_UP_GENERATION_PROMPT_TEMPLATE,
394
+ TAGS_GENERATION_PROMPT_TEMPLATE,
395
+ IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE,
396
+ TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
397
+ QUERY_GENERATION_PROMPT_TEMPLATE,
398
+ AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE,
399
+ AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH,
400
+ AppConfig,
401
+ reset_config,
402
+ )
403
+ from open_webui.env import (
404
+ LICENSE_KEY,
405
+ AUDIT_EXCLUDED_PATHS,
406
+ AUDIT_LOG_LEVEL,
407
+ CHANGELOG,
408
+ REDIS_URL,
409
+ REDIS_CLUSTER,
410
+ REDIS_KEY_PREFIX,
411
+ REDIS_SENTINEL_HOSTS,
412
+ REDIS_SENTINEL_PORT,
413
+ GLOBAL_LOG_LEVEL,
414
+ MAX_BODY_LOG_SIZE,
415
+ SAFE_MODE,
416
+ SRC_LOG_LEVELS,
417
+ VERSION,
418
+ INSTANCE_ID,
419
+ WEBUI_BUILD_HASH,
420
+ WEBUI_SECRET_KEY,
421
+ WEBUI_SESSION_COOKIE_SAME_SITE,
422
+ WEBUI_SESSION_COOKIE_SECURE,
423
+ ENABLE_SIGNUP_PASSWORD_CONFIRMATION,
424
+ WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
425
+ WEBUI_AUTH_TRUSTED_NAME_HEADER,
426
+ WEBUI_AUTH_SIGNOUT_REDIRECT_URL,
427
+ # SCIM
428
+ SCIM_ENABLED,
429
+ SCIM_TOKEN,
430
+ ENABLE_COMPRESSION_MIDDLEWARE,
431
+ ENABLE_WEBSOCKET_SUPPORT,
432
+ BYPASS_MODEL_ACCESS_CONTROL,
433
+ RESET_CONFIG_ON_START,
434
+ ENABLE_VERSION_UPDATE_CHECK,
435
+ ENABLE_OTEL,
436
+ EXTERNAL_PWA_MANIFEST_URL,
437
+ AIOHTTP_CLIENT_SESSION_SSL,
438
+ )
439
+
440
+
441
+ from open_webui.utils.models import (
442
+ get_all_models,
443
+ get_all_base_models,
444
+ check_model_access,
445
+ )
446
+ from open_webui.utils.chat import (
447
+ generate_chat_completion as chat_completion_handler,
448
+ chat_completed as chat_completed_handler,
449
+ chat_action as chat_action_handler,
450
+ )
451
+ from open_webui.utils.embeddings import generate_embeddings
452
+ from open_webui.utils.middleware import process_chat_payload, process_chat_response
453
+ from open_webui.utils.access_control import has_access
454
+
455
+ from open_webui.utils.auth import (
456
+ get_license_data,
457
+ get_http_authorization_cred,
458
+ decode_token,
459
+ get_admin_user,
460
+ get_verified_user,
461
+ )
462
+ from open_webui.utils.plugin import install_tool_and_function_dependencies
463
+ from open_webui.utils.oauth import OAuthManager
464
+ from open_webui.utils.security_headers import SecurityHeadersMiddleware
465
+ from open_webui.utils.redis import get_redis_connection
466
+
467
+ from open_webui.tasks import (
468
+ redis_task_command_listener,
469
+ list_task_ids_by_item_id,
470
+ create_task,
471
+ stop_task,
472
+ list_tasks,
473
+ ) # Import from tasks.py
474
+
475
+ from open_webui.utils.redis import get_sentinels_from_env
476
+
477
+
478
+ from open_webui.constants import ERROR_MESSAGES
479
+
480
+
481
+ if SAFE_MODE:
482
+ print("SAFE MODE ENABLED")
483
+ Functions.deactivate_all_functions()
484
+
485
+ logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
486
+ log = logging.getLogger(__name__)
487
+ log.setLevel(SRC_LOG_LEVELS["MAIN"])
488
+
489
+
490
+ class SPAStaticFiles(StaticFiles):
491
+ async def get_response(self, path: str, scope):
492
+ try:
493
+ return await super().get_response(path, scope)
494
+ except (HTTPException, StarletteHTTPException) as ex:
495
+ if ex.status_code == 404:
496
+ if path.endswith(".js"):
497
+ # Return 404 for javascript files
498
+ raise ex
499
+ else:
500
+ return await super().get_response("index.html", scope)
501
+ else:
502
+ raise ex
503
+
504
+
505
+ print(
506
+ rf"""
507
+ ██████╗ ██████╗ ███████╗███╗ ██╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗██╗
508
+ ██╔═══██╗██╔══██╗██╔════╝████╗ ██║ ██║ ██║██╔════╝██╔══██╗██║ ██║██║
509
+ ██║ ██║██████╔╝█████╗ ██╔██╗ ██║ ██║ █╗ ██║█████╗ ██████╔╝██║ ██║██║
510
+ ██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║ ██║███╗██║██╔══╝ ██╔══██╗██║ ██║██║
511
+ ╚██████╔╝██║ ███████╗██║ ╚████║ ╚███╔███╔╝███████╗██████╔╝╚██████╔╝██║
512
+ ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚══╝╚══╝ ╚══════╝╚═════╝ ╚═════╝ ╚═╝
513
+
514
+
515
+ v{VERSION} - building the best AI user interface.
516
+ {f"Commit: {WEBUI_BUILD_HASH}" if WEBUI_BUILD_HASH != "dev-build" else ""}
517
+ https://github.com/open-webui/open-webui
518
+ """
519
+ )
520
+
521
+
522
+ @asynccontextmanager
523
+ async def lifespan(app: FastAPI):
524
+ app.state.instance_id = INSTANCE_ID
525
+ start_logger()
526
+
527
+ if RESET_CONFIG_ON_START:
528
+ reset_config()
529
+
530
+ if LICENSE_KEY:
531
+ get_license_data(app, LICENSE_KEY)
532
+
533
+ # This should be blocking (sync) so functions are not deactivated on first /get_models calls
534
+ # when the first user lands on the / route.
535
+ log.info("Installing external dependencies of functions and tools...")
536
+ install_tool_and_function_dependencies()
537
+
538
+ app.state.redis = get_redis_connection(
539
+ redis_url=REDIS_URL,
540
+ redis_sentinels=get_sentinels_from_env(
541
+ REDIS_SENTINEL_HOSTS, REDIS_SENTINEL_PORT
542
+ ),
543
+ redis_cluster=REDIS_CLUSTER,
544
+ async_mode=True,
545
+ )
546
+
547
+ if app.state.redis is not None:
548
+ app.state.redis_task_command_listener = asyncio.create_task(
549
+ redis_task_command_listener(app)
550
+ )
551
+
552
+ if THREAD_POOL_SIZE and THREAD_POOL_SIZE > 0:
553
+ limiter = anyio.to_thread.current_default_thread_limiter()
554
+ limiter.total_tokens = THREAD_POOL_SIZE
555
+
556
+ asyncio.create_task(periodic_usage_pool_cleanup())
557
+
558
+ if app.state.config.ENABLE_BASE_MODELS_CACHE:
559
+ await get_all_models(
560
+ Request(
561
+ # Creating a mock request object to pass to get_all_models
562
+ {
563
+ "type": "http",
564
+ "asgi.version": "3.0",
565
+ "asgi.spec_version": "2.0",
566
+ "method": "GET",
567
+ "path": "/internal",
568
+ "query_string": b"",
569
+ "headers": Headers({}).raw,
570
+ "client": ("127.0.0.1", 12345),
571
+ "server": ("127.0.0.1", 80),
572
+ "scheme": "http",
573
+ "app": app,
574
+ }
575
+ ),
576
+ None,
577
+ )
578
+
579
+ yield
580
+
581
+ if hasattr(app.state, "redis_task_command_listener"):
582
+ app.state.redis_task_command_listener.cancel()
583
+
584
+
585
+ app = FastAPI(
586
+ title="Open WebUI",
587
+ docs_url="/docs" if ENV == "dev" else None,
588
+ openapi_url="/openapi.json" if ENV == "dev" else None,
589
+ redoc_url=None,
590
+ lifespan=lifespan,
591
+ )
592
+
593
+ oauth_manager = OAuthManager(app)
594
+
595
+ app.state.instance_id = None
596
+ app.state.config = AppConfig(
597
+ redis_url=REDIS_URL,
598
+ redis_sentinels=get_sentinels_from_env(REDIS_SENTINEL_HOSTS, REDIS_SENTINEL_PORT),
599
+ redis_cluster=REDIS_CLUSTER,
600
+ redis_key_prefix=REDIS_KEY_PREFIX,
601
+ )
602
+ app.state.redis = None
603
+
604
+ app.state.WEBUI_NAME = WEBUI_NAME
605
+ app.state.LICENSE_METADATA = None
606
+
607
+
608
+ ########################################
609
+ #
610
+ # OPENTELEMETRY
611
+ #
612
+ ########################################
613
+
614
+ if ENABLE_OTEL:
615
+ from open_webui.utils.telemetry.setup import setup as setup_opentelemetry
616
+
617
+ setup_opentelemetry(app=app, db_engine=engine)
618
+
619
+
620
+ ########################################
621
+ #
622
+ # OLLAMA
623
+ #
624
+ ########################################
625
+
626
+
627
+ app.state.config.ENABLE_OLLAMA_API = ENABLE_OLLAMA_API
628
+ app.state.config.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS
629
+ app.state.config.OLLAMA_API_CONFIGS = OLLAMA_API_CONFIGS
630
+
631
+ app.state.OLLAMA_MODELS = {}
632
+
633
+ ########################################
634
+ #
635
+ # OPENAI
636
+ #
637
+ ########################################
638
+
639
+ app.state.config.ENABLE_OPENAI_API = ENABLE_OPENAI_API
640
+ app.state.config.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS
641
+ app.state.config.OPENAI_API_KEYS = OPENAI_API_KEYS
642
+ app.state.config.OPENAI_API_CONFIGS = OPENAI_API_CONFIGS
643
+
644
+ app.state.OPENAI_MODELS = {}
645
+
646
+ ########################################
647
+ #
648
+ # TOOL SERVERS
649
+ #
650
+ ########################################
651
+
652
+ app.state.config.TOOL_SERVER_CONNECTIONS = TOOL_SERVER_CONNECTIONS
653
+ app.state.TOOL_SERVERS = []
654
+
655
+ ########################################
656
+ #
657
+ # DIRECT CONNECTIONS
658
+ #
659
+ ########################################
660
+
661
+ app.state.config.ENABLE_DIRECT_CONNECTIONS = ENABLE_DIRECT_CONNECTIONS
662
+
663
+ ########################################
664
+ #
665
+ # SCIM
666
+ #
667
+ ########################################
668
+
669
+ app.state.SCIM_ENABLED = SCIM_ENABLED
670
+ app.state.SCIM_TOKEN = SCIM_TOKEN
671
+
672
+ ########################################
673
+ #
674
+ # MODELS
675
+ #
676
+ ########################################
677
+
678
+ app.state.config.ENABLE_BASE_MODELS_CACHE = ENABLE_BASE_MODELS_CACHE
679
+ app.state.BASE_MODELS = []
680
+
681
+ ########################################
682
+ #
683
+ # WEBUI
684
+ #
685
+ ########################################
686
+
687
+ app.state.config.WEBUI_URL = WEBUI_URL
688
+ app.state.config.ENABLE_SIGNUP = ENABLE_SIGNUP
689
+ app.state.config.ENABLE_LOGIN_FORM = ENABLE_LOGIN_FORM
690
+
691
+ app.state.config.ENABLE_API_KEY = ENABLE_API_KEY
692
+ app.state.config.ENABLE_API_KEY_ENDPOINT_RESTRICTIONS = (
693
+ ENABLE_API_KEY_ENDPOINT_RESTRICTIONS
694
+ )
695
+ app.state.config.API_KEY_ALLOWED_ENDPOINTS = API_KEY_ALLOWED_ENDPOINTS
696
+
697
+ app.state.config.JWT_EXPIRES_IN = JWT_EXPIRES_IN
698
+
699
+ app.state.config.SHOW_ADMIN_DETAILS = SHOW_ADMIN_DETAILS
700
+ app.state.config.ADMIN_EMAIL = ADMIN_EMAIL
701
+
702
+
703
+ app.state.config.DEFAULT_MODELS = DEFAULT_MODELS
704
+ app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
705
+ app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
706
+
707
+ app.state.config.PENDING_USER_OVERLAY_CONTENT = PENDING_USER_OVERLAY_CONTENT
708
+ app.state.config.PENDING_USER_OVERLAY_TITLE = PENDING_USER_OVERLAY_TITLE
709
+
710
+ app.state.config.RESPONSE_WATERMARK = RESPONSE_WATERMARK
711
+
712
+ app.state.config.USER_PERMISSIONS = USER_PERMISSIONS
713
+ app.state.config.WEBHOOK_URL = WEBHOOK_URL
714
+ app.state.config.BANNERS = WEBUI_BANNERS
715
+ app.state.config.MODEL_ORDER_LIST = MODEL_ORDER_LIST
716
+
717
+
718
+ app.state.config.ENABLE_CHANNELS = ENABLE_CHANNELS
719
+ app.state.config.ENABLE_NOTES = ENABLE_NOTES
720
+ app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING
721
+ app.state.config.ENABLE_MESSAGE_RATING = ENABLE_MESSAGE_RATING
722
+ app.state.config.ENABLE_USER_WEBHOOKS = ENABLE_USER_WEBHOOKS
723
+
724
+ app.state.config.ENABLE_EVALUATION_ARENA_MODELS = ENABLE_EVALUATION_ARENA_MODELS
725
+ app.state.config.EVALUATION_ARENA_MODELS = EVALUATION_ARENA_MODELS
726
+
727
+ app.state.config.OAUTH_USERNAME_CLAIM = OAUTH_USERNAME_CLAIM
728
+ app.state.config.OAUTH_PICTURE_CLAIM = OAUTH_PICTURE_CLAIM
729
+ app.state.config.OAUTH_EMAIL_CLAIM = OAUTH_EMAIL_CLAIM
730
+
731
+ app.state.config.ENABLE_OAUTH_ROLE_MANAGEMENT = ENABLE_OAUTH_ROLE_MANAGEMENT
732
+ app.state.config.OAUTH_ROLES_CLAIM = OAUTH_ROLES_CLAIM
733
+ app.state.config.OAUTH_ALLOWED_ROLES = OAUTH_ALLOWED_ROLES
734
+ app.state.config.OAUTH_ADMIN_ROLES = OAUTH_ADMIN_ROLES
735
+
736
+ app.state.config.ENABLE_LDAP = ENABLE_LDAP
737
+ app.state.config.LDAP_SERVER_LABEL = LDAP_SERVER_LABEL
738
+ app.state.config.LDAP_SERVER_HOST = LDAP_SERVER_HOST
739
+ app.state.config.LDAP_SERVER_PORT = LDAP_SERVER_PORT
740
+ app.state.config.LDAP_ATTRIBUTE_FOR_MAIL = LDAP_ATTRIBUTE_FOR_MAIL
741
+ app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME = LDAP_ATTRIBUTE_FOR_USERNAME
742
+ app.state.config.LDAP_APP_DN = LDAP_APP_DN
743
+ app.state.config.LDAP_APP_PASSWORD = LDAP_APP_PASSWORD
744
+ app.state.config.LDAP_SEARCH_BASE = LDAP_SEARCH_BASE
745
+ app.state.config.LDAP_SEARCH_FILTERS = LDAP_SEARCH_FILTERS
746
+ app.state.config.LDAP_USE_TLS = LDAP_USE_TLS
747
+ app.state.config.LDAP_CA_CERT_FILE = LDAP_CA_CERT_FILE
748
+ app.state.config.LDAP_VALIDATE_CERT = LDAP_VALIDATE_CERT
749
+ app.state.config.LDAP_CIPHERS = LDAP_CIPHERS
750
+
751
+ # For LDAP Group Management
752
+ app.state.config.ENABLE_LDAP_GROUP_MANAGEMENT = ENABLE_LDAP_GROUP_MANAGEMENT
753
+ app.state.config.ENABLE_LDAP_GROUP_CREATION = ENABLE_LDAP_GROUP_CREATION
754
+ app.state.config.LDAP_ATTRIBUTE_FOR_GROUPS = LDAP_ATTRIBUTE_FOR_GROUPS
755
+
756
+
757
+ app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
758
+ app.state.AUTH_TRUSTED_NAME_HEADER = WEBUI_AUTH_TRUSTED_NAME_HEADER
759
+ app.state.WEBUI_AUTH_SIGNOUT_REDIRECT_URL = WEBUI_AUTH_SIGNOUT_REDIRECT_URL
760
+ app.state.EXTERNAL_PWA_MANIFEST_URL = EXTERNAL_PWA_MANIFEST_URL
761
+
762
+ app.state.USER_COUNT = None
763
+
764
+ app.state.TOOLS = {}
765
+ app.state.TOOL_CONTENTS = {}
766
+
767
+ app.state.FUNCTIONS = {}
768
+ app.state.FUNCTION_CONTENTS = {}
769
+
770
+ ########################################
771
+ #
772
+ # RETRIEVAL
773
+ #
774
+ ########################################
775
+
776
+
777
+ app.state.config.TOP_K = RAG_TOP_K
778
+ app.state.config.TOP_K_RERANKER = RAG_TOP_K_RERANKER
779
+ app.state.config.RELEVANCE_THRESHOLD = RAG_RELEVANCE_THRESHOLD
780
+ app.state.config.HYBRID_BM25_WEIGHT = RAG_HYBRID_BM25_WEIGHT
781
+
782
+
783
+ app.state.config.ALLOWED_FILE_EXTENSIONS = RAG_ALLOWED_FILE_EXTENSIONS
784
+ app.state.config.FILE_MAX_SIZE = RAG_FILE_MAX_SIZE
785
+ app.state.config.FILE_MAX_COUNT = RAG_FILE_MAX_COUNT
786
+ app.state.config.FILE_IMAGE_COMPRESSION_WIDTH = FILE_IMAGE_COMPRESSION_WIDTH
787
+ app.state.config.FILE_IMAGE_COMPRESSION_HEIGHT = FILE_IMAGE_COMPRESSION_HEIGHT
788
+
789
+
790
+ app.state.config.RAG_FULL_CONTEXT = RAG_FULL_CONTEXT
791
+ app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL = BYPASS_EMBEDDING_AND_RETRIEVAL
792
+ app.state.config.ENABLE_RAG_HYBRID_SEARCH = ENABLE_RAG_HYBRID_SEARCH
793
+ app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION = ENABLE_WEB_LOADER_SSL_VERIFICATION
794
+
795
+ app.state.config.CONTENT_EXTRACTION_ENGINE = CONTENT_EXTRACTION_ENGINE
796
+ app.state.config.DATALAB_MARKER_API_KEY = DATALAB_MARKER_API_KEY
797
+ app.state.config.DATALAB_MARKER_API_BASE_URL = DATALAB_MARKER_API_BASE_URL
798
+ app.state.config.DATALAB_MARKER_ADDITIONAL_CONFIG = DATALAB_MARKER_ADDITIONAL_CONFIG
799
+ app.state.config.DATALAB_MARKER_SKIP_CACHE = DATALAB_MARKER_SKIP_CACHE
800
+ app.state.config.DATALAB_MARKER_FORCE_OCR = DATALAB_MARKER_FORCE_OCR
801
+ app.state.config.DATALAB_MARKER_PAGINATE = DATALAB_MARKER_PAGINATE
802
+ app.state.config.DATALAB_MARKER_STRIP_EXISTING_OCR = DATALAB_MARKER_STRIP_EXISTING_OCR
803
+ app.state.config.DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION = (
804
+ DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION
805
+ )
806
+ app.state.config.DATALAB_MARKER_FORMAT_LINES = DATALAB_MARKER_FORMAT_LINES
807
+ app.state.config.DATALAB_MARKER_USE_LLM = DATALAB_MARKER_USE_LLM
808
+ app.state.config.DATALAB_MARKER_OUTPUT_FORMAT = DATALAB_MARKER_OUTPUT_FORMAT
809
+ app.state.config.EXTERNAL_DOCUMENT_LOADER_URL = EXTERNAL_DOCUMENT_LOADER_URL
810
+ app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY = EXTERNAL_DOCUMENT_LOADER_API_KEY
811
+ app.state.config.TIKA_SERVER_URL = TIKA_SERVER_URL
812
+ app.state.config.DOCLING_SERVER_URL = DOCLING_SERVER_URL
813
+ app.state.config.DOCLING_OCR_ENGINE = DOCLING_OCR_ENGINE
814
+ app.state.config.DOCLING_OCR_LANG = DOCLING_OCR_LANG
815
+ app.state.config.DOCLING_DO_PICTURE_DESCRIPTION = DOCLING_DO_PICTURE_DESCRIPTION
816
+ app.state.config.DOCLING_PICTURE_DESCRIPTION_MODE = DOCLING_PICTURE_DESCRIPTION_MODE
817
+ app.state.config.DOCLING_PICTURE_DESCRIPTION_LOCAL = DOCLING_PICTURE_DESCRIPTION_LOCAL
818
+ app.state.config.DOCLING_PICTURE_DESCRIPTION_API = DOCLING_PICTURE_DESCRIPTION_API
819
+ app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT = DOCUMENT_INTELLIGENCE_ENDPOINT
820
+ app.state.config.DOCUMENT_INTELLIGENCE_KEY = DOCUMENT_INTELLIGENCE_KEY
821
+ app.state.config.MISTRAL_OCR_API_KEY = MISTRAL_OCR_API_KEY
822
+
823
+ app.state.config.TEXT_SPLITTER = RAG_TEXT_SPLITTER
824
+ app.state.config.TIKTOKEN_ENCODING_NAME = TIKTOKEN_ENCODING_NAME
825
+
826
+ app.state.config.CHUNK_SIZE = CHUNK_SIZE
827
+ app.state.config.CHUNK_OVERLAP = CHUNK_OVERLAP
828
+
829
+ app.state.config.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE
830
+ app.state.config.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
831
+ app.state.config.RAG_EMBEDDING_BATCH_SIZE = RAG_EMBEDDING_BATCH_SIZE
832
+
833
+ app.state.config.RAG_RERANKING_ENGINE = RAG_RERANKING_ENGINE
834
+ app.state.config.RAG_RERANKING_MODEL = RAG_RERANKING_MODEL
835
+ app.state.config.RAG_EXTERNAL_RERANKER_URL = RAG_EXTERNAL_RERANKER_URL
836
+ app.state.config.RAG_EXTERNAL_RERANKER_API_KEY = RAG_EXTERNAL_RERANKER_API_KEY
837
+
838
+ app.state.config.RAG_TEMPLATE = RAG_TEMPLATE
839
+
840
+ app.state.config.RAG_OPENAI_API_BASE_URL = RAG_OPENAI_API_BASE_URL
841
+ app.state.config.RAG_OPENAI_API_KEY = RAG_OPENAI_API_KEY
842
+
843
+ app.state.config.RAG_AZURE_OPENAI_BASE_URL = RAG_AZURE_OPENAI_BASE_URL
844
+ app.state.config.RAG_AZURE_OPENAI_API_KEY = RAG_AZURE_OPENAI_API_KEY
845
+ app.state.config.RAG_AZURE_OPENAI_API_VERSION = RAG_AZURE_OPENAI_API_VERSION
846
+
847
+ app.state.config.RAG_OLLAMA_BASE_URL = RAG_OLLAMA_BASE_URL
848
+ app.state.config.RAG_OLLAMA_API_KEY = RAG_OLLAMA_API_KEY
849
+
850
+ app.state.config.PDF_EXTRACT_IMAGES = PDF_EXTRACT_IMAGES
851
+
852
+ app.state.config.YOUTUBE_LOADER_LANGUAGE = YOUTUBE_LOADER_LANGUAGE
853
+ app.state.config.YOUTUBE_LOADER_PROXY_URL = YOUTUBE_LOADER_PROXY_URL
854
+
855
+
856
+ app.state.config.ENABLE_WEB_SEARCH = ENABLE_WEB_SEARCH
857
+ app.state.config.WEB_SEARCH_ENGINE = WEB_SEARCH_ENGINE
858
+ app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST = WEB_SEARCH_DOMAIN_FILTER_LIST
859
+ app.state.config.WEB_SEARCH_RESULT_COUNT = WEB_SEARCH_RESULT_COUNT
860
+ app.state.config.WEB_SEARCH_CONCURRENT_REQUESTS = WEB_SEARCH_CONCURRENT_REQUESTS
861
+
862
+ app.state.config.WEB_LOADER_ENGINE = WEB_LOADER_ENGINE
863
+ app.state.config.WEB_LOADER_CONCURRENT_REQUESTS = WEB_LOADER_CONCURRENT_REQUESTS
864
+
865
+ app.state.config.WEB_SEARCH_TRUST_ENV = WEB_SEARCH_TRUST_ENV
866
+ app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = (
867
+ BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL
868
+ )
869
+ app.state.config.BYPASS_WEB_SEARCH_WEB_LOADER = BYPASS_WEB_SEARCH_WEB_LOADER
870
+
871
+ app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = ENABLE_GOOGLE_DRIVE_INTEGRATION
872
+ app.state.config.ENABLE_ONEDRIVE_INTEGRATION = ENABLE_ONEDRIVE_INTEGRATION
873
+ app.state.config.SEARXNG_QUERY_URL = SEARXNG_QUERY_URL
874
+ app.state.config.YACY_QUERY_URL = YACY_QUERY_URL
875
+ app.state.config.YACY_USERNAME = YACY_USERNAME
876
+ app.state.config.YACY_PASSWORD = YACY_PASSWORD
877
+ app.state.config.GOOGLE_PSE_API_KEY = GOOGLE_PSE_API_KEY
878
+ app.state.config.GOOGLE_PSE_ENGINE_ID = GOOGLE_PSE_ENGINE_ID
879
+ app.state.config.BRAVE_SEARCH_API_KEY = BRAVE_SEARCH_API_KEY
880
+ app.state.config.KAGI_SEARCH_API_KEY = KAGI_SEARCH_API_KEY
881
+ app.state.config.MOJEEK_SEARCH_API_KEY = MOJEEK_SEARCH_API_KEY
882
+ app.state.config.BOCHA_SEARCH_API_KEY = BOCHA_SEARCH_API_KEY
883
+ app.state.config.SERPSTACK_API_KEY = SERPSTACK_API_KEY
884
+ app.state.config.SERPSTACK_HTTPS = SERPSTACK_HTTPS
885
+ app.state.config.SERPER_API_KEY = SERPER_API_KEY
886
+ app.state.config.SERPLY_API_KEY = SERPLY_API_KEY
887
+ app.state.config.TAVILY_API_KEY = TAVILY_API_KEY
888
+ app.state.config.SEARCHAPI_API_KEY = SEARCHAPI_API_KEY
889
+ app.state.config.SEARCHAPI_ENGINE = SEARCHAPI_ENGINE
890
+ app.state.config.SERPAPI_API_KEY = SERPAPI_API_KEY
891
+ app.state.config.SERPAPI_ENGINE = SERPAPI_ENGINE
892
+ app.state.config.JINA_API_KEY = JINA_API_KEY
893
+ app.state.config.BING_SEARCH_V7_ENDPOINT = BING_SEARCH_V7_ENDPOINT
894
+ app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY = BING_SEARCH_V7_SUBSCRIPTION_KEY
895
+ app.state.config.EXA_API_KEY = EXA_API_KEY
896
+ app.state.config.PERPLEXITY_API_KEY = PERPLEXITY_API_KEY
897
+ app.state.config.PERPLEXITY_MODEL = PERPLEXITY_MODEL
898
+ app.state.config.PERPLEXITY_SEARCH_CONTEXT_USAGE = PERPLEXITY_SEARCH_CONTEXT_USAGE
899
+ app.state.config.SOUGOU_API_SID = SOUGOU_API_SID
900
+ app.state.config.SOUGOU_API_SK = SOUGOU_API_SK
901
+ app.state.config.EXTERNAL_WEB_SEARCH_URL = EXTERNAL_WEB_SEARCH_URL
902
+ app.state.config.EXTERNAL_WEB_SEARCH_API_KEY = EXTERNAL_WEB_SEARCH_API_KEY
903
+ app.state.config.EXTERNAL_WEB_LOADER_URL = EXTERNAL_WEB_LOADER_URL
904
+ app.state.config.EXTERNAL_WEB_LOADER_API_KEY = EXTERNAL_WEB_LOADER_API_KEY
905
+
906
+
907
+ app.state.config.PLAYWRIGHT_WS_URL = PLAYWRIGHT_WS_URL
908
+ app.state.config.PLAYWRIGHT_TIMEOUT = PLAYWRIGHT_TIMEOUT
909
+ app.state.config.FIRECRAWL_API_BASE_URL = FIRECRAWL_API_BASE_URL
910
+ app.state.config.FIRECRAWL_API_KEY = FIRECRAWL_API_KEY
911
+ app.state.config.TAVILY_EXTRACT_DEPTH = TAVILY_EXTRACT_DEPTH
912
+
913
+ app.state.EMBEDDING_FUNCTION = None
914
+ app.state.RERANKING_FUNCTION = None
915
+ app.state.ef = None
916
+ app.state.rf = None
917
+
918
+ app.state.YOUTUBE_LOADER_TRANSLATION = None
919
+
920
+
921
+ try:
922
+ app.state.ef = get_ef(
923
+ app.state.config.RAG_EMBEDDING_ENGINE,
924
+ app.state.config.RAG_EMBEDDING_MODEL,
925
+ RAG_EMBEDDING_MODEL_AUTO_UPDATE,
926
+ )
927
+ if (
928
+ app.state.config.ENABLE_RAG_HYBRID_SEARCH
929
+ and not app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL
930
+ ):
931
+ app.state.rf = get_rf(
932
+ app.state.config.RAG_RERANKING_ENGINE,
933
+ app.state.config.RAG_RERANKING_MODEL,
934
+ app.state.config.RAG_EXTERNAL_RERANKER_URL,
935
+ app.state.config.RAG_EXTERNAL_RERANKER_API_KEY,
936
+ RAG_RERANKING_MODEL_AUTO_UPDATE,
937
+ )
938
+ else:
939
+ app.state.rf = None
940
+ except Exception as e:
941
+ log.error(f"Error updating models: {e}")
942
+ pass
943
+
944
+
945
+ app.state.EMBEDDING_FUNCTION = get_embedding_function(
946
+ app.state.config.RAG_EMBEDDING_ENGINE,
947
+ app.state.config.RAG_EMBEDDING_MODEL,
948
+ embedding_function=app.state.ef,
949
+ url=(
950
+ app.state.config.RAG_OPENAI_API_BASE_URL
951
+ if app.state.config.RAG_EMBEDDING_ENGINE == "openai"
952
+ else (
953
+ app.state.config.RAG_OLLAMA_BASE_URL
954
+ if app.state.config.RAG_EMBEDDING_ENGINE == "ollama"
955
+ else app.state.config.RAG_AZURE_OPENAI_BASE_URL
956
+ )
957
+ ),
958
+ key=(
959
+ app.state.config.RAG_OPENAI_API_KEY
960
+ if app.state.config.RAG_EMBEDDING_ENGINE == "openai"
961
+ else (
962
+ app.state.config.RAG_OLLAMA_API_KEY
963
+ if app.state.config.RAG_EMBEDDING_ENGINE == "ollama"
964
+ else app.state.config.RAG_AZURE_OPENAI_API_KEY
965
+ )
966
+ ),
967
+ embedding_batch_size=app.state.config.RAG_EMBEDDING_BATCH_SIZE,
968
+ azure_api_version=(
969
+ app.state.config.RAG_AZURE_OPENAI_API_VERSION
970
+ if app.state.config.RAG_EMBEDDING_ENGINE == "azure_openai"
971
+ else None
972
+ ),
973
+ )
974
+
975
+ app.state.RERANKING_FUNCTION = get_reranking_function(
976
+ app.state.config.RAG_RERANKING_ENGINE,
977
+ app.state.config.RAG_RERANKING_MODEL,
978
+ reranking_function=app.state.rf,
979
+ )
980
+
981
+ ########################################
982
+ #
983
+ # CODE EXECUTION
984
+ #
985
+ ########################################
986
+
987
+ app.state.config.ENABLE_CODE_EXECUTION = ENABLE_CODE_EXECUTION
988
+ app.state.config.CODE_EXECUTION_ENGINE = CODE_EXECUTION_ENGINE
989
+ app.state.config.CODE_EXECUTION_JUPYTER_URL = CODE_EXECUTION_JUPYTER_URL
990
+ app.state.config.CODE_EXECUTION_JUPYTER_AUTH = CODE_EXECUTION_JUPYTER_AUTH
991
+ app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN = CODE_EXECUTION_JUPYTER_AUTH_TOKEN
992
+ app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = (
993
+ CODE_EXECUTION_JUPYTER_AUTH_PASSWORD
994
+ )
995
+ app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT = CODE_EXECUTION_JUPYTER_TIMEOUT
996
+
997
+ app.state.config.ENABLE_CODE_INTERPRETER = ENABLE_CODE_INTERPRETER
998
+ app.state.config.CODE_INTERPRETER_ENGINE = CODE_INTERPRETER_ENGINE
999
+ app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE = CODE_INTERPRETER_PROMPT_TEMPLATE
1000
+
1001
+ app.state.config.CODE_INTERPRETER_JUPYTER_URL = CODE_INTERPRETER_JUPYTER_URL
1002
+ app.state.config.CODE_INTERPRETER_JUPYTER_AUTH = CODE_INTERPRETER_JUPYTER_AUTH
1003
+ app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN = (
1004
+ CODE_INTERPRETER_JUPYTER_AUTH_TOKEN
1005
+ )
1006
+ app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = (
1007
+ CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD
1008
+ )
1009
+ app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT = CODE_INTERPRETER_JUPYTER_TIMEOUT
1010
+
1011
+ ########################################
1012
+ #
1013
+ # IMAGES
1014
+ #
1015
+ ########################################
1016
+
1017
+ app.state.config.IMAGE_GENERATION_ENGINE = IMAGE_GENERATION_ENGINE
1018
+ app.state.config.ENABLE_IMAGE_GENERATION = ENABLE_IMAGE_GENERATION
1019
+ app.state.config.ENABLE_IMAGE_PROMPT_GENERATION = ENABLE_IMAGE_PROMPT_GENERATION
1020
+
1021
+ app.state.config.IMAGES_OPENAI_API_BASE_URL = IMAGES_OPENAI_API_BASE_URL
1022
+ app.state.config.IMAGES_OPENAI_API_KEY = IMAGES_OPENAI_API_KEY
1023
+
1024
+ app.state.config.IMAGES_GEMINI_API_BASE_URL = IMAGES_GEMINI_API_BASE_URL
1025
+ app.state.config.IMAGES_GEMINI_API_KEY = IMAGES_GEMINI_API_KEY
1026
+
1027
+ app.state.config.IMAGE_GENERATION_MODEL = IMAGE_GENERATION_MODEL
1028
+
1029
+ app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
1030
+ app.state.config.AUTOMATIC1111_API_AUTH = AUTOMATIC1111_API_AUTH
1031
+ app.state.config.AUTOMATIC1111_CFG_SCALE = AUTOMATIC1111_CFG_SCALE
1032
+ app.state.config.AUTOMATIC1111_SAMPLER = AUTOMATIC1111_SAMPLER
1033
+ app.state.config.AUTOMATIC1111_SCHEDULER = AUTOMATIC1111_SCHEDULER
1034
+ app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
1035
+ app.state.config.COMFYUI_API_KEY = COMFYUI_API_KEY
1036
+ app.state.config.COMFYUI_WORKFLOW = COMFYUI_WORKFLOW
1037
+ app.state.config.COMFYUI_WORKFLOW_NODES = COMFYUI_WORKFLOW_NODES
1038
+
1039
+ app.state.config.IMAGE_SIZE = IMAGE_SIZE
1040
+ app.state.config.IMAGE_STEPS = IMAGE_STEPS
1041
+
1042
+
1043
+ ########################################
1044
+ #
1045
+ # AUDIO
1046
+ #
1047
+ ########################################
1048
+
1049
+ app.state.config.STT_ENGINE = AUDIO_STT_ENGINE
1050
+ app.state.config.STT_MODEL = AUDIO_STT_MODEL
1051
+ app.state.config.STT_SUPPORTED_CONTENT_TYPES = AUDIO_STT_SUPPORTED_CONTENT_TYPES
1052
+
1053
+ app.state.config.STT_OPENAI_API_BASE_URL = AUDIO_STT_OPENAI_API_BASE_URL
1054
+ app.state.config.STT_OPENAI_API_KEY = AUDIO_STT_OPENAI_API_KEY
1055
+
1056
+ app.state.config.WHISPER_MODEL = WHISPER_MODEL
1057
+ app.state.config.WHISPER_VAD_FILTER = WHISPER_VAD_FILTER
1058
+ app.state.config.DEEPGRAM_API_KEY = DEEPGRAM_API_KEY
1059
+
1060
+ app.state.config.AUDIO_STT_AZURE_API_KEY = AUDIO_STT_AZURE_API_KEY
1061
+ app.state.config.AUDIO_STT_AZURE_REGION = AUDIO_STT_AZURE_REGION
1062
+ app.state.config.AUDIO_STT_AZURE_LOCALES = AUDIO_STT_AZURE_LOCALES
1063
+ app.state.config.AUDIO_STT_AZURE_BASE_URL = AUDIO_STT_AZURE_BASE_URL
1064
+ app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS = AUDIO_STT_AZURE_MAX_SPEAKERS
1065
+
1066
+ app.state.config.TTS_OPENAI_API_BASE_URL = AUDIO_TTS_OPENAI_API_BASE_URL
1067
+ app.state.config.TTS_OPENAI_API_KEY = AUDIO_TTS_OPENAI_API_KEY
1068
+ app.state.config.TTS_ENGINE = AUDIO_TTS_ENGINE
1069
+ app.state.config.TTS_MODEL = AUDIO_TTS_MODEL
1070
+ app.state.config.TTS_VOICE = AUDIO_TTS_VOICE
1071
+ app.state.config.TTS_API_KEY = AUDIO_TTS_API_KEY
1072
+ app.state.config.TTS_SPLIT_ON = AUDIO_TTS_SPLIT_ON
1073
+
1074
+
1075
+ app.state.config.TTS_AZURE_SPEECH_REGION = AUDIO_TTS_AZURE_SPEECH_REGION
1076
+ app.state.config.TTS_AZURE_SPEECH_BASE_URL = AUDIO_TTS_AZURE_SPEECH_BASE_URL
1077
+ app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT = AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT
1078
+
1079
+
1080
+ app.state.faster_whisper_model = None
1081
+ app.state.speech_synthesiser = None
1082
+ app.state.speech_speaker_embeddings_dataset = None
1083
+
1084
+
1085
+ ########################################
1086
+ #
1087
+ # TASKS
1088
+ #
1089
+ ########################################
1090
+
1091
+
1092
+ app.state.config.TASK_MODEL = TASK_MODEL
1093
+ app.state.config.TASK_MODEL_EXTERNAL = TASK_MODEL_EXTERNAL
1094
+
1095
+
1096
+ app.state.config.ENABLE_SEARCH_QUERY_GENERATION = ENABLE_SEARCH_QUERY_GENERATION
1097
+ app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION = ENABLE_RETRIEVAL_QUERY_GENERATION
1098
+ app.state.config.ENABLE_AUTOCOMPLETE_GENERATION = ENABLE_AUTOCOMPLETE_GENERATION
1099
+ app.state.config.ENABLE_TAGS_GENERATION = ENABLE_TAGS_GENERATION
1100
+ app.state.config.ENABLE_TITLE_GENERATION = ENABLE_TITLE_GENERATION
1101
+ app.state.config.ENABLE_FOLLOW_UP_GENERATION = ENABLE_FOLLOW_UP_GENERATION
1102
+
1103
+
1104
+ app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = TITLE_GENERATION_PROMPT_TEMPLATE
1105
+ app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE = TAGS_GENERATION_PROMPT_TEMPLATE
1106
+ app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE = (
1107
+ IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE
1108
+ )
1109
+ app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE = (
1110
+ FOLLOW_UP_GENERATION_PROMPT_TEMPLATE
1111
+ )
1112
+
1113
+ app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = (
1114
+ TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE
1115
+ )
1116
+ app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE = QUERY_GENERATION_PROMPT_TEMPLATE
1117
+ app.state.config.AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE = (
1118
+ AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE
1119
+ )
1120
+ app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH = (
1121
+ AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH
1122
+ )
1123
+
1124
+
1125
+ ########################################
1126
+ #
1127
+ # WEBUI
1128
+ #
1129
+ ########################################
1130
+
1131
+ app.state.MODELS = {}
1132
+
1133
+
1134
+ class RedirectMiddleware(BaseHTTPMiddleware):
1135
+ async def dispatch(self, request: Request, call_next):
1136
+ # Check if the request is a GET request
1137
+ if request.method == "GET":
1138
+ path = request.url.path
1139
+ query_params = dict(parse_qs(urlparse(str(request.url)).query))
1140
+
1141
+ # Check for the specific watch path and the presence of 'v' parameter
1142
+ if path.endswith("/watch") and "v" in query_params:
1143
+ # Extract the first 'v' parameter
1144
+ video_id = query_params["v"][0]
1145
+ encoded_video_id = urlencode({"youtube": video_id})
1146
+ redirect_url = f"/?{encoded_video_id}"
1147
+ return RedirectResponse(url=redirect_url)
1148
+
1149
+ # Proceed with the normal flow of other requests
1150
+ response = await call_next(request)
1151
+ return response
1152
+
1153
+
1154
+ # Add the middleware to the app
1155
+ if ENABLE_COMPRESSION_MIDDLEWARE:
1156
+ app.add_middleware(CompressMiddleware)
1157
+
1158
+ app.add_middleware(RedirectMiddleware)
1159
+ app.add_middleware(SecurityHeadersMiddleware)
1160
+
1161
+
1162
+ @app.middleware("http")
1163
+ async def commit_session_after_request(request: Request, call_next):
1164
+ response = await call_next(request)
1165
+ # log.debug("Commit session after request")
1166
+ Session.commit()
1167
+ return response
1168
+
1169
+
1170
+ @app.middleware("http")
1171
+ async def check_url(request: Request, call_next):
1172
+ start_time = int(time.time())
1173
+ request.state.token = get_http_authorization_cred(
1174
+ request.headers.get("Authorization")
1175
+ )
1176
+
1177
+ request.state.enable_api_key = app.state.config.ENABLE_API_KEY
1178
+ response = await call_next(request)
1179
+ process_time = int(time.time()) - start_time
1180
+ response.headers["X-Process-Time"] = str(process_time)
1181
+ return response
1182
+
1183
+
1184
+ @app.middleware("http")
1185
+ async def inspect_websocket(request: Request, call_next):
1186
+ if (
1187
+ "/ws/socket.io" in request.url.path
1188
+ and request.query_params.get("transport") == "websocket"
1189
+ ):
1190
+ upgrade = (request.headers.get("Upgrade") or "").lower()
1191
+ connection = (request.headers.get("Connection") or "").lower().split(",")
1192
+ # Check that there's the correct headers for an upgrade, else reject the connection
1193
+ # This is to work around this upstream issue: https://github.com/miguelgrinberg/python-engineio/issues/367
1194
+ if upgrade != "websocket" or "upgrade" not in connection:
1195
+ return JSONResponse(
1196
+ status_code=status.HTTP_400_BAD_REQUEST,
1197
+ content={"detail": "Invalid WebSocket upgrade request"},
1198
+ )
1199
+ return await call_next(request)
1200
+
1201
+
1202
+ app.add_middleware(
1203
+ CORSMiddleware,
1204
+ allow_origins=CORS_ALLOW_ORIGIN,
1205
+ allow_credentials=True,
1206
+ allow_methods=["*"],
1207
+ allow_headers=["*"],
1208
+ )
1209
+
1210
+
1211
+ app.mount("/ws", socket_app)
1212
+
1213
+
1214
+ app.include_router(ollama.router, prefix="/ollama", tags=["ollama"])
1215
+ app.include_router(openai.router, prefix="/openai", tags=["openai"])
1216
+
1217
+
1218
+ app.include_router(pipelines.router, prefix="/api/v1/pipelines", tags=["pipelines"])
1219
+ app.include_router(tasks.router, prefix="/api/v1/tasks", tags=["tasks"])
1220
+ app.include_router(images.router, prefix="/api/v1/images", tags=["images"])
1221
+
1222
+ app.include_router(audio.router, prefix="/api/v1/audio", tags=["audio"])
1223
+ app.include_router(retrieval.router, prefix="/api/v1/retrieval", tags=["retrieval"])
1224
+
1225
+ app.include_router(configs.router, prefix="/api/v1/configs", tags=["configs"])
1226
+
1227
+ app.include_router(auths.router, prefix="/api/v1/auths", tags=["auths"])
1228
+ app.include_router(users.router, prefix="/api/v1/users", tags=["users"])
1229
+
1230
+
1231
+ app.include_router(channels.router, prefix="/api/v1/channels", tags=["channels"])
1232
+ app.include_router(chats.router, prefix="/api/v1/chats", tags=["chats"])
1233
+ app.include_router(notes.router, prefix="/api/v1/notes", tags=["notes"])
1234
+
1235
+
1236
+ app.include_router(models.router, prefix="/api/v1/models", tags=["models"])
1237
+ app.include_router(knowledge.router, prefix="/api/v1/knowledge", tags=["knowledge"])
1238
+ app.include_router(prompts.router, prefix="/api/v1/prompts", tags=["prompts"])
1239
+ app.include_router(tools.router, prefix="/api/v1/tools", tags=["tools"])
1240
+
1241
+ app.include_router(memories.router, prefix="/api/v1/memories", tags=["memories"])
1242
+ app.include_router(folders.router, prefix="/api/v1/folders", tags=["folders"])
1243
+ app.include_router(groups.router, prefix="/api/v1/groups", tags=["groups"])
1244
+ app.include_router(files.router, prefix="/api/v1/files", tags=["files"])
1245
+ app.include_router(functions.router, prefix="/api/v1/functions", tags=["functions"])
1246
+ app.include_router(
1247
+ evaluations.router, prefix="/api/v1/evaluations", tags=["evaluations"]
1248
+ )
1249
+ app.include_router(utils.router, prefix="/api/v1/utils", tags=["utils"])
1250
+
1251
+ # SCIM 2.0 API for identity management
1252
+ if SCIM_ENABLED:
1253
+ app.include_router(scim.router, prefix="/api/v1/scim/v2", tags=["scim"])
1254
+
1255
+
1256
+ try:
1257
+ audit_level = AuditLevel(AUDIT_LOG_LEVEL)
1258
+ except ValueError as e:
1259
+ logger.error(f"Invalid audit level: {AUDIT_LOG_LEVEL}. Error: {e}")
1260
+ audit_level = AuditLevel.NONE
1261
+
1262
+ if audit_level != AuditLevel.NONE:
1263
+ app.add_middleware(
1264
+ AuditLoggingMiddleware,
1265
+ audit_level=audit_level,
1266
+ excluded_paths=AUDIT_EXCLUDED_PATHS,
1267
+ max_body_size=MAX_BODY_LOG_SIZE,
1268
+ )
1269
+ ##################################
1270
+ #
1271
+ # Chat Endpoints
1272
+ #
1273
+ ##################################
1274
+
1275
+
1276
+ @app.get("/api/models")
1277
+ @app.get("/api/v1/models") # Experimental: Compatibility with OpenAI API
1278
+ async def get_models(
1279
+ request: Request, refresh: bool = False, user=Depends(get_verified_user)
1280
+ ):
1281
+ def get_filtered_models(models, user):
1282
+ filtered_models = []
1283
+ for model in models:
1284
+ if model.get("arena"):
1285
+ if has_access(
1286
+ user.id,
1287
+ type="read",
1288
+ access_control=model.get("info", {})
1289
+ .get("meta", {})
1290
+ .get("access_control", {}),
1291
+ ):
1292
+ filtered_models.append(model)
1293
+ continue
1294
+
1295
+ model_info = Models.get_model_by_id(model["id"])
1296
+ if model_info:
1297
+ if (
1298
+ (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL)
1299
+ or user.id == model_info.user_id
1300
+ or has_access(
1301
+ user.id, type="read", access_control=model_info.access_control
1302
+ )
1303
+ ):
1304
+ filtered_models.append(model)
1305
+
1306
+ return filtered_models
1307
+
1308
+ all_models = await get_all_models(request, refresh=refresh, user=user)
1309
+
1310
+ models = []
1311
+ for model in all_models:
1312
+ # Filter out filter pipelines
1313
+ if "pipeline" in model and model["pipeline"].get("type", None) == "filter":
1314
+ continue
1315
+
1316
+ try:
1317
+ model_tags = [
1318
+ tag.get("name")
1319
+ for tag in model.get("info", {}).get("meta", {}).get("tags", [])
1320
+ ]
1321
+ tags = [tag.get("name") for tag in model.get("tags", [])]
1322
+
1323
+ tags = list(set(model_tags + tags))
1324
+ model["tags"] = [{"name": tag} for tag in tags]
1325
+ except Exception as e:
1326
+ log.debug(f"Error processing model tags: {e}")
1327
+ model["tags"] = []
1328
+ pass
1329
+
1330
+ models.append(model)
1331
+
1332
+ model_order_list = request.app.state.config.MODEL_ORDER_LIST
1333
+ if model_order_list:
1334
+ model_order_dict = {model_id: i for i, model_id in enumerate(model_order_list)}
1335
+ # Sort models by order list priority, with fallback for those not in the list
1336
+ models.sort(
1337
+ key=lambda model: (
1338
+ model_order_dict.get(model.get("id", ""), float("inf")),
1339
+ (model.get("name", "") or ""),
1340
+ )
1341
+ )
1342
+
1343
+ # Filter out models that the user does not have access to
1344
+ if (
1345
+ user.role == "user"
1346
+ or (user.role == "admin" and not BYPASS_ADMIN_ACCESS_CONTROL)
1347
+ ) and not BYPASS_MODEL_ACCESS_CONTROL:
1348
+ models = get_filtered_models(models, user)
1349
+
1350
+ log.debug(
1351
+ f"/api/models returned filtered models accessible to the user: {json.dumps([model.get('id') for model in models])}"
1352
+ )
1353
+ return {"data": models}
1354
+
1355
+
1356
+ @app.get("/api/models/base")
1357
+ async def get_base_models(request: Request, user=Depends(get_admin_user)):
1358
+ models = await get_all_base_models(request, user=user)
1359
+ return {"data": models}
1360
+
1361
+
1362
+ ##################################
1363
+ # Embeddings
1364
+ ##################################
1365
+
1366
+
1367
+ @app.post("/api/embeddings")
1368
+ @app.post("/api/v1/embeddings") # Experimental: Compatibility with OpenAI API
1369
+ async def embeddings(
1370
+ request: Request, form_data: dict, user=Depends(get_verified_user)
1371
+ ):
1372
+ """
1373
+ OpenAI-compatible embeddings endpoint.
1374
+
1375
+ This handler:
1376
+ - Performs user/model checks and dispatches to the correct backend.
1377
+ - Supports OpenAI, Ollama, arena models, pipelines, and any compatible provider.
1378
+
1379
+ Args:
1380
+ request (Request): Request context.
1381
+ form_data (dict): OpenAI-like payload (e.g., {"model": "...", "input": [...]})
1382
+ user (UserModel): Authenticated user.
1383
+
1384
+ Returns:
1385
+ dict: OpenAI-compatible embeddings response.
1386
+ """
1387
+ # Make sure models are loaded in app state
1388
+ if not request.app.state.MODELS:
1389
+ await get_all_models(request, user=user)
1390
+ # Use generic dispatcher in utils.embeddings
1391
+ return await generate_embeddings(request, form_data, user)
1392
+
1393
+
1394
+ @app.post("/api/chat/completions")
1395
+ @app.post("/api/v1/chat/completions") # Experimental: Compatibility with OpenAI API
1396
+ async def chat_completion(
1397
+ request: Request,
1398
+ form_data: dict,
1399
+ user=Depends(get_verified_user),
1400
+ ):
1401
+ if not request.app.state.MODELS:
1402
+ await get_all_models(request, user=user)
1403
+
1404
+ model_id = form_data.get("model", None)
1405
+ model_item = form_data.pop("model_item", {})
1406
+ tasks = form_data.pop("background_tasks", None)
1407
+
1408
+ metadata = {}
1409
+ try:
1410
+ if not model_item.get("direct", False):
1411
+ if model_id not in request.app.state.MODELS:
1412
+ raise Exception("Model not found")
1413
+
1414
+ model = request.app.state.MODELS[model_id]
1415
+ model_info = Models.get_model_by_id(model_id)
1416
+
1417
+ # Check if user has access to the model
1418
+ if not BYPASS_MODEL_ACCESS_CONTROL and (
1419
+ user.role != "admin" or not BYPASS_ADMIN_ACCESS_CONTROL
1420
+ ):
1421
+ try:
1422
+ check_model_access(user, model)
1423
+ except Exception as e:
1424
+ raise e
1425
+ else:
1426
+ model = model_item
1427
+ model_info = None
1428
+
1429
+ request.state.direct = True
1430
+ request.state.model = model
1431
+
1432
+ model_info_params = (
1433
+ model_info.params.model_dump() if model_info and model_info.params else {}
1434
+ )
1435
+
1436
+ # Chat Params
1437
+ stream_delta_chunk_size = form_data.get("params", {}).get(
1438
+ "stream_delta_chunk_size"
1439
+ )
1440
+
1441
+ # Model Params
1442
+ if model_info_params.get("stream_delta_chunk_size"):
1443
+ stream_delta_chunk_size = model_info_params.get("stream_delta_chunk_size")
1444
+
1445
+ metadata = {
1446
+ "user_id": user.id,
1447
+ "chat_id": form_data.pop("chat_id", None),
1448
+ "message_id": form_data.pop("id", None),
1449
+ "session_id": form_data.pop("session_id", None),
1450
+ "filter_ids": form_data.pop("filter_ids", []),
1451
+ "tool_ids": form_data.get("tool_ids", None),
1452
+ "tool_servers": form_data.pop("tool_servers", None),
1453
+ "files": form_data.get("files", None),
1454
+ "features": form_data.get("features", {}),
1455
+ "variables": form_data.get("variables", {}),
1456
+ "model": model,
1457
+ "direct": model_item.get("direct", False),
1458
+ "params": {
1459
+ "stream_delta_chunk_size": stream_delta_chunk_size,
1460
+ "function_calling": (
1461
+ "native"
1462
+ if (
1463
+ form_data.get("params", {}).get("function_calling") == "native"
1464
+ or model_info_params.get("function_calling") == "native"
1465
+ )
1466
+ else "default"
1467
+ ),
1468
+ },
1469
+ }
1470
+
1471
+ if metadata.get("chat_id") and (user and user.role != "admin"):
1472
+ if metadata["chat_id"] != "local":
1473
+ chat = Chats.get_chat_by_id_and_user_id(metadata["chat_id"], user.id)
1474
+ if chat is None:
1475
+ raise HTTPException(
1476
+ status_code=status.HTTP_404_NOT_FOUND,
1477
+ detail=ERROR_MESSAGES.DEFAULT(),
1478
+ )
1479
+
1480
+ request.state.metadata = metadata
1481
+ form_data["metadata"] = metadata
1482
+
1483
+ except Exception as e:
1484
+ log.debug(f"Error processing chat metadata: {e}")
1485
+ raise HTTPException(
1486
+ status_code=status.HTTP_400_BAD_REQUEST,
1487
+ detail=str(e),
1488
+ )
1489
+
1490
+ async def process_chat(request, form_data, user, metadata, model):
1491
+ try:
1492
+ form_data, metadata, events = await process_chat_payload(
1493
+ request, form_data, user, metadata, model
1494
+ )
1495
+
1496
+ response = await chat_completion_handler(request, form_data, user)
1497
+ if metadata.get("chat_id") and metadata.get("message_id"):
1498
+ try:
1499
+ Chats.upsert_message_to_chat_by_id_and_message_id(
1500
+ metadata["chat_id"],
1501
+ metadata["message_id"],
1502
+ {
1503
+ "model": model_id,
1504
+ },
1505
+ )
1506
+ except:
1507
+ pass
1508
+
1509
+ return await process_chat_response(
1510
+ request, response, form_data, user, metadata, model, events, tasks
1511
+ )
1512
+ except asyncio.CancelledError:
1513
+ log.info("Chat processing was cancelled")
1514
+ try:
1515
+ event_emitter = get_event_emitter(metadata)
1516
+ await event_emitter(
1517
+ {"type": "task-cancelled"},
1518
+ )
1519
+ except Exception as e:
1520
+ pass
1521
+ except Exception as e:
1522
+ log.debug(f"Error processing chat payload: {e}")
1523
+ if metadata.get("chat_id") and metadata.get("message_id"):
1524
+ # Update the chat message with the error
1525
+ try:
1526
+ Chats.upsert_message_to_chat_by_id_and_message_id(
1527
+ metadata["chat_id"],
1528
+ metadata["message_id"],
1529
+ {
1530
+ "error": {"content": str(e)},
1531
+ },
1532
+ )
1533
+ except:
1534
+ pass
1535
+
1536
+ raise HTTPException(
1537
+ status_code=status.HTTP_400_BAD_REQUEST,
1538
+ detail=str(e),
1539
+ )
1540
+
1541
+ if (
1542
+ metadata.get("session_id")
1543
+ and metadata.get("chat_id")
1544
+ and metadata.get("message_id")
1545
+ ):
1546
+ # Asynchronous Chat Processing
1547
+ task_id, _ = await create_task(
1548
+ request.app.state.redis,
1549
+ process_chat(request, form_data, user, metadata, model),
1550
+ id=metadata["chat_id"],
1551
+ )
1552
+ return {"status": True, "task_id": task_id}
1553
+ else:
1554
+ return await process_chat(request, form_data, user, metadata, model)
1555
+
1556
+
1557
+ # Alias for chat_completion (Legacy)
1558
+ generate_chat_completions = chat_completion
1559
+ generate_chat_completion = chat_completion
1560
+
1561
+
1562
+ @app.post("/api/chat/completed")
1563
+ async def chat_completed(
1564
+ request: Request, form_data: dict, user=Depends(get_verified_user)
1565
+ ):
1566
+ try:
1567
+ model_item = form_data.pop("model_item", {})
1568
+
1569
+ if model_item.get("direct", False):
1570
+ request.state.direct = True
1571
+ request.state.model = model_item
1572
+
1573
+ return await chat_completed_handler(request, form_data, user)
1574
+ except Exception as e:
1575
+ raise HTTPException(
1576
+ status_code=status.HTTP_400_BAD_REQUEST,
1577
+ detail=str(e),
1578
+ )
1579
+
1580
+
1581
+ @app.post("/api/chat/actions/{action_id}")
1582
+ async def chat_action(
1583
+ request: Request, action_id: str, form_data: dict, user=Depends(get_verified_user)
1584
+ ):
1585
+ try:
1586
+ model_item = form_data.pop("model_item", {})
1587
+
1588
+ if model_item.get("direct", False):
1589
+ request.state.direct = True
1590
+ request.state.model = model_item
1591
+
1592
+ return await chat_action_handler(request, action_id, form_data, user)
1593
+ except Exception as e:
1594
+ raise HTTPException(
1595
+ status_code=status.HTTP_400_BAD_REQUEST,
1596
+ detail=str(e),
1597
+ )
1598
+
1599
+
1600
+ @app.post("/api/tasks/stop/{task_id}")
1601
+ async def stop_task_endpoint(
1602
+ request: Request, task_id: str, user=Depends(get_verified_user)
1603
+ ):
1604
+ try:
1605
+ result = await stop_task(request.app.state.redis, task_id)
1606
+ return result
1607
+ except ValueError as e:
1608
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
1609
+
1610
+
1611
+ @app.get("/api/tasks")
1612
+ async def list_tasks_endpoint(request: Request, user=Depends(get_verified_user)):
1613
+ return {"tasks": await list_tasks(request.app.state.redis)}
1614
+
1615
+
1616
+ @app.get("/api/tasks/chat/{chat_id}")
1617
+ async def list_tasks_by_chat_id_endpoint(
1618
+ request: Request, chat_id: str, user=Depends(get_verified_user)
1619
+ ):
1620
+ chat = Chats.get_chat_by_id(chat_id)
1621
+ if chat is None or chat.user_id != user.id:
1622
+ return {"task_ids": []}
1623
+
1624
+ task_ids = await list_task_ids_by_item_id(request.app.state.redis, chat_id)
1625
+
1626
+ log.debug(f"Task IDs for chat {chat_id}: {task_ids}")
1627
+ return {"task_ids": task_ids}
1628
+
1629
+
1630
+ ##################################
1631
+ #
1632
+ # Config Endpoints
1633
+ #
1634
+ ##################################
1635
+
1636
+
1637
+ @app.get("/api/config")
1638
+ async def get_app_config(request: Request):
1639
+ user = None
1640
+ if "token" in request.cookies:
1641
+ token = request.cookies.get("token")
1642
+ try:
1643
+ data = decode_token(token)
1644
+ except Exception as e:
1645
+ log.debug(e)
1646
+ raise HTTPException(
1647
+ status_code=status.HTTP_401_UNAUTHORIZED,
1648
+ detail="Invalid token",
1649
+ )
1650
+ if data is not None and "id" in data:
1651
+ user = Users.get_user_by_id(data["id"])
1652
+
1653
+ user_count = Users.get_num_users()
1654
+ onboarding = False
1655
+
1656
+ if user is None:
1657
+ onboarding = user_count == 0
1658
+
1659
+ return {
1660
+ **({"onboarding": True} if onboarding else {}),
1661
+ "status": True,
1662
+ "name": app.state.WEBUI_NAME,
1663
+ "version": VERSION,
1664
+ "default_locale": str(DEFAULT_LOCALE),
1665
+ "oauth": {
1666
+ "providers": {
1667
+ name: config.get("name", name)
1668
+ for name, config in OAUTH_PROVIDERS.items()
1669
+ }
1670
+ },
1671
+ "features": {
1672
+ "auth": WEBUI_AUTH,
1673
+ "auth_trusted_header": bool(app.state.AUTH_TRUSTED_EMAIL_HEADER),
1674
+ "enable_signup_password_confirmation": ENABLE_SIGNUP_PASSWORD_CONFIRMATION,
1675
+ "enable_ldap": app.state.config.ENABLE_LDAP,
1676
+ "enable_api_key": app.state.config.ENABLE_API_KEY,
1677
+ "enable_signup": app.state.config.ENABLE_SIGNUP,
1678
+ "enable_login_form": app.state.config.ENABLE_LOGIN_FORM,
1679
+ "enable_websocket": ENABLE_WEBSOCKET_SUPPORT,
1680
+ "enable_version_update_check": ENABLE_VERSION_UPDATE_CHECK,
1681
+ **(
1682
+ {
1683
+ "enable_direct_connections": app.state.config.ENABLE_DIRECT_CONNECTIONS,
1684
+ "enable_channels": app.state.config.ENABLE_CHANNELS,
1685
+ "enable_notes": app.state.config.ENABLE_NOTES,
1686
+ "enable_web_search": app.state.config.ENABLE_WEB_SEARCH,
1687
+ "enable_code_execution": app.state.config.ENABLE_CODE_EXECUTION,
1688
+ "enable_code_interpreter": app.state.config.ENABLE_CODE_INTERPRETER,
1689
+ "enable_image_generation": app.state.config.ENABLE_IMAGE_GENERATION,
1690
+ "enable_autocomplete_generation": app.state.config.ENABLE_AUTOCOMPLETE_GENERATION,
1691
+ "enable_community_sharing": app.state.config.ENABLE_COMMUNITY_SHARING,
1692
+ "enable_message_rating": app.state.config.ENABLE_MESSAGE_RATING,
1693
+ "enable_user_webhooks": app.state.config.ENABLE_USER_WEBHOOKS,
1694
+ "enable_admin_export": ENABLE_ADMIN_EXPORT,
1695
+ "enable_admin_chat_access": ENABLE_ADMIN_CHAT_ACCESS,
1696
+ "enable_google_drive_integration": app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
1697
+ "enable_onedrive_integration": app.state.config.ENABLE_ONEDRIVE_INTEGRATION,
1698
+ }
1699
+ if user is not None
1700
+ else {}
1701
+ ),
1702
+ },
1703
+ **(
1704
+ {
1705
+ "default_models": app.state.config.DEFAULT_MODELS,
1706
+ "default_prompt_suggestions": app.state.config.DEFAULT_PROMPT_SUGGESTIONS,
1707
+ "user_count": user_count,
1708
+ "code": {
1709
+ "engine": app.state.config.CODE_EXECUTION_ENGINE,
1710
+ },
1711
+ "audio": {
1712
+ "tts": {
1713
+ "engine": app.state.config.TTS_ENGINE,
1714
+ "voice": app.state.config.TTS_VOICE,
1715
+ "split_on": app.state.config.TTS_SPLIT_ON,
1716
+ },
1717
+ "stt": {
1718
+ "engine": app.state.config.STT_ENGINE,
1719
+ },
1720
+ },
1721
+ "file": {
1722
+ "max_size": app.state.config.FILE_MAX_SIZE,
1723
+ "max_count": app.state.config.FILE_MAX_COUNT,
1724
+ "image_compression": {
1725
+ "width": app.state.config.FILE_IMAGE_COMPRESSION_WIDTH,
1726
+ "height": app.state.config.FILE_IMAGE_COMPRESSION_HEIGHT,
1727
+ },
1728
+ },
1729
+ "permissions": {**app.state.config.USER_PERMISSIONS},
1730
+ "google_drive": {
1731
+ "client_id": GOOGLE_DRIVE_CLIENT_ID.value,
1732
+ "api_key": GOOGLE_DRIVE_API_KEY.value,
1733
+ },
1734
+ "onedrive": {
1735
+ "client_id": ONEDRIVE_CLIENT_ID.value,
1736
+ "sharepoint_url": ONEDRIVE_SHAREPOINT_URL.value,
1737
+ "sharepoint_tenant_id": ONEDRIVE_SHAREPOINT_TENANT_ID.value,
1738
+ },
1739
+ "ui": {
1740
+ "pending_user_overlay_title": app.state.config.PENDING_USER_OVERLAY_TITLE,
1741
+ "pending_user_overlay_content": app.state.config.PENDING_USER_OVERLAY_CONTENT,
1742
+ "response_watermark": app.state.config.RESPONSE_WATERMARK,
1743
+ },
1744
+ "license_metadata": app.state.LICENSE_METADATA,
1745
+ **(
1746
+ {
1747
+ "active_entries": app.state.USER_COUNT,
1748
+ }
1749
+ if user.role == "admin"
1750
+ else {}
1751
+ ),
1752
+ }
1753
+ if user is not None and (user.role in ["admin", "user"])
1754
+ else {
1755
+ **(
1756
+ {
1757
+ "ui": {
1758
+ "pending_user_overlay_title": app.state.config.PENDING_USER_OVERLAY_TITLE,
1759
+ "pending_user_overlay_content": app.state.config.PENDING_USER_OVERLAY_CONTENT,
1760
+ }
1761
+ }
1762
+ if user and user.role == "pending"
1763
+ else {}
1764
+ ),
1765
+ **(
1766
+ {
1767
+ "metadata": {
1768
+ "login_footer": app.state.LICENSE_METADATA.get(
1769
+ "login_footer", ""
1770
+ ),
1771
+ "auth_logo_position": app.state.LICENSE_METADATA.get(
1772
+ "auth_logo_position", ""
1773
+ ),
1774
+ }
1775
+ }
1776
+ if app.state.LICENSE_METADATA
1777
+ else {}
1778
+ ),
1779
+ }
1780
+ ),
1781
+ }
1782
+
1783
+
1784
+ class UrlForm(BaseModel):
1785
+ url: str
1786
+
1787
+
1788
+ @app.get("/api/webhook")
1789
+ async def get_webhook_url(user=Depends(get_admin_user)):
1790
+ return {
1791
+ "url": app.state.config.WEBHOOK_URL,
1792
+ }
1793
+
1794
+
1795
+ @app.post("/api/webhook")
1796
+ async def update_webhook_url(form_data: UrlForm, user=Depends(get_admin_user)):
1797
+ app.state.config.WEBHOOK_URL = form_data.url
1798
+ app.state.WEBHOOK_URL = app.state.config.WEBHOOK_URL
1799
+ return {"url": app.state.config.WEBHOOK_URL}
1800
+
1801
+
1802
+ @app.get("/api/version")
1803
+ async def get_app_version():
1804
+ return {
1805
+ "version": VERSION,
1806
+ }
1807
+
1808
+
1809
+ @app.get("/api/version/updates")
1810
+ async def get_app_latest_release_version(user=Depends(get_verified_user)):
1811
+ if not ENABLE_VERSION_UPDATE_CHECK:
1812
+ log.debug(
1813
+ f"Version update check is disabled, returning current version as latest version"
1814
+ )
1815
+ return {"current": VERSION, "latest": VERSION}
1816
+ try:
1817
+ timeout = aiohttp.ClientTimeout(total=1)
1818
+ async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
1819
+ async with session.get(
1820
+ "https://api.github.com/repos/open-webui/open-webui/releases/latest",
1821
+ ssl=AIOHTTP_CLIENT_SESSION_SSL,
1822
+ ) as response:
1823
+ response.raise_for_status()
1824
+ data = await response.json()
1825
+ latest_version = data["tag_name"]
1826
+
1827
+ return {"current": VERSION, "latest": latest_version[1:]}
1828
+ except Exception as e:
1829
+ log.debug(e)
1830
+ return {"current": VERSION, "latest": VERSION}
1831
+
1832
+
1833
+ @app.get("/api/changelog")
1834
+ async def get_app_changelog():
1835
+ return {key: CHANGELOG[key] for idx, key in enumerate(CHANGELOG) if idx < 5}
1836
+
1837
+
1838
+ @app.get("/api/usage")
1839
+ async def get_current_usage(user=Depends(get_verified_user)):
1840
+ """
1841
+ Get current usage statistics for Open WebUI.
1842
+ This is an experimental endpoint and subject to change.
1843
+ """
1844
+ try:
1845
+ return {"model_ids": get_models_in_use(), "user_ids": get_active_user_ids()}
1846
+ except Exception as e:
1847
+ log.error(f"Error getting usage statistics: {e}")
1848
+ raise HTTPException(status_code=500, detail="Internal Server Error")
1849
+
1850
+
1851
+ ############################
1852
+ # OAuth Login & Callback
1853
+ ############################
1854
+
1855
+ # SessionMiddleware is used by authlib for oauth
1856
+ if len(OAUTH_PROVIDERS) > 0:
1857
+ app.add_middleware(
1858
+ SessionMiddleware,
1859
+ secret_key=WEBUI_SECRET_KEY,
1860
+ session_cookie="oui-session",
1861
+ same_site=WEBUI_SESSION_COOKIE_SAME_SITE,
1862
+ https_only=WEBUI_SESSION_COOKIE_SECURE,
1863
+ )
1864
+
1865
+
1866
+ @app.get("/oauth/{provider}/login")
1867
+ async def oauth_login(provider: str, request: Request):
1868
+ return await oauth_manager.handle_login(request, provider)
1869
+
1870
+
1871
+ # OAuth login logic is as follows:
1872
+ # 1. Attempt to find a user with matching subject ID, tied to the provider
1873
+ # 2. If OAUTH_MERGE_ACCOUNTS_BY_EMAIL is true, find a user with the email address provided via OAuth
1874
+ # - This is considered insecure in general, as OAuth providers do not always verify email addresses
1875
+ # 3. If there is no user, and ENABLE_OAUTH_SIGNUP is true, create a user
1876
+ # - Email addresses are considered unique, so we fail registration if the email address is already taken
1877
+ @app.get("/oauth/{provider}/callback")
1878
+ async def oauth_callback(provider: str, request: Request, response: Response):
1879
+ return await oauth_manager.handle_callback(request, provider, response)
1880
+
1881
+
1882
+ @app.get("/manifest.json")
1883
+ async def get_manifest_json():
1884
+ if app.state.EXTERNAL_PWA_MANIFEST_URL:
1885
+ return requests.get(app.state.EXTERNAL_PWA_MANIFEST_URL).json()
1886
+ else:
1887
+ return {
1888
+ "name": app.state.WEBUI_NAME,
1889
+ "short_name": app.state.WEBUI_NAME,
1890
+ "description": f"{app.state.WEBUI_NAME} is an open, extensible, user-friendly interface for AI that adapts to your workflow.",
1891
+ "start_url": "/",
1892
+ "display": "standalone",
1893
+ "background_color": "#343541",
1894
+ "icons": [
1895
+ {
1896
+ "src": "/static/logo.png",
1897
+ "type": "image/png",
1898
+ "sizes": "500x500",
1899
+ "purpose": "any",
1900
+ },
1901
+ {
1902
+ "src": "/static/logo.png",
1903
+ "type": "image/png",
1904
+ "sizes": "500x500",
1905
+ "purpose": "maskable",
1906
+ },
1907
+ ],
1908
+ }
1909
+
1910
+
1911
+ @app.get("/opensearch.xml")
1912
+ async def get_opensearch_xml():
1913
+ xml_content = rf"""
1914
+ <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">
1915
+ <ShortName>{app.state.WEBUI_NAME}</ShortName>
1916
+ <Description>Search {app.state.WEBUI_NAME}</Description>
1917
+ <InputEncoding>UTF-8</InputEncoding>
1918
+ <Image width="16" height="16" type="image/x-icon">{app.state.config.WEBUI_URL}/static/favicon.png</Image>
1919
+ <Url type="text/html" method="get" template="{app.state.config.WEBUI_URL}/?q={"{searchTerms}"}"/>
1920
+ <moz:SearchForm>{app.state.config.WEBUI_URL}</moz:SearchForm>
1921
+ </OpenSearchDescription>
1922
+ """
1923
+ return Response(content=xml_content, media_type="application/xml")
1924
+
1925
+
1926
+ @app.get("/health")
1927
+ async def healthcheck():
1928
+ return {"status": True}
1929
+
1930
+
1931
+ @app.get("/health/db")
1932
+ async def healthcheck_with_db():
1933
+ Session.execute(text("SELECT 1;")).all()
1934
+ return {"status": True}
1935
+
1936
+
1937
+ app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
1938
+
1939
+
1940
+ @app.get("/cache/{path:path}")
1941
+ async def serve_cache_file(
1942
+ path: str,
1943
+ user=Depends(get_verified_user),
1944
+ ):
1945
+ file_path = os.path.abspath(os.path.join(CACHE_DIR, path))
1946
+ # prevent path traversal
1947
+ if not file_path.startswith(os.path.abspath(CACHE_DIR)):
1948
+ raise HTTPException(status_code=404, detail="File not found")
1949
+ if not os.path.isfile(file_path):
1950
+ raise HTTPException(status_code=404, detail="File not found")
1951
+ return FileResponse(file_path)
1952
+
1953
+
1954
+ def swagger_ui_html(*args, **kwargs):
1955
+ return get_swagger_ui_html(
1956
+ *args,
1957
+ **kwargs,
1958
+ swagger_js_url="/static/swagger-ui/swagger-ui-bundle.js",
1959
+ swagger_css_url="/static/swagger-ui/swagger-ui.css",
1960
+ swagger_favicon_url="/static/swagger-ui/favicon.png",
1961
+ )
1962
+
1963
+
1964
+ applications.get_swagger_ui_html = swagger_ui_html
1965
+
1966
+ if os.path.exists(FRONTEND_BUILD_DIR):
1967
+ mimetypes.add_type("text/javascript", ".js")
1968
+ app.mount(
1969
+ "/",
1970
+ SPAStaticFiles(directory=FRONTEND_BUILD_DIR, html=True),
1971
+ name="spa-static-files",
1972
+ )
1973
+ else:
1974
+ log.warning(
1975
+ f"Frontend build directory not found at '{FRONTEND_BUILD_DIR}'. Serving API only."
1976
+ )