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

GitHub deploy: 4d024c91d61f15a8b39171610ab1406915ef598d

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 +42 -0
  6. .github/FUNDING.yml +1 -0
  7. .github/ISSUE_TEMPLATE/bug_report.yaml +176 -0
  8. .github/ISSUE_TEMPLATE/config.yml +1 -0
  9. .github/ISSUE_TEMPLATE/feature_request.yaml +82 -0
  10. .github/dependabot.yml +20 -0
  11. .github/pull_request_template.md +96 -0
  12. .github/workflows/build-release.yml +72 -0
  13. .github/workflows/codespell.disabled +25 -0
  14. .github/workflows/deploy-to-hf-spaces.yml +64 -0
  15. .github/workflows/docker-build.yaml +806 -0
  16. .github/workflows/format-backend.yaml +49 -0
  17. .github/workflows/format-build-frontend.yaml +65 -0
  18. .github/workflows/integration-test.disabled +255 -0
  19. .github/workflows/lint-backend.disabled +27 -0
  20. .github/workflows/lint-frontend.disabled +21 -0
  21. .github/workflows/release-pypi.yml +36 -0
  22. .gitignore +312 -0
  23. .npmrc +1 -0
  24. .prettierignore +314 -0
  25. .prettierrc +10 -0
  26. CHANGELOG.md +0 -0
  27. CODE_OF_CONDUCT.md +99 -0
  28. CONTRIBUTOR_LICENSE_AGREEMENT +7 -0
  29. Dockerfile +202 -0
  30. LICENSE +33 -0
  31. LICENSE_HISTORY +53 -0
  32. LICENSE_NOTICE +11 -0
  33. Makefile +33 -0
  34. README.md +247 -0
  35. TROUBLESHOOTING.md +36 -0
  36. backend/.dockerignore +14 -0
  37. backend/.gitignore +12 -0
  38. backend/dev.sh +3 -0
  39. backend/open_webui/__init__.py +103 -0
  40. backend/open_webui/alembic.ini +114 -0
  41. backend/open_webui/config.py +0 -0
  42. backend/open_webui/constants.py +126 -0
  43. backend/open_webui/env.py +967 -0
  44. backend/open_webui/functions.py +350 -0
  45. backend/open_webui/internal/db.py +172 -0
  46. backend/open_webui/internal/migrations/001_initial_schema.py +253 -0
  47. backend/open_webui/internal/migrations/002_add_local_sharing.py +47 -0
  48. backend/open_webui/internal/migrations/003_add_auth_api_key.py +47 -0
  49. backend/open_webui/internal/migrations/004_add_archived.py +45 -0
  50. backend/open_webui/internal/migrations/005_add_updated_at.py +129 -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,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ *.ttf filter=lfs diff=lfs merge=lfs -text
42
+ *.jpg filter=lfs diff=lfs merge=lfs -text
.github/FUNDING.yml ADDED
@@ -0,0 +1 @@
 
 
1
+ github: tjbck
.github/ISSUE_TEMPLATE/bug_report.yaml ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Bug Report
2
+ description: Create a detailed bug report to help us improve Open WebUI.
3
+ title: 'issue: '
4
+ labels: ['bug', 'triage']
5
+ assignees: []
6
+ body:
7
+ - type: markdown
8
+ attributes:
9
+ value: |
10
+ # Bug Report
11
+
12
+ ## Important Notes
13
+
14
+ - **Before submitting a bug report**: Please check the [Issues](https://github.com/open-webui/open-webui/issues) and [Discussions](https://github.com/open-webui/open-webui/discussions) sections to see if a similar issue has already been reported. If unsure, start a discussion first, as this helps us efficiently focus on improving the project. Duplicates may be closed without notice. **Please search for existing issues AND discussions. No matter open or closed.**
15
+
16
+ - Check for opened, **but also for (recently) CLOSED issues** as the issue you are trying to report **might already have been fixed on the dev branch!**
17
+
18
+ - **Respectful collaboration**: Open WebUI is a volunteer-driven project with a single maintainer and contributors who also have full-time jobs. Please be constructive and respectful in your communication.
19
+
20
+ - **Contributing**: If you encounter an issue, consider submitting a pull request or forking the project. We prioritize preventing contributor burnout to maintain Open WebUI's quality.
21
+
22
+ - **Bug Reproducibility**: If a bug cannot be reproduced using a `:main` or `:dev` Docker setup or with `pip install` on Python 3.11, community assistance may be required. In such cases, we will move it to the "[Issues](https://github.com/open-webui/open-webui/discussions/categories/issues)" Discussions section. Your help is appreciated!
23
+
24
+ - **Scope**: If you want to report a SECURITY VULNERABILITY, then do so through our [GitHub security page](https://github.com/open-webui/open-webui/security).
25
+
26
+ - type: checkboxes
27
+ id: issue-check
28
+ attributes:
29
+ label: Check Existing Issues
30
+ description: Confirm that you’ve checked for existing reports before submitting a new one.
31
+ options:
32
+ - label: I have searched for any existing and/or related issues.
33
+ required: true
34
+ - label: I have searched for any existing and/or related discussions.
35
+ required: true
36
+ - label: I have also searched in the CLOSED issues AND CLOSED discussions and found no related items (your issue might already be addressed on the development branch!).
37
+ required: true
38
+ - label: I am using the latest version of Open WebUI.
39
+ required: true
40
+
41
+ - type: dropdown
42
+ id: installation-method
43
+ attributes:
44
+ label: Installation Method
45
+ description: How did you install Open WebUI?
46
+ options:
47
+ - Git Clone
48
+ - Pip Install
49
+ - Docker
50
+ - Other
51
+ validations:
52
+ required: true
53
+
54
+ - type: input
55
+ id: open-webui-version
56
+ attributes:
57
+ label: Open WebUI Version
58
+ description: Specify the version (e.g., v0.6.26)
59
+ validations:
60
+ required: true
61
+
62
+ - type: input
63
+ id: ollama-version
64
+ attributes:
65
+ label: Ollama Version (if applicable)
66
+ description: Specify the version (e.g., v0.2.0, or v0.1.32-rc1)
67
+ validations:
68
+ required: false
69
+
70
+ - type: input
71
+ id: operating-system
72
+ attributes:
73
+ label: Operating System
74
+ description: Specify the OS (e.g., Windows 10, macOS Sonoma, Ubuntu 22.04, Debian 12)
75
+ validations:
76
+ required: true
77
+
78
+ - type: input
79
+ id: browser
80
+ attributes:
81
+ label: Browser (if applicable)
82
+ description: Specify the browser/version (e.g., Chrome 100.0, Firefox 98.0)
83
+ validations:
84
+ required: false
85
+
86
+ - type: checkboxes
87
+ id: confirmation
88
+ attributes:
89
+ label: Confirmation
90
+ description: Ensure the following prerequisites have been met.
91
+ options:
92
+ - label: I have read and followed all instructions in `README.md`.
93
+ required: true
94
+ - label: I am using the latest version of **both** Open WebUI and Ollama.
95
+ required: true
96
+ - label: I have included the browser console logs.
97
+ required: true
98
+ - label: I have included the Docker container logs.
99
+ required: true
100
+ - label: I have **provided every relevant configuration, setting, and environment variable used in my setup.**
101
+ required: true
102
+ - label: I have clearly **listed every relevant configuration, custom setting, environment variable, and command-line option that influences my setup** (such as Docker Compose overrides, .env values, browser settings, authentication configurations, etc).
103
+ required: true
104
+ - label: |
105
+ I have documented **step-by-step reproduction instructions that are precise, sequential, and leave nothing to interpretation**. My steps:
106
+ - Start with the initial platform/version/OS and dependencies used,
107
+ - Specify exact install/launch/configure commands,
108
+ - List URLs visited, user input (incl. example values/emails/passwords if needed),
109
+ - Describe all options and toggles enabled or changed,
110
+ - Include any files or environmental changes,
111
+ - Identify the expected and actual result at each stage,
112
+ - Ensure any reasonably skilled user can follow and hit the same issue.
113
+ required: true
114
+ - type: textarea
115
+ id: expected-behavior
116
+ attributes:
117
+ label: Expected Behavior
118
+ description: Describe what should have happened.
119
+ validations:
120
+ required: true
121
+
122
+ - type: textarea
123
+ id: actual-behavior
124
+ attributes:
125
+ label: Actual Behavior
126
+ description: Describe what actually happened.
127
+ validations:
128
+ required: true
129
+
130
+ - type: textarea
131
+ id: reproduction-steps
132
+ attributes:
133
+ label: Steps to Reproduce
134
+ description: |
135
+ Please provide a **very detailed, step-by-step guide** to reproduce the issue. Your instructions should be so clear and precise that anyone can follow them without guesswork. Include every relevant detail—settings, configuration options, exact commands used, values entered, and any prerequisites or environment variables.
136
+ **If full reproduction steps and all relevant settings are not provided, your issue may not be addressed.**
137
+ **If your steps to reproduction are incomplete, lacking detail or not reproducible, your issue can not be addressed.**
138
+
139
+ placeholder: |
140
+ Example (include every detail):
141
+ 1. Start with a clean Ubuntu 22.04 install.
142
+ 2. Install Docker v24.0.5 and start the service.
143
+ 3. Clone the Open WebUI repo (git clone ...).
144
+ 4. Use the Docker Compose file without modifications.
145
+ 5. Open browser Chrome 115.0 in incognito mode.
146
+ 6. Go to http://localhost:8080 and log in with user "test@example.com".
147
+ 7. Set the language to "English" and theme to "Dark".
148
+ 8. Attempt to connect to Ollama at "http://localhost:11434".
149
+ 9. Observe that the error message "Connection refused" appears at the top right.
150
+
151
+ Please list each step carefully and include all relevant configuration, settings, and options.
152
+ validations:
153
+ required: true
154
+ - type: textarea
155
+ id: logs-screenshots
156
+ attributes:
157
+ label: Logs & Screenshots
158
+ description: Include relevant logs, errors, or screenshots to help diagnose the issue.
159
+ placeholder: 'Attach logs from the browser console, Docker logs, or error messages.'
160
+ validations:
161
+ required: true
162
+
163
+ - type: textarea
164
+ id: additional-info
165
+ attributes:
166
+ label: Additional Information
167
+ description: Provide any extra details that may assist in understanding the issue.
168
+ validations:
169
+ required: false
170
+
171
+ - type: markdown
172
+ attributes:
173
+ value: |
174
+ ## Note
175
+ **If the bug report is incomplete, does not follow instructions or is lacking details it may not be addressed.** Ensure that you've followed all the **README.md** and **troubleshooting.md** guidelines, and provide all necessary information for us to reproduce the issue.
176
+ Thank you for contributing to Open WebUI!
.github/ISSUE_TEMPLATE/config.yml ADDED
@@ -0,0 +1 @@
 
 
1
+ blank_issues_enabled: false
.github/ISSUE_TEMPLATE/feature_request.yaml ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Feature Request
2
+ description: Suggest an idea for this project
3
+ title: 'feat: '
4
+ labels: ['triage']
5
+ body:
6
+ - type: markdown
7
+ attributes:
8
+ value: |
9
+ ## Important Notes
10
+ ### Before submitting
11
+
12
+ Please check the **open AND closed** [Issues](https://github.com/open-webui/open-webui/issues) AND [Discussions](https://github.com/open-webui/open-webui/discussions) to see if a similar request has been posted.
13
+ It's likely we're already tracking it! If you’re unsure, start a discussion post first.
14
+
15
+ #### Scope
16
+
17
+ If your feature request is likely to take more than a quick coding session to implement, test and verify, then open it in the **Ideas** section of the [Discussions](https://github.com/open-webui/open-webui/discussions) instead.
18
+ **We will close and force move your feature request to the Ideas section, if we believe your feature request is not trivial/quick to implement.**
19
+ This is to ensure the issues tab is used only for issues, quickly addressable feature requests and tracking tickets by the maintainers.
20
+ Other feature requests belong in the **Ideas** section of the [Discussions](https://github.com/open-webui/open-webui/discussions).
21
+
22
+ If your feature request might impact others in the community, definitely open a discussion instead and evaluate whether and how to implement it.
23
+
24
+ This will help us efficiently focus on improving the project.
25
+
26
+ ### Collaborate respectfully
27
+ We value a **constructive attitude**, so please be mindful of your communication. If negativity is part of your approach, our capacity to engage may be limited. We're here to help if you're **open to learning** and **communicating positively**.
28
+
29
+ Remember:
30
+ - Open WebUI is a **volunteer-driven project**
31
+ - It's managed by a **single maintainer**
32
+ - It's supported by contributors who also have **full-time jobs**
33
+
34
+ We appreciate your time and ask that you **respect ours**.
35
+
36
+ ### Contributing
37
+ If you encounter an issue, we highly encourage you to submit a pull request or fork the project. We actively work to prevent contributor burnout to maintain the quality and continuity of Open WebUI.
38
+
39
+ ### Bug reproducibility
40
+ If a bug cannot be reproduced with a `:main` or `:dev` Docker setup, or a `pip install` with Python 3.11, it may require additional help from the community. In such cases, we will move it to the "[issues](https://github.com/open-webui/open-webui/discussions/categories/issues)" Discussions section due to our limited resources. We encourage the community to assist with these issues. Remember, it’s not that the issue doesn’t exist; we need your help!
41
+
42
+ - type: checkboxes
43
+ id: existing-issue
44
+ attributes:
45
+ label: Check Existing Issues
46
+ description: Please confirm that you've checked for existing similar requests
47
+ options:
48
+ - label: I have searched for all existing **open AND closed** issues and discussions for similar requests. I have found none that is comparable to my request.
49
+ required: true
50
+ - type: checkboxes
51
+ id: feature-scope
52
+ attributes:
53
+ label: Verify Feature Scope
54
+ description: Please confirm the feature's scope is within the described scope
55
+ options:
56
+ - label: I have read through and understood the scope definition for feature requests in the Issues section. I believe my feature request meets the definition and belongs in the Issues section instead of the Discussions.
57
+ required: true
58
+ - type: textarea
59
+ id: problem-description
60
+ attributes:
61
+ label: Problem Description
62
+ description: Is your feature request related to a problem? Please provide a clear and concise description of what the problem is.
63
+ placeholder: "Ex. I'm always frustrated when... / Not related to a problem"
64
+ validations:
65
+ required: true
66
+ - type: textarea
67
+ id: solution-description
68
+ attributes:
69
+ label: Desired Solution you'd like
70
+ description: Clearly describe what you want to happen.
71
+ validations:
72
+ required: true
73
+ - type: textarea
74
+ id: alternatives-considered
75
+ attributes:
76
+ label: Alternatives Considered
77
+ description: A clear and concise description of any alternative solutions or features you've considered.
78
+ - type: textarea
79
+ id: additional-context
80
+ attributes:
81
+ label: Additional Context
82
+ description: Add any other context or screenshots about the feature request here.
.github/dependabot.yml ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: uv
4
+ directory: '/'
5
+ schedule:
6
+ interval: monthly
7
+ target-branch: 'dev'
8
+
9
+ - package-ecosystem: pip
10
+ directory: '/backend'
11
+ schedule:
12
+ interval: monthly
13
+ target-branch: 'dev'
14
+
15
+ - package-ecosystem: 'github-actions'
16
+ directory: '/'
17
+ schedule:
18
+ # Check for updates to GitHub Actions every week
19
+ interval: monthly
20
+ target-branch: 'dev'
.github/pull_request_template.md ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!--
2
+ ⚠️ CRITICAL CHECKS FOR CONTRIBUTORS (READ, DON'T DELETE) ⚠️
3
+ 1. Target the `dev` branch. PRs targeting `main` will be automatically closed.
4
+ 2. Do NOT delete the CLA section at the bottom. It is required for the bot to accept your PR.
5
+ -->
6
+
7
+ # Pull Request Checklist
8
+
9
+ ### Note to first-time contributors: Please open a discussion post in [Discussions](https://github.com/open-webui/open-webui/discussions) to discuss your idea/fix with the community before creating a pull request, and describe your changes before submitting a pull request.
10
+
11
+ This is to ensure large feature PRs are discussed with the community first, before starting work on it. If the community does not want this feature or it is not relevant for Open WebUI as a project, it can be identified in the discussion before working on the feature and submitting the PR.
12
+
13
+ **Before submitting, make sure you've checked the following:**
14
+
15
+ - [ ] **Target branch:** Verify that the pull request targets the `dev` branch. **PRs targeting `main` will be immediately closed.**
16
+ - [ ] **Description:** Provide a concise description of the changes made in this pull request down below.
17
+ - [ ] **Changelog:** Ensure a changelog entry following the format of [Keep a Changelog](https://keepachangelog.com/) is added at the bottom of the PR description.
18
+ - [ ] **Documentation:** Add docs in [Open WebUI Docs Repository](https://github.com/open-webui/docs). Document user-facing behavior, environment variables, public APIs/interfaces, or deployment steps.
19
+ - [ ] **Dependencies:** Are there any new or upgraded dependencies? If so, explain why, update the changelog/docs, and include any compatibility notes. Actually run the code/function that uses updated library to ensure it doesn't crash.
20
+ - [ ] **Testing:** Perform manual tests to **verify the implemented fix/feature works as intended AND does not break any other functionality**. Include reproducible steps to demonstrate the issue before the fix. Test edge cases (URL encoding, HTML entities, types). Take this as an opportunity to **make screenshots of the feature/fix and include them in the PR description**.
21
+ - [ ] **Agentic AI Code:** Confirm this Pull Request is **not written by any AI Agent** or has at least **gone through additional human review AND manual testing**. If any AI Agent is the co-author of this PR, it may lead to immediate closure of the PR.
22
+ - [ ] **Code review:** Have you performed a self-review of your code, addressing any coding standard issues and ensuring adherence to the project's coding standards?
23
+ - [ ] **Design & Architecture:** Prefer smart defaults over adding new settings; use local state for ephemeral UI logic. Open a Discussion for major architectural or UX changes.
24
+ - [ ] **Git Hygiene:** Keep PRs atomic (one logical change). Clean up commits and rebase on `dev` to ensure no unrelated commits (e.g. from `main`) are included. Push updates to the existing PR branch instead of closing and reopening.
25
+ - [ ] **Title Prefix:** To clearly categorize this pull request, prefix the pull request title using one of the following:
26
+ - **BREAKING CHANGE**: Significant changes that may affect compatibility
27
+ - **build**: Changes that affect the build system or external dependencies
28
+ - **ci**: Changes to our continuous integration processes or workflows
29
+ - **chore**: Refactor, cleanup, or other non-functional code changes
30
+ - **docs**: Documentation update or addition
31
+ - **feat**: Introduces a new feature or enhancement to the codebase
32
+ - **fix**: Bug fix or error correction
33
+ - **i18n**: Internationalization or localization changes
34
+ - **perf**: Performance improvement
35
+ - **refactor**: Code restructuring for better maintainability, readability, or scalability
36
+ - **style**: Changes that do not affect the meaning of the code (white space, formatting, missing semi-colons, etc.)
37
+ - **test**: Adding missing tests or correcting existing tests
38
+ - **WIP**: Work in progress, a temporary label for incomplete or ongoing work
39
+
40
+ # Changelog Entry
41
+
42
+ ### Description
43
+
44
+ - [Concisely describe the changes made in this pull request, including any relevant motivation and impact (e.g., fixing a bug, adding a feature, or improving performance)]
45
+
46
+ ### Added
47
+
48
+ - [List any new features, functionalities, or additions]
49
+
50
+ ### Changed
51
+
52
+ - [List any changes, updates, refactorings, or optimizations]
53
+
54
+ ### Deprecated
55
+
56
+ - [List any deprecated functionality or features that have been removed]
57
+
58
+ ### Removed
59
+
60
+ - [List any removed features, files, or functionalities]
61
+
62
+ ### Fixed
63
+
64
+ - [List any fixes, corrections, or bug fixes]
65
+
66
+ ### Security
67
+
68
+ - [List any new or updated security-related changes, including vulnerability fixes]
69
+
70
+ ### Breaking Changes
71
+
72
+ - **BREAKING CHANGE**: [List any breaking changes affecting compatibility or functionality]
73
+
74
+ ---
75
+
76
+ ### Additional Information
77
+
78
+ - [Insert any additional context, notes, or explanations for the changes]
79
+ - [Reference any related issues, commits, or other relevant information]
80
+
81
+ ### Screenshots or Videos
82
+
83
+ - [Attach any relevant screenshots or videos demonstrating the changes]
84
+
85
+ ### Contributor License Agreement
86
+
87
+ <!--
88
+ 🚨 DO NOT DELETE THE TEXT BELOW 🚨
89
+ Keep the "Contributor License Agreement" confirmation text intact.
90
+ Deleting it will trigger the CLA-Bot to INVALIDATE your PR.
91
+ -->
92
+
93
+ By submitting this pull request, I confirm that I have read and fully agree to the [Contributor License Agreement (CLA)](https://github.com/open-webui/open-webui/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT), and I am providing my contributions under its terms.
94
+
95
+ > [!NOTE]
96
+ > Deleting the CLA section will lead to immediate closure of your PR and it will not be merged in.
.github/workflows/build-release.yml ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main # or whatever branch you want to use
7
+
8
+ jobs:
9
+ release:
10
+ runs-on: ubuntu-latest
11
+
12
+ steps:
13
+ - name: Checkout repository
14
+ uses: actions/checkout@v5
15
+
16
+ - name: Check for changes in package.json
17
+ run: |
18
+ git diff --cached --diff-filter=d package.json || {
19
+ echo "No changes to package.json"
20
+ exit 1
21
+ }
22
+
23
+ - name: Get version number from package.json
24
+ id: get_version
25
+ run: |
26
+ VERSION=$(jq -r '.version' package.json)
27
+ echo "::set-output name=version::$VERSION"
28
+
29
+ - name: Extract latest CHANGELOG entry
30
+ id: changelog
31
+ run: |
32
+ CHANGELOG_CONTENT=$(awk 'BEGIN {print_section=0;} /^## \[/ {if (print_section == 0) {print_section=1;} else {exit;}} print_section {print;}' CHANGELOG.md)
33
+ CHANGELOG_ESCAPED=$(echo "$CHANGELOG_CONTENT" | sed ':a;N;$!ba;s/\n/%0A/g')
34
+ echo "Extracted latest release notes from CHANGELOG.md:"
35
+ echo -e "$CHANGELOG_CONTENT"
36
+ echo "::set-output name=content::$CHANGELOG_ESCAPED"
37
+
38
+ - name: Create GitHub release
39
+ uses: actions/github-script@v8
40
+ with:
41
+ github-token: ${{ secrets.GITHUB_TOKEN }}
42
+ script: |
43
+ const changelog = `${{ steps.changelog.outputs.content }}`;
44
+ const release = await github.rest.repos.createRelease({
45
+ owner: context.repo.owner,
46
+ repo: context.repo.repo,
47
+ tag_name: `v${{ steps.get_version.outputs.version }}`,
48
+ name: `v${{ steps.get_version.outputs.version }}`,
49
+ body: changelog,
50
+ })
51
+ console.log(`Created release ${release.data.html_url}`)
52
+
53
+ - name: Upload package to GitHub release
54
+ uses: actions/upload-artifact@v4
55
+ with:
56
+ name: package
57
+ path: |
58
+ .
59
+ !.git
60
+ env:
61
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
62
+
63
+ - name: Trigger Docker build workflow
64
+ uses: actions/github-script@v8
65
+ with:
66
+ script: |
67
+ github.rest.actions.createWorkflowDispatch({
68
+ owner: context.repo.owner,
69
+ repo: context.repo.repo,
70
+ workflow_id: 'docker-build.yaml',
71
+ ref: 'v${{ steps.get_version.outputs.version }}',
72
+ })
.github/workflows/codespell.disabled ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Codespell configuration is within pyproject.toml
2
+ ---
3
+ name: Codespell
4
+
5
+ on:
6
+ push:
7
+ branches: [main]
8
+ pull_request:
9
+ branches: [main]
10
+
11
+ permissions:
12
+ contents: read
13
+
14
+ jobs:
15
+ codespell:
16
+ name: Check for spelling errors
17
+ runs-on: ubuntu-latest
18
+
19
+ steps:
20
+ - name: Checkout
21
+ uses: actions/checkout@v4
22
+ - name: Annotate locations with typos
23
+ uses: codespell-project/codespell-problem-matcher@v1
24
+ - name: Codespell
25
+ uses: codespell-project/actions-codespell@v2
.github/workflows/deploy-to-hf-spaces.yml ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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@v5
31
+ with:
32
+ lfs: true
33
+
34
+ - name: Remove git history
35
+ run: rm -rf .git
36
+
37
+ - name: Prepend YAML front matter to README.md
38
+ run: |
39
+ echo "---" > temp_readme.md
40
+ echo "title: Open WebUI" >> temp_readme.md
41
+ echo "emoji: 🐳" >> temp_readme.md
42
+ echo "colorFrom: purple" >> temp_readme.md
43
+ echo "colorTo: gray" >> temp_readme.md
44
+ echo "sdk: docker" >> temp_readme.md
45
+ echo "app_port: 8080" >> temp_readme.md
46
+ echo "---" >> temp_readme.md
47
+ cat README.md >> temp_readme.md
48
+ mv temp_readme.md README.md
49
+
50
+ - name: Configure git
51
+ run: |
52
+ git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
53
+ git config --global user.name "github-actions[bot]"
54
+ - name: Set up Git and push to Space
55
+ run: |
56
+ git init --initial-branch=main
57
+ git lfs install
58
+ git lfs track "*.ttf"
59
+ git lfs track "*.jpg"
60
+ rm demo.png
61
+ rm banner.png
62
+ git add .
63
+ git commit -m "GitHub deploy: ${{ github.sha }}"
64
+ git push --force https://open-webui:${HF_TOKEN}@huggingface.co/spaces/open-webui/open-webui main
.github/workflows/docker-build.yaml ADDED
@@ -0,0 +1,806 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Create and publish Docker images with specific build args
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ push:
6
+ branches:
7
+ - main
8
+ - dev
9
+ tags:
10
+ - v*
11
+
12
+ env:
13
+ REGISTRY: ghcr.io
14
+
15
+ jobs:
16
+ build-main-image:
17
+ runs-on: ${{ matrix.runner }}
18
+ permissions:
19
+ contents: read
20
+ packages: write
21
+ strategy:
22
+ fail-fast: false
23
+ matrix:
24
+ include:
25
+ - platform: linux/amd64
26
+ runner: ubuntu-latest
27
+ - platform: linux/arm64
28
+ runner: ubuntu-24.04-arm
29
+
30
+ steps:
31
+ # GitHub Packages requires the entire repository name to be in lowercase
32
+ # although the repository owner has a lowercase username, this prevents some people from running actions after forking
33
+ - name: Set repository and image name to lowercase
34
+ run: |
35
+ echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
36
+ echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
37
+ env:
38
+ IMAGE_NAME: '${{ github.repository }}'
39
+
40
+ - name: Prepare
41
+ run: |
42
+ platform=${{ matrix.platform }}
43
+ echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
44
+
45
+ - name: Checkout repository
46
+ uses: actions/checkout@v5
47
+
48
+ - name: Set up QEMU
49
+ uses: docker/setup-qemu-action@v3
50
+
51
+ - name: Set up Docker Buildx
52
+ uses: docker/setup-buildx-action@v3
53
+
54
+ - name: Log in to the Container registry
55
+ uses: docker/login-action@v3
56
+ with:
57
+ registry: ${{ env.REGISTRY }}
58
+ username: ${{ github.actor }}
59
+ password: ${{ secrets.GITHUB_TOKEN }}
60
+
61
+ - name: Extract metadata for Docker images (default latest tag)
62
+ id: meta
63
+ uses: docker/metadata-action@v5
64
+ with:
65
+ images: ${{ env.FULL_IMAGE_NAME }}
66
+ tags: |
67
+ type=ref,event=branch
68
+ type=ref,event=tag
69
+ type=sha,prefix=git-
70
+ type=semver,pattern={{version}}
71
+ type=semver,pattern={{major}}.{{minor}}
72
+ flavor: |
73
+ latest=${{ github.ref == 'refs/heads/main' }}
74
+
75
+ - name: Extract metadata for Docker cache
76
+ id: cache-meta
77
+ uses: docker/metadata-action@v5
78
+ with:
79
+ images: ${{ env.FULL_IMAGE_NAME }}
80
+ tags: |
81
+ type=ref,event=branch
82
+ ${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
83
+ flavor: |
84
+ prefix=cache-${{ matrix.platform }}-
85
+ latest=false
86
+
87
+ - name: Build Docker image (latest)
88
+ uses: docker/build-push-action@v5
89
+ id: build
90
+ with:
91
+ context: .
92
+ push: true
93
+ platforms: ${{ matrix.platform }}
94
+ labels: ${{ steps.meta.outputs.labels }}
95
+ outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
96
+ cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
97
+ cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
98
+ build-args: |
99
+ BUILD_HASH=${{ github.sha }}
100
+
101
+ - name: Export digest
102
+ run: |
103
+ mkdir -p /tmp/digests
104
+ digest="${{ steps.build.outputs.digest }}"
105
+ touch "/tmp/digests/${digest#sha256:}"
106
+
107
+ - name: Upload digest
108
+ uses: actions/upload-artifact@v4
109
+ with:
110
+ name: digests-main-${{ env.PLATFORM_PAIR }}
111
+ path: /tmp/digests/*
112
+ if-no-files-found: error
113
+ retention-days: 1
114
+
115
+ build-cuda-image:
116
+ runs-on: ${{ matrix.runner }}
117
+ permissions:
118
+ contents: read
119
+ packages: write
120
+ strategy:
121
+ fail-fast: false
122
+ matrix:
123
+ include:
124
+ - platform: linux/amd64
125
+ runner: ubuntu-latest
126
+ - platform: linux/arm64
127
+ runner: ubuntu-24.04-arm
128
+
129
+ steps:
130
+ # GitHub Packages requires the entire repository name to be in lowercase
131
+ # although the repository owner has a lowercase username, this prevents some people from running actions after forking
132
+ - name: Set repository and image name to lowercase
133
+ run: |
134
+ echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
135
+ echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
136
+ env:
137
+ IMAGE_NAME: '${{ github.repository }}'
138
+
139
+ - name: Prepare
140
+ run: |
141
+ platform=${{ matrix.platform }}
142
+ echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
143
+
144
+ - name: Delete huge unnecessary tools folder
145
+ run: rm -rf /opt/hostedtoolcache
146
+
147
+ - name: Checkout repository
148
+ uses: actions/checkout@v5
149
+
150
+ - name: Set up QEMU
151
+ uses: docker/setup-qemu-action@v3
152
+
153
+ - name: Set up Docker Buildx
154
+ uses: docker/setup-buildx-action@v3
155
+
156
+ - name: Log in to the Container registry
157
+ uses: docker/login-action@v3
158
+ with:
159
+ registry: ${{ env.REGISTRY }}
160
+ username: ${{ github.actor }}
161
+ password: ${{ secrets.GITHUB_TOKEN }}
162
+
163
+ - name: Extract metadata for Docker images (cuda tag)
164
+ id: meta
165
+ uses: docker/metadata-action@v5
166
+ with:
167
+ images: ${{ env.FULL_IMAGE_NAME }}
168
+ tags: |
169
+ type=ref,event=branch
170
+ type=ref,event=tag
171
+ type=sha,prefix=git-
172
+ type=semver,pattern={{version}}
173
+ type=semver,pattern={{major}}.{{minor}}
174
+ type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda
175
+ flavor: |
176
+ latest=${{ github.ref == 'refs/heads/main' }}
177
+ suffix=-cuda,onlatest=true
178
+
179
+ - name: Extract metadata for Docker cache
180
+ id: cache-meta
181
+ uses: docker/metadata-action@v5
182
+ with:
183
+ images: ${{ env.FULL_IMAGE_NAME }}
184
+ tags: |
185
+ type=ref,event=branch
186
+ ${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
187
+ flavor: |
188
+ prefix=cache-cuda-${{ matrix.platform }}-
189
+ latest=false
190
+
191
+ - name: Build Docker image (cuda)
192
+ uses: docker/build-push-action@v5
193
+ id: build
194
+ with:
195
+ context: .
196
+ push: true
197
+ platforms: ${{ matrix.platform }}
198
+ labels: ${{ steps.meta.outputs.labels }}
199
+ outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
200
+ cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
201
+ cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
202
+ build-args: |
203
+ BUILD_HASH=${{ github.sha }}
204
+ USE_CUDA=true
205
+
206
+ - name: Export digest
207
+ run: |
208
+ mkdir -p /tmp/digests
209
+ digest="${{ steps.build.outputs.digest }}"
210
+ touch "/tmp/digests/${digest#sha256:}"
211
+
212
+ - name: Upload digest
213
+ uses: actions/upload-artifact@v4
214
+ with:
215
+ name: digests-cuda-${{ env.PLATFORM_PAIR }}
216
+ path: /tmp/digests/*
217
+ if-no-files-found: error
218
+ retention-days: 1
219
+
220
+ build-cuda126-image:
221
+ runs-on: ${{ matrix.runner }}
222
+ permissions:
223
+ contents: read
224
+ packages: write
225
+ strategy:
226
+ fail-fast: false
227
+ matrix:
228
+ include:
229
+ - platform: linux/amd64
230
+ runner: ubuntu-latest
231
+ - platform: linux/arm64
232
+ runner: ubuntu-24.04-arm
233
+
234
+ steps:
235
+ # GitHub Packages requires the entire repository name to be in lowercase
236
+ # although the repository owner has a lowercase username, this prevents some people from running actions after forking
237
+ - name: Set repository and image name to lowercase
238
+ run: |
239
+ echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
240
+ echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
241
+ env:
242
+ IMAGE_NAME: '${{ github.repository }}'
243
+
244
+ - name: Prepare
245
+ run: |
246
+ platform=${{ matrix.platform }}
247
+ echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
248
+
249
+ - name: Delete huge unnecessary tools folder
250
+ run: rm -rf /opt/hostedtoolcache
251
+
252
+ - name: Checkout repository
253
+ uses: actions/checkout@v5
254
+
255
+ - name: Set up QEMU
256
+ uses: docker/setup-qemu-action@v3
257
+
258
+ - name: Set up Docker Buildx
259
+ uses: docker/setup-buildx-action@v3
260
+
261
+ - name: Log in to the Container registry
262
+ uses: docker/login-action@v3
263
+ with:
264
+ registry: ${{ env.REGISTRY }}
265
+ username: ${{ github.actor }}
266
+ password: ${{ secrets.GITHUB_TOKEN }}
267
+
268
+ - name: Extract metadata for Docker images (cuda126 tag)
269
+ id: meta
270
+ uses: docker/metadata-action@v5
271
+ with:
272
+ images: ${{ env.FULL_IMAGE_NAME }}
273
+ tags: |
274
+ type=ref,event=branch
275
+ type=ref,event=tag
276
+ type=sha,prefix=git-
277
+ type=semver,pattern={{version}}
278
+ type=semver,pattern={{major}}.{{minor}}
279
+ type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda126
280
+ flavor: |
281
+ latest=${{ github.ref == 'refs/heads/main' }}
282
+ suffix=-cuda126,onlatest=true
283
+
284
+ - name: Extract metadata for Docker cache
285
+ id: cache-meta
286
+ uses: docker/metadata-action@v5
287
+ with:
288
+ images: ${{ env.FULL_IMAGE_NAME }}
289
+ tags: |
290
+ type=ref,event=branch
291
+ ${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
292
+ flavor: |
293
+ prefix=cache-cuda126-${{ matrix.platform }}-
294
+ latest=false
295
+
296
+ - name: Build Docker image (cuda126)
297
+ uses: docker/build-push-action@v5
298
+ id: build
299
+ with:
300
+ context: .
301
+ push: true
302
+ platforms: ${{ matrix.platform }}
303
+ labels: ${{ steps.meta.outputs.labels }}
304
+ outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
305
+ cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
306
+ cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
307
+ build-args: |
308
+ BUILD_HASH=${{ github.sha }}
309
+ USE_CUDA=true
310
+ USE_CUDA_VER=cu126
311
+
312
+ - name: Export digest
313
+ run: |
314
+ mkdir -p /tmp/digests
315
+ digest="${{ steps.build.outputs.digest }}"
316
+ touch "/tmp/digests/${digest#sha256:}"
317
+
318
+ - name: Upload digest
319
+ uses: actions/upload-artifact@v4
320
+ with:
321
+ name: digests-cuda126-${{ env.PLATFORM_PAIR }}
322
+ path: /tmp/digests/*
323
+ if-no-files-found: error
324
+ retention-days: 1
325
+
326
+ build-ollama-image:
327
+ runs-on: ${{ matrix.runner }}
328
+ permissions:
329
+ contents: read
330
+ packages: write
331
+ strategy:
332
+ fail-fast: false
333
+ matrix:
334
+ include:
335
+ - platform: linux/amd64
336
+ runner: ubuntu-latest
337
+ - platform: linux/arm64
338
+ runner: ubuntu-24.04-arm
339
+
340
+ steps:
341
+ # GitHub Packages requires the entire repository name to be in lowercase
342
+ # although the repository owner has a lowercase username, this prevents some people from running actions after forking
343
+ - name: Set repository and image name to lowercase
344
+ run: |
345
+ echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
346
+ echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
347
+ env:
348
+ IMAGE_NAME: '${{ github.repository }}'
349
+
350
+ - name: Prepare
351
+ run: |
352
+ platform=${{ matrix.platform }}
353
+ echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
354
+
355
+ - name: Checkout repository
356
+ uses: actions/checkout@v5
357
+
358
+ - name: Set up QEMU
359
+ uses: docker/setup-qemu-action@v3
360
+
361
+ - name: Set up Docker Buildx
362
+ uses: docker/setup-buildx-action@v3
363
+
364
+ - name: Log in to the Container registry
365
+ uses: docker/login-action@v3
366
+ with:
367
+ registry: ${{ env.REGISTRY }}
368
+ username: ${{ github.actor }}
369
+ password: ${{ secrets.GITHUB_TOKEN }}
370
+
371
+ - name: Extract metadata for Docker images (ollama tag)
372
+ id: meta
373
+ uses: docker/metadata-action@v5
374
+ with:
375
+ images: ${{ env.FULL_IMAGE_NAME }}
376
+ tags: |
377
+ type=ref,event=branch
378
+ type=ref,event=tag
379
+ type=sha,prefix=git-
380
+ type=semver,pattern={{version}}
381
+ type=semver,pattern={{major}}.{{minor}}
382
+ type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=ollama
383
+ flavor: |
384
+ latest=${{ github.ref == 'refs/heads/main' }}
385
+ suffix=-ollama,onlatest=true
386
+
387
+ - name: Extract metadata for Docker cache
388
+ id: cache-meta
389
+ uses: docker/metadata-action@v5
390
+ with:
391
+ images: ${{ env.FULL_IMAGE_NAME }}
392
+ tags: |
393
+ type=ref,event=branch
394
+ ${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
395
+ flavor: |
396
+ prefix=cache-ollama-${{ matrix.platform }}-
397
+ latest=false
398
+
399
+ - name: Build Docker image (ollama)
400
+ uses: docker/build-push-action@v5
401
+ id: build
402
+ with:
403
+ context: .
404
+ push: true
405
+ platforms: ${{ matrix.platform }}
406
+ labels: ${{ steps.meta.outputs.labels }}
407
+ outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
408
+ cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
409
+ cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
410
+ build-args: |
411
+ BUILD_HASH=${{ github.sha }}
412
+ USE_OLLAMA=true
413
+
414
+ - name: Export digest
415
+ run: |
416
+ mkdir -p /tmp/digests
417
+ digest="${{ steps.build.outputs.digest }}"
418
+ touch "/tmp/digests/${digest#sha256:}"
419
+
420
+ - name: Upload digest
421
+ uses: actions/upload-artifact@v4
422
+ with:
423
+ name: digests-ollama-${{ env.PLATFORM_PAIR }}
424
+ path: /tmp/digests/*
425
+ if-no-files-found: error
426
+ retention-days: 1
427
+
428
+ build-slim-image:
429
+ runs-on: ${{ matrix.runner }}
430
+ permissions:
431
+ contents: read
432
+ packages: write
433
+ strategy:
434
+ fail-fast: false
435
+ matrix:
436
+ include:
437
+ - platform: linux/amd64
438
+ runner: ubuntu-latest
439
+ - platform: linux/arm64
440
+ runner: ubuntu-24.04-arm
441
+
442
+ steps:
443
+ # GitHub Packages requires the entire repository name to be in lowercase
444
+ # although the repository owner has a lowercase username, this prevents some people from running actions after forking
445
+ - name: Set repository and image name to lowercase
446
+ run: |
447
+ echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
448
+ echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
449
+ env:
450
+ IMAGE_NAME: '${{ github.repository }}'
451
+
452
+ - name: Prepare
453
+ run: |
454
+ platform=${{ matrix.platform }}
455
+ echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
456
+
457
+ - name: Checkout repository
458
+ uses: actions/checkout@v5
459
+
460
+ - name: Set up QEMU
461
+ uses: docker/setup-qemu-action@v3
462
+
463
+ - name: Set up Docker Buildx
464
+ uses: docker/setup-buildx-action@v3
465
+
466
+ - name: Log in to the Container registry
467
+ uses: docker/login-action@v3
468
+ with:
469
+ registry: ${{ env.REGISTRY }}
470
+ username: ${{ github.actor }}
471
+ password: ${{ secrets.GITHUB_TOKEN }}
472
+
473
+ - name: Extract metadata for Docker images (slim tag)
474
+ id: meta
475
+ uses: docker/metadata-action@v5
476
+ with:
477
+ images: ${{ env.FULL_IMAGE_NAME }}
478
+ tags: |
479
+ type=ref,event=branch
480
+ type=ref,event=tag
481
+ type=sha,prefix=git-
482
+ type=semver,pattern={{version}}
483
+ type=semver,pattern={{major}}.{{minor}}
484
+ type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=slim
485
+ flavor: |
486
+ latest=${{ github.ref == 'refs/heads/main' }}
487
+ suffix=-slim,onlatest=true
488
+
489
+ - name: Extract metadata for Docker cache
490
+ id: cache-meta
491
+ uses: docker/metadata-action@v5
492
+ with:
493
+ images: ${{ env.FULL_IMAGE_NAME }}
494
+ tags: |
495
+ type=ref,event=branch
496
+ ${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
497
+ flavor: |
498
+ prefix=cache-slim-${{ matrix.platform }}-
499
+ latest=false
500
+
501
+ - name: Build Docker image (slim)
502
+ uses: docker/build-push-action@v5
503
+ id: build
504
+ with:
505
+ context: .
506
+ push: true
507
+ platforms: ${{ matrix.platform }}
508
+ labels: ${{ steps.meta.outputs.labels }}
509
+ outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
510
+ cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
511
+ cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
512
+ build-args: |
513
+ BUILD_HASH=${{ github.sha }}
514
+ USE_SLIM=true
515
+
516
+ - name: Export digest
517
+ run: |
518
+ mkdir -p /tmp/digests
519
+ digest="${{ steps.build.outputs.digest }}"
520
+ touch "/tmp/digests/${digest#sha256:}"
521
+
522
+ - name: Upload digest
523
+ uses: actions/upload-artifact@v4
524
+ with:
525
+ name: digests-slim-${{ env.PLATFORM_PAIR }}
526
+ path: /tmp/digests/*
527
+ if-no-files-found: error
528
+ retention-days: 1
529
+
530
+ merge-main-images:
531
+ runs-on: ubuntu-latest
532
+ needs: [build-main-image]
533
+ steps:
534
+ # GitHub Packages requires the entire repository name to be in lowercase
535
+ # although the repository owner has a lowercase username, this prevents some people from running actions after forking
536
+ - name: Set repository and image name to lowercase
537
+ run: |
538
+ echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
539
+ echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
540
+ env:
541
+ IMAGE_NAME: '${{ github.repository }}'
542
+
543
+ - name: Download digests
544
+ uses: actions/download-artifact@v5
545
+ with:
546
+ pattern: digests-main-*
547
+ path: /tmp/digests
548
+ merge-multiple: true
549
+
550
+ - name: Set up Docker Buildx
551
+ uses: docker/setup-buildx-action@v3
552
+
553
+ - name: Log in to the Container registry
554
+ uses: docker/login-action@v3
555
+ with:
556
+ registry: ${{ env.REGISTRY }}
557
+ username: ${{ github.actor }}
558
+ password: ${{ secrets.GITHUB_TOKEN }}
559
+
560
+ - name: Extract metadata for Docker images (default latest tag)
561
+ id: meta
562
+ uses: docker/metadata-action@v5
563
+ with:
564
+ images: ${{ env.FULL_IMAGE_NAME }}
565
+ tags: |
566
+ type=ref,event=branch
567
+ type=ref,event=tag
568
+ type=sha,prefix=git-
569
+ type=semver,pattern={{version}}
570
+ type=semver,pattern={{major}}.{{minor}}
571
+ flavor: |
572
+ latest=${{ github.ref == 'refs/heads/main' }}
573
+
574
+ - name: Create manifest list and push
575
+ working-directory: /tmp/digests
576
+ run: |
577
+ docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
578
+ $(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
579
+
580
+ - name: Inspect image
581
+ run: |
582
+ docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
583
+
584
+ merge-cuda-images:
585
+ runs-on: ubuntu-latest
586
+ needs: [build-cuda-image]
587
+ steps:
588
+ # GitHub Packages requires the entire repository name to be in lowercase
589
+ # although the repository owner has a lowercase username, this prevents some people from running actions after forking
590
+ - name: Set repository and image name to lowercase
591
+ run: |
592
+ echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
593
+ echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
594
+ env:
595
+ IMAGE_NAME: '${{ github.repository }}'
596
+
597
+ - name: Download digests
598
+ uses: actions/download-artifact@v5
599
+ with:
600
+ pattern: digests-cuda-*
601
+ path: /tmp/digests
602
+ merge-multiple: true
603
+
604
+ - name: Set up Docker Buildx
605
+ uses: docker/setup-buildx-action@v3
606
+
607
+ - name: Log in to the Container registry
608
+ uses: docker/login-action@v3
609
+ with:
610
+ registry: ${{ env.REGISTRY }}
611
+ username: ${{ github.actor }}
612
+ password: ${{ secrets.GITHUB_TOKEN }}
613
+
614
+ - name: Extract metadata for Docker images (default latest tag)
615
+ id: meta
616
+ uses: docker/metadata-action@v5
617
+ with:
618
+ images: ${{ env.FULL_IMAGE_NAME }}
619
+ tags: |
620
+ type=ref,event=branch
621
+ type=ref,event=tag
622
+ type=sha,prefix=git-
623
+ type=semver,pattern={{version}}
624
+ type=semver,pattern={{major}}.{{minor}}
625
+ type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda
626
+ flavor: |
627
+ latest=${{ github.ref == 'refs/heads/main' }}
628
+ suffix=-cuda,onlatest=true
629
+
630
+ - name: Create manifest list and push
631
+ working-directory: /tmp/digests
632
+ run: |
633
+ docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
634
+ $(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
635
+
636
+ - name: Inspect image
637
+ run: |
638
+ docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
639
+
640
+ merge-cuda126-images:
641
+ runs-on: ubuntu-latest
642
+ needs: [build-cuda126-image]
643
+ steps:
644
+ # GitHub Packages requires the entire repository name to be in lowercase
645
+ # although the repository owner has a lowercase username, this prevents some people from running actions after forking
646
+ - name: Set repository and image name to lowercase
647
+ run: |
648
+ echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
649
+ echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
650
+ env:
651
+ IMAGE_NAME: '${{ github.repository }}'
652
+
653
+ - name: Download digests
654
+ uses: actions/download-artifact@v5
655
+ with:
656
+ pattern: digests-cuda126-*
657
+ path: /tmp/digests
658
+ merge-multiple: true
659
+
660
+ - name: Set up Docker Buildx
661
+ uses: docker/setup-buildx-action@v3
662
+
663
+ - name: Log in to the Container registry
664
+ uses: docker/login-action@v3
665
+ with:
666
+ registry: ${{ env.REGISTRY }}
667
+ username: ${{ github.actor }}
668
+ password: ${{ secrets.GITHUB_TOKEN }}
669
+
670
+ - name: Extract metadata for Docker images (default latest tag)
671
+ id: meta
672
+ uses: docker/metadata-action@v5
673
+ with:
674
+ images: ${{ env.FULL_IMAGE_NAME }}
675
+ tags: |
676
+ type=ref,event=branch
677
+ type=ref,event=tag
678
+ type=sha,prefix=git-
679
+ type=semver,pattern={{version}}
680
+ type=semver,pattern={{major}}.{{minor}}
681
+ type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda126
682
+ flavor: |
683
+ latest=${{ github.ref == 'refs/heads/main' }}
684
+ suffix=-cuda126,onlatest=true
685
+
686
+ - name: Create manifest list and push
687
+ working-directory: /tmp/digests
688
+ run: |
689
+ docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
690
+ $(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
691
+
692
+ - name: Inspect image
693
+ run: |
694
+ docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
695
+
696
+ merge-ollama-images:
697
+ runs-on: ubuntu-latest
698
+ needs: [build-ollama-image]
699
+ steps:
700
+ # GitHub Packages requires the entire repository name to be in lowercase
701
+ # although the repository owner has a lowercase username, this prevents some people from running actions after forking
702
+ - name: Set repository and image name to lowercase
703
+ run: |
704
+ echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
705
+ echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
706
+ env:
707
+ IMAGE_NAME: '${{ github.repository }}'
708
+
709
+ - name: Download digests
710
+ uses: actions/download-artifact@v5
711
+ with:
712
+ pattern: digests-ollama-*
713
+ path: /tmp/digests
714
+ merge-multiple: true
715
+
716
+ - name: Set up Docker Buildx
717
+ uses: docker/setup-buildx-action@v3
718
+
719
+ - name: Log in to the Container registry
720
+ uses: docker/login-action@v3
721
+ with:
722
+ registry: ${{ env.REGISTRY }}
723
+ username: ${{ github.actor }}
724
+ password: ${{ secrets.GITHUB_TOKEN }}
725
+
726
+ - name: Extract metadata for Docker images (default ollama tag)
727
+ id: meta
728
+ uses: docker/metadata-action@v5
729
+ with:
730
+ images: ${{ env.FULL_IMAGE_NAME }}
731
+ tags: |
732
+ type=ref,event=branch
733
+ type=ref,event=tag
734
+ type=sha,prefix=git-
735
+ type=semver,pattern={{version}}
736
+ type=semver,pattern={{major}}.{{minor}}
737
+ type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=ollama
738
+ flavor: |
739
+ latest=${{ github.ref == 'refs/heads/main' }}
740
+ suffix=-ollama,onlatest=true
741
+
742
+ - name: Create manifest list and push
743
+ working-directory: /tmp/digests
744
+ run: |
745
+ docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
746
+ $(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
747
+
748
+ - name: Inspect image
749
+ run: |
750
+ docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
751
+
752
+ merge-slim-images:
753
+ runs-on: ubuntu-latest
754
+ needs: [build-slim-image]
755
+ steps:
756
+ # GitHub Packages requires the entire repository name to be in lowercase
757
+ # although the repository owner has a lowercase username, this prevents some people from running actions after forking
758
+ - name: Set repository and image name to lowercase
759
+ run: |
760
+ echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
761
+ echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
762
+ env:
763
+ IMAGE_NAME: '${{ github.repository }}'
764
+
765
+ - name: Download digests
766
+ uses: actions/download-artifact@v5
767
+ with:
768
+ pattern: digests-slim-*
769
+ path: /tmp/digests
770
+ merge-multiple: true
771
+
772
+ - name: Set up Docker Buildx
773
+ uses: docker/setup-buildx-action@v3
774
+
775
+ - name: Log in to the Container registry
776
+ uses: docker/login-action@v3
777
+ with:
778
+ registry: ${{ env.REGISTRY }}
779
+ username: ${{ github.actor }}
780
+ password: ${{ secrets.GITHUB_TOKEN }}
781
+
782
+ - name: Extract metadata for Docker images (default slim tag)
783
+ id: meta
784
+ uses: docker/metadata-action@v5
785
+ with:
786
+ images: ${{ env.FULL_IMAGE_NAME }}
787
+ tags: |
788
+ type=ref,event=branch
789
+ type=ref,event=tag
790
+ type=sha,prefix=git-
791
+ type=semver,pattern={{version}}
792
+ type=semver,pattern={{major}}.{{minor}}
793
+ type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=slim
794
+ flavor: |
795
+ latest=${{ github.ref == 'refs/heads/main' }}
796
+ suffix=-slim,onlatest=true
797
+
798
+ - name: Create manifest list and push
799
+ working-directory: /tmp/digests
800
+ run: |
801
+ docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
802
+ $(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
803
+
804
+ - name: Inspect image
805
+ run: |
806
+ docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
.github/workflows/format-backend.yaml ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Python CI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ - dev
8
+ paths:
9
+ - 'backend/**'
10
+ - 'pyproject.toml'
11
+ - 'uv.lock'
12
+ pull_request:
13
+ branches:
14
+ - main
15
+ - dev
16
+ paths:
17
+ - 'backend/**'
18
+ - 'pyproject.toml'
19
+ - 'uv.lock'
20
+
21
+ jobs:
22
+ build:
23
+ name: 'Format Backend'
24
+ runs-on: ubuntu-latest
25
+
26
+ strategy:
27
+ matrix:
28
+ python-version:
29
+ - 3.11.x
30
+ - 3.12.x
31
+
32
+ steps:
33
+ - uses: actions/checkout@v5
34
+
35
+ - name: Set up Python
36
+ uses: actions/setup-python@v6
37
+ with:
38
+ python-version: '${{ matrix.python-version }}'
39
+
40
+ - name: Install dependencies
41
+ run: |
42
+ python -m pip install --upgrade pip
43
+ pip install black
44
+
45
+ - name: Format backend
46
+ run: npm run format:backend
47
+
48
+ - name: Check for changes after format
49
+ run: git diff --exit-code
.github/workflows/format-build-frontend.yaml ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Frontend Build
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ - dev
8
+ paths-ignore:
9
+ - 'backend/**'
10
+ - 'pyproject.toml'
11
+ - 'uv.lock'
12
+ pull_request:
13
+ branches:
14
+ - main
15
+ - dev
16
+ paths-ignore:
17
+ - 'backend/**'
18
+ - 'pyproject.toml'
19
+ - 'uv.lock'
20
+
21
+ jobs:
22
+ build:
23
+ name: 'Format & Build Frontend'
24
+ runs-on: ubuntu-latest
25
+ steps:
26
+ - name: Checkout Repository
27
+ uses: actions/checkout@v5
28
+
29
+ - name: Setup Node.js
30
+ uses: actions/setup-node@v5
31
+ with:
32
+ node-version: '22'
33
+
34
+ - name: Install Dependencies
35
+ run: npm install --force
36
+
37
+ - name: Format Frontend
38
+ run: npm run format
39
+
40
+ - name: Run i18next
41
+ run: npm run i18n:parse
42
+
43
+ - name: Check for Changes After Format
44
+ run: git diff --exit-code
45
+
46
+ - name: Build Frontend
47
+ run: npm run build
48
+
49
+ test-frontend:
50
+ name: 'Frontend Unit Tests'
51
+ runs-on: ubuntu-latest
52
+ steps:
53
+ - name: Checkout Repository
54
+ uses: actions/checkout@v5
55
+
56
+ - name: Setup Node.js
57
+ uses: actions/setup-node@v5
58
+ with:
59
+ node-version: '22'
60
+
61
+ - name: Install Dependencies
62
+ run: npm ci --force
63
+
64
+ - name: Run vitest
65
+ run: npm run test:frontend
.github/workflows/integration-test.disabled ADDED
@@ -0,0 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Integration Test
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ - dev
8
+ pull_request:
9
+ branches:
10
+ - main
11
+ - dev
12
+
13
+ jobs:
14
+ cypress-run:
15
+ name: Run Cypress Integration Tests
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - name: Maximize build space
19
+ uses: AdityaGarg8/remove-unwanted-software@v4.1
20
+ with:
21
+ remove-android: 'true'
22
+ remove-haskell: 'true'
23
+ remove-codeql: 'true'
24
+
25
+ - name: Checkout Repository
26
+ uses: actions/checkout@v4
27
+
28
+ - name: Build and run Compose Stack
29
+ run: |
30
+ docker compose \
31
+ --file docker-compose.yaml \
32
+ --file docker-compose.api.yaml \
33
+ --file docker-compose.a1111-test.yaml \
34
+ up --detach --build
35
+
36
+ - name: Delete Docker build cache
37
+ run: |
38
+ docker builder prune --all --force
39
+
40
+ - name: Wait for Ollama to be up
41
+ timeout-minutes: 5
42
+ run: |
43
+ until curl --output /dev/null --silent --fail http://localhost:11434; do
44
+ printf '.'
45
+ sleep 1
46
+ done
47
+ echo "Service is up!"
48
+
49
+ - name: Preload Ollama model
50
+ run: |
51
+ docker exec ollama ollama pull qwen:0.5b-chat-v1.5-q2_K
52
+
53
+ - name: Cypress run
54
+ uses: cypress-io/github-action@v6
55
+ env:
56
+ LIBGL_ALWAYS_SOFTWARE: 1
57
+ with:
58
+ browser: chrome
59
+ wait-on: 'http://localhost:3000'
60
+ config: baseUrl=http://localhost:3000
61
+
62
+ - uses: actions/upload-artifact@v4
63
+ if: always()
64
+ name: Upload Cypress videos
65
+ with:
66
+ name: cypress-videos
67
+ path: cypress/videos
68
+ if-no-files-found: ignore
69
+
70
+ - name: Extract Compose logs
71
+ if: always()
72
+ run: |
73
+ docker compose logs > compose-logs.txt
74
+
75
+ - uses: actions/upload-artifact@v4
76
+ if: always()
77
+ name: Upload Compose logs
78
+ with:
79
+ name: compose-logs
80
+ path: compose-logs.txt
81
+ if-no-files-found: ignore
82
+
83
+ # pytest:
84
+ # name: Run Backend Tests
85
+ # runs-on: ubuntu-latest
86
+ # steps:
87
+ # - uses: actions/checkout@v4
88
+
89
+ # - name: Set up Python
90
+ # uses: actions/setup-python@v5
91
+ # with:
92
+ # python-version: ${{ matrix.python-version }}
93
+
94
+ # - name: Install dependencies
95
+ # run: |
96
+ # python -m pip install --upgrade pip
97
+ # pip install -r backend/requirements.txt
98
+
99
+ # - name: pytest run
100
+ # run: |
101
+ # ls -al
102
+ # cd backend
103
+ # PYTHONPATH=. pytest . -o log_cli=true -o log_cli_level=INFO
104
+
105
+ migration_test:
106
+ name: Run Migration Tests
107
+ runs-on: ubuntu-latest
108
+ services:
109
+ postgres:
110
+ image: postgres
111
+ env:
112
+ POSTGRES_PASSWORD: postgres
113
+ options: >-
114
+ --health-cmd pg_isready
115
+ --health-interval 10s
116
+ --health-timeout 5s
117
+ --health-retries 5
118
+ ports:
119
+ - 5432:5432
120
+ # mysql:
121
+ # image: mysql
122
+ # env:
123
+ # MYSQL_ROOT_PASSWORD: mysql
124
+ # MYSQL_DATABASE: mysql
125
+ # options: >-
126
+ # --health-cmd "mysqladmin ping -h localhost"
127
+ # --health-interval 10s
128
+ # --health-timeout 5s
129
+ # --health-retries 5
130
+ # ports:
131
+ # - 3306:3306
132
+ steps:
133
+ - name: Checkout Repository
134
+ uses: actions/checkout@v4
135
+
136
+ - name: Set up Python
137
+ uses: actions/setup-python@v5
138
+ with:
139
+ python-version: ${{ matrix.python-version }}
140
+
141
+ - name: Set up uv
142
+ uses: yezz123/setup-uv@v4
143
+ with:
144
+ uv-venv: venv
145
+
146
+ - name: Activate virtualenv
147
+ run: |
148
+ . venv/bin/activate
149
+ echo PATH=$PATH >> $GITHUB_ENV
150
+
151
+ - name: Install dependencies
152
+ run: |
153
+ uv pip install -r backend/requirements.txt
154
+
155
+ - name: Test backend with SQLite
156
+ id: sqlite
157
+ env:
158
+ WEBUI_SECRET_KEY: secret-key
159
+ GLOBAL_LOG_LEVEL: debug
160
+ run: |
161
+ cd backend
162
+ uvicorn open_webui.main:app --port "8080" --forwarded-allow-ips '*' &
163
+ UVICORN_PID=$!
164
+ # Wait up to 40 seconds for the server to start
165
+ for i in {1..40}; do
166
+ curl -s http://localhost:8080/api/config > /dev/null && break
167
+ sleep 1
168
+ if [ $i -eq 40 ]; then
169
+ echo "Server failed to start"
170
+ kill -9 $UVICORN_PID
171
+ exit 1
172
+ fi
173
+ done
174
+ # Check that the server is still running after 5 seconds
175
+ sleep 5
176
+ if ! kill -0 $UVICORN_PID; then
177
+ echo "Server has stopped"
178
+ exit 1
179
+ fi
180
+
181
+ - name: Test backend with Postgres
182
+ if: success() || steps.sqlite.conclusion == 'failure'
183
+ env:
184
+ WEBUI_SECRET_KEY: secret-key
185
+ GLOBAL_LOG_LEVEL: debug
186
+ DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
187
+ DATABASE_POOL_SIZE: 10
188
+ DATABASE_POOL_MAX_OVERFLOW: 10
189
+ DATABASE_POOL_TIMEOUT: 30
190
+ run: |
191
+ cd backend
192
+ uvicorn open_webui.main:app --port "8081" --forwarded-allow-ips '*' &
193
+ UVICORN_PID=$!
194
+ # Wait up to 20 seconds for the server to start
195
+ for i in {1..20}; do
196
+ curl -s http://localhost:8081/api/config > /dev/null && break
197
+ sleep 1
198
+ if [ $i -eq 20 ]; then
199
+ echo "Server failed to start"
200
+ kill -9 $UVICORN_PID
201
+ exit 1
202
+ fi
203
+ done
204
+ # Check that the server is still running after 5 seconds
205
+ sleep 5
206
+ if ! kill -0 $UVICORN_PID; then
207
+ echo "Server has stopped"
208
+ exit 1
209
+ fi
210
+
211
+ # Check that service will reconnect to postgres when connection will be closed
212
+ status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health/db)
213
+ if [[ "$status_code" -ne 200 ]] ; then
214
+ echo "Server has failed before postgres reconnect check"
215
+ exit 1
216
+ fi
217
+
218
+ echo "Terminating all connections to postgres..."
219
+ python -c "import os, psycopg2 as pg2; \
220
+ conn = pg2.connect(dsn=os.environ['DATABASE_URL'].replace('+pool', '')); \
221
+ cur = conn.cursor(); \
222
+ cur.execute('SELECT pg_terminate_backend(psa.pid) FROM pg_stat_activity psa WHERE datname = current_database() AND pid <> pg_backend_pid();')"
223
+
224
+ status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health/db)
225
+ if [[ "$status_code" -ne 200 ]] ; then
226
+ echo "Server has not reconnected to postgres after connection was closed: returned status $status_code"
227
+ exit 1
228
+ fi
229
+
230
+ # - name: Test backend with MySQL
231
+ # if: success() || steps.sqlite.conclusion == 'failure' || steps.postgres.conclusion == 'failure'
232
+ # env:
233
+ # WEBUI_SECRET_KEY: secret-key
234
+ # GLOBAL_LOG_LEVEL: debug
235
+ # DATABASE_URL: mysql://root:mysql@localhost:3306/mysql
236
+ # run: |
237
+ # cd backend
238
+ # uvicorn open_webui.main:app --port "8083" --forwarded-allow-ips '*' &
239
+ # UVICORN_PID=$!
240
+ # # Wait up to 20 seconds for the server to start
241
+ # for i in {1..20}; do
242
+ # curl -s http://localhost:8083/api/config > /dev/null && break
243
+ # sleep 1
244
+ # if [ $i -eq 20 ]; then
245
+ # echo "Server failed to start"
246
+ # kill -9 $UVICORN_PID
247
+ # exit 1
248
+ # fi
249
+ # done
250
+ # # Check that the server is still running after 5 seconds
251
+ # sleep 5
252
+ # if ! kill -0 $UVICORN_PID; then
253
+ # echo "Server has stopped"
254
+ # exit 1
255
+ # fi
.github/workflows/lint-backend.disabled ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Python CI
2
+ on:
3
+ push:
4
+ branches: ['main']
5
+ pull_request:
6
+ jobs:
7
+ build:
8
+ name: 'Lint Backend'
9
+ env:
10
+ PUBLIC_API_BASE_URL: ''
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ node-version:
15
+ - latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - name: Use Python
19
+ uses: actions/setup-python@v5
20
+ - name: Use Bun
21
+ uses: oven-sh/setup-bun@v1
22
+ - name: Install dependencies
23
+ run: |
24
+ python -m pip install --upgrade pip
25
+ pip install pylint
26
+ - name: Lint backend
27
+ run: bun run lint:backend
.github/workflows/lint-frontend.disabled ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Bun CI
2
+ on:
3
+ push:
4
+ branches: ['main']
5
+ pull_request:
6
+ jobs:
7
+ build:
8
+ name: 'Lint Frontend'
9
+ env:
10
+ PUBLIC_API_BASE_URL: ''
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - name: Use Bun
15
+ uses: oven-sh/setup-bun@v1
16
+ - run: bun --version
17
+ - name: Install frontend dependencies
18
+ run: bun install --frozen-lockfile
19
+ - run: bun run lint:frontend
20
+ - run: bun run lint:types
21
+ if: success() || failure()
.github/workflows/release-pypi.yml ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Release to PyPI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main # or whatever branch you want to use
7
+ - pypi-release
8
+
9
+ jobs:
10
+ release:
11
+ runs-on: ubuntu-latest
12
+ environment:
13
+ name: pypi
14
+ url: https://pypi.org/p/open-webui
15
+ permissions:
16
+ id-token: write
17
+ steps:
18
+ - name: Checkout repository
19
+ uses: actions/checkout@v5
20
+ with:
21
+ fetch-depth: 0
22
+ - name: Install Git
23
+ run: sudo apt-get update && sudo apt-get install -y git
24
+ - uses: actions/setup-node@v5
25
+ with:
26
+ node-version: 22
27
+ - uses: actions/setup-python@v6
28
+ with:
29
+ python-version: 3.11
30
+ - name: Build
31
+ run: |
32
+ python -m pip install --upgrade pip
33
+ pip install build
34
+ python -m build .
35
+ - name: Publish package distributions to PyPI
36
+ uses: pypa/gh-action-pypi-publish@release/v1
.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,314 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ignore files for PNPM, NPM and YARN
2
+ pnpm-lock.yaml
3
+ package-lock.json
4
+ yarn.lock
5
+
6
+ # Copy of .gitignore
7
+ .DS_Store
8
+ node_modules
9
+ /build
10
+ /.svelte-kit
11
+ /package
12
+ .env
13
+ .env.*
14
+ !.env.example
15
+ vite.config.js.timestamp-*
16
+ vite.config.ts.timestamp-*
17
+ # Byte-compiled / optimized / DLL files
18
+ __pycache__/
19
+ *.py[cod]
20
+ *$py.class
21
+
22
+ # C extensions
23
+ *.so
24
+
25
+ # Distribution / packaging
26
+ .Python
27
+ build/
28
+ develop-eggs/
29
+ dist/
30
+ downloads/
31
+ eggs/
32
+ .eggs/
33
+ lib64/
34
+ parts/
35
+ sdist/
36
+ var/
37
+ wheels/
38
+ share/python-wheels/
39
+ *.egg-info/
40
+ .installed.cfg
41
+ *.egg
42
+ MANIFEST
43
+
44
+ # PyInstaller
45
+ # Usually these files are written by a python script from a template
46
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
47
+ *.manifest
48
+ *.spec
49
+
50
+ # Installer logs
51
+ pip-log.txt
52
+ pip-delete-this-directory.txt
53
+
54
+ # Unit test / coverage reports
55
+ htmlcov/
56
+ .tox/
57
+ .nox/
58
+ .coverage
59
+ .coverage.*
60
+ .cache
61
+ nosetests.xml
62
+ coverage.xml
63
+ *.cover
64
+ *.py,cover
65
+ .hypothesis/
66
+ .pytest_cache/
67
+ cover/
68
+
69
+ # Translations
70
+ *.mo
71
+ *.pot
72
+
73
+ # Django stuff:
74
+ *.log
75
+ local_settings.py
76
+ db.sqlite3
77
+ db.sqlite3-journal
78
+
79
+ # Flask stuff:
80
+ instance/
81
+ .webassets-cache
82
+
83
+ # Scrapy stuff:
84
+ .scrapy
85
+
86
+ # Sphinx documentation
87
+ docs/_build/
88
+
89
+ # PyBuilder
90
+ .pybuilder/
91
+ target/
92
+
93
+ # Jupyter Notebook
94
+ .ipynb_checkpoints
95
+
96
+ # IPython
97
+ profile_default/
98
+ ipython_config.py
99
+
100
+ # pyenv
101
+ # For a library or package, you might want to ignore these files since the code is
102
+ # intended to run in multiple environments; otherwise, check them in:
103
+ # .python-version
104
+
105
+ # pipenv
106
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
107
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
108
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
109
+ # install all needed dependencies.
110
+ #Pipfile.lock
111
+
112
+ # poetry
113
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
114
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
115
+ # commonly ignored for libraries.
116
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
117
+ #poetry.lock
118
+
119
+ # pdm
120
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
121
+ #pdm.lock
122
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
123
+ # in version control.
124
+ # https://pdm.fming.dev/#use-with-ide
125
+ .pdm.toml
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # SageMath parsed files
135
+ *.sage.py
136
+
137
+ # Environments
138
+ .env
139
+ .venv
140
+ env/
141
+ venv/
142
+ ENV/
143
+ env.bak/
144
+ venv.bak/
145
+
146
+ # Spyder project settings
147
+ .spyderproject
148
+ .spyproject
149
+
150
+ # Rope project settings
151
+ .ropeproject
152
+
153
+ # mkdocs documentation
154
+ /site
155
+
156
+ # mypy
157
+ .mypy_cache/
158
+ .dmypy.json
159
+ dmypy.json
160
+
161
+ # Pyre type checker
162
+ .pyre/
163
+
164
+ # pytype static type analyzer
165
+ .pytype/
166
+
167
+ # Cython debug symbols
168
+ cython_debug/
169
+
170
+ # PyCharm
171
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
172
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
173
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
174
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
175
+ .idea/
176
+
177
+ # Logs
178
+ logs
179
+ *.log
180
+ npm-debug.log*
181
+ yarn-debug.log*
182
+ yarn-error.log*
183
+ lerna-debug.log*
184
+ .pnpm-debug.log*
185
+
186
+ # Diagnostic reports (https://nodejs.org/api/report.html)
187
+ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
188
+
189
+ # Runtime data
190
+ pids
191
+ *.pid
192
+ *.seed
193
+ *.pid.lock
194
+
195
+ # Directory for instrumented libs generated by jscoverage/JSCover
196
+ lib-cov
197
+
198
+ # Coverage directory used by tools like istanbul
199
+ coverage
200
+ *.lcov
201
+
202
+ # nyc test coverage
203
+ .nyc_output
204
+
205
+ # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
206
+ .grunt
207
+
208
+ # Bower dependency directory (https://bower.io/)
209
+ bower_components
210
+
211
+ # node-waf configuration
212
+ .lock-wscript
213
+
214
+ # Compiled binary addons (https://nodejs.org/api/addons.html)
215
+ build/Release
216
+
217
+ # Dependency directories
218
+ node_modules/
219
+ jspm_packages/
220
+
221
+ # Snowpack dependency directory (https://snowpack.dev/)
222
+ web_modules/
223
+
224
+ # TypeScript cache
225
+ *.tsbuildinfo
226
+
227
+ # Optional npm cache directory
228
+ .npm
229
+
230
+ # Optional eslint cache
231
+ .eslintcache
232
+
233
+ # Optional stylelint cache
234
+ .stylelintcache
235
+
236
+ # Microbundle cache
237
+ .rpt2_cache/
238
+ .rts2_cache_cjs/
239
+ .rts2_cache_es/
240
+ .rts2_cache_umd/
241
+
242
+ # Optional REPL history
243
+ .node_repl_history
244
+
245
+ # Output of 'npm pack'
246
+ *.tgz
247
+
248
+ # Yarn Integrity file
249
+ .yarn-integrity
250
+
251
+ # dotenv environment variable files
252
+ .env
253
+ .env.development.local
254
+ .env.test.local
255
+ .env.production.local
256
+ .env.local
257
+
258
+ # parcel-bundler cache (https://parceljs.org/)
259
+ .cache
260
+ .parcel-cache
261
+
262
+ # Next.js build output
263
+ .next
264
+ out
265
+
266
+ # Nuxt.js build / generate output
267
+ .nuxt
268
+ dist
269
+
270
+ # Gatsby files
271
+ .cache/
272
+ # Comment in the public line in if your project uses Gatsby and not Next.js
273
+ # https://nextjs.org/blog/next-9-1#public-directory-support
274
+ # public
275
+
276
+ # vuepress build output
277
+ .vuepress/dist
278
+
279
+ # vuepress v2.x temp and cache directory
280
+ .temp
281
+ .cache
282
+
283
+ # Docusaurus cache and generated files
284
+ .docusaurus
285
+
286
+ # Serverless directories
287
+ .serverless/
288
+
289
+ # FuseBox cache
290
+ .fusebox/
291
+
292
+ # DynamoDB Local files
293
+ .dynamodb/
294
+
295
+ # TernJS port file
296
+ .tern-port
297
+
298
+ # Stores VSCode versions used for testing VSCode extensions
299
+ .vscode-test
300
+
301
+ # yarn v2
302
+ .yarn/cache
303
+ .yarn/unplugged
304
+ .yarn/build-state.yml
305
+ .yarn/install-state.gz
306
+ .pnp.*
307
+
308
+ # cypress artifacts
309
+ cypress/videos
310
+ cypress/screenshots
311
+
312
+
313
+
314
+ /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 **hello@openwebui.com**. 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,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ ARG USE_SLIM=false
7
+ ARG USE_PERMISSION_HARDENING=false
8
+ # Tested with cu117 for CUDA 11 and cu121 for CUDA 12 (default)
9
+ ARG USE_CUDA_VER=cu128
10
+ # any sentence transformer model; models to use can be found at https://huggingface.co/models?library=sentence-transformers
11
+ # Leaderboard: https://huggingface.co/spaces/mteb/leaderboard
12
+ # for better performance and multilangauge support use "intfloat/multilingual-e5-large" (~2.5GB) or "intfloat/multilingual-e5-base" (~1.5GB)
13
+ # 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.
14
+ ARG USE_EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
15
+ ARG USE_RERANKING_MODEL=""
16
+ ARG USE_AUXILIARY_EMBEDDING_MODEL=TaylorAI/bge-micro-v2
17
+
18
+ # Tiktoken encoding name; models to use can be found at https://huggingface.co/models?library=tiktoken
19
+ ARG USE_TIKTOKEN_ENCODING_NAME="cl100k_base"
20
+
21
+ ARG BUILD_HASH=dev-build
22
+ # Override at your own risk - non-root configurations are untested
23
+ ARG UID=0
24
+ ARG GID=0
25
+
26
+ ######## WebUI frontend ########
27
+ FROM --platform=$BUILDPLATFORM node:22-alpine3.20 AS build
28
+ ARG BUILD_HASH
29
+
30
+ # Set Node.js options (heap limit Allocation failed - JavaScript heap out of memory)
31
+ # ENV NODE_OPTIONS="--max-old-space-size=4096"
32
+
33
+ WORKDIR /app
34
+
35
+ # to store git revision in build
36
+ RUN apk add --no-cache git
37
+
38
+ COPY package.json package-lock.json ./
39
+ RUN npm ci --force
40
+
41
+ COPY . .
42
+ ENV APP_BUILD_HASH=${BUILD_HASH}
43
+ RUN npm run build
44
+
45
+ ######## WebUI backend ########
46
+ FROM python:3.11.14-slim-bookworm AS base
47
+
48
+ # Use args
49
+ ARG USE_CUDA
50
+ ARG USE_OLLAMA
51
+ ARG USE_CUDA_VER
52
+ ARG USE_SLIM
53
+ ARG USE_PERMISSION_HARDENING
54
+ ARG USE_EMBEDDING_MODEL
55
+ ARG USE_RERANKING_MODEL
56
+ ARG USE_AUXILIARY_EMBEDDING_MODEL
57
+ ARG UID
58
+ ARG GID
59
+
60
+ # Python settings
61
+ ENV PYTHONUNBUFFERED=1
62
+
63
+ ## Basis ##
64
+ ENV ENV=prod \
65
+ PORT=8080 \
66
+ # pass build args to the build
67
+ USE_OLLAMA_DOCKER=${USE_OLLAMA} \
68
+ USE_CUDA_DOCKER=${USE_CUDA} \
69
+ USE_SLIM_DOCKER=${USE_SLIM} \
70
+ USE_CUDA_DOCKER_VER=${USE_CUDA_VER} \
71
+ USE_EMBEDDING_MODEL_DOCKER=${USE_EMBEDDING_MODEL} \
72
+ USE_RERANKING_MODEL_DOCKER=${USE_RERANKING_MODEL} \
73
+ USE_AUXILIARY_EMBEDDING_MODEL_DOCKER=${USE_AUXILIARY_EMBEDDING_MODEL}
74
+
75
+ ## Basis URL Config ##
76
+ ENV OLLAMA_BASE_URL="/ollama" \
77
+ OPENAI_API_BASE_URL=""
78
+
79
+ ## API Key and Security Config ##
80
+ ENV OPENAI_API_KEY="" \
81
+ WEBUI_SECRET_KEY="" \
82
+ SCARF_NO_ANALYTICS=true \
83
+ DO_NOT_TRACK=true \
84
+ ANONYMIZED_TELEMETRY=false
85
+
86
+ #### Other models #########################################################
87
+ ## whisper TTS model settings ##
88
+ ENV WHISPER_MODEL="base" \
89
+ WHISPER_MODEL_DIR="/app/backend/data/cache/whisper/models"
90
+
91
+ ## RAG Embedding model settings ##
92
+ ENV RAG_EMBEDDING_MODEL="$USE_EMBEDDING_MODEL_DOCKER" \
93
+ RAG_RERANKING_MODEL="$USE_RERANKING_MODEL_DOCKER" \
94
+ AUXILIARY_EMBEDDING_MODEL="$USE_AUXILIARY_EMBEDDING_MODEL_DOCKER" \
95
+ SENTENCE_TRANSFORMERS_HOME="/app/backend/data/cache/embedding/models"
96
+
97
+ ## Tiktoken model settings ##
98
+ ENV TIKTOKEN_ENCODING_NAME="cl100k_base" \
99
+ TIKTOKEN_CACHE_DIR="/app/backend/data/cache/tiktoken"
100
+
101
+ ## Hugging Face download cache ##
102
+ ENV HF_HOME="/app/backend/data/cache/embedding/models"
103
+
104
+ ## Torch Extensions ##
105
+ # ENV TORCH_EXTENSIONS_DIR="/.cache/torch_extensions"
106
+
107
+ #### Other models ##########################################################
108
+
109
+ WORKDIR /app/backend
110
+
111
+ ENV HOME=/root
112
+ # Create user and group if not root
113
+ RUN if [ $UID -ne 0 ]; then \
114
+ if [ $GID -ne 0 ]; then \
115
+ addgroup --gid $GID app; \
116
+ fi; \
117
+ adduser --uid $UID --gid $GID --home $HOME --disabled-password --no-create-home app; \
118
+ fi
119
+
120
+ RUN mkdir -p $HOME/.cache/chroma
121
+ RUN echo -n 00000000-0000-0000-0000-000000000000 > $HOME/.cache/chroma/telemetry_user_id
122
+
123
+ # Make sure the user has access to the app and root directory
124
+ RUN chown -R $UID:$GID /app $HOME
125
+
126
+ # Install common system dependencies
127
+ RUN apt-get update && \
128
+ apt-get install -y --no-install-recommends \
129
+ git build-essential pandoc gcc netcat-openbsd curl jq \
130
+ python3-dev \
131
+ ffmpeg libsm6 libxext6 zstd \
132
+ && rm -rf /var/lib/apt/lists/*
133
+
134
+ # install python dependencies
135
+ COPY --chown=$UID:$GID ./backend/requirements.txt ./requirements.txt
136
+
137
+ RUN pip3 install --no-cache-dir uv && \
138
+ if [ "$USE_CUDA" = "true" ]; then \
139
+ # If you use CUDA the whisper and embedding model will be downloaded on first use
140
+ pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir && \
141
+ uv pip install --system -r requirements.txt --no-cache-dir && \
142
+ python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
143
+ python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ.get('AUXILIARY_EMBEDDING_MODEL', 'TaylorAI/bge-micro-v2'), device='cpu')" && \
144
+ 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'])"; \
145
+ python -c "import os; import tiktoken; tiktoken.get_encoding(os.environ['TIKTOKEN_ENCODING_NAME'])"; \
146
+ python -c "import nltk; nltk.download('punkt_tab')"; \
147
+ else \
148
+ pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \
149
+ uv pip install --system -r requirements.txt --no-cache-dir && \
150
+ if [ "$USE_SLIM" != "true" ]; then \
151
+ python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
152
+ python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ.get('AUXILIARY_EMBEDDING_MODEL', 'TaylorAI/bge-micro-v2'), device='cpu')" && \
153
+ 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'])"; \
154
+ python -c "import os; import tiktoken; tiktoken.get_encoding(os.environ['TIKTOKEN_ENCODING_NAME'])"; \
155
+ python -c "import nltk; nltk.download('punkt_tab')"; \
156
+ fi; \
157
+ fi; \
158
+ mkdir -p /app/backend/data && chown -R $UID:$GID /app/backend/data/ && \
159
+ rm -rf /var/lib/apt/lists/*;
160
+
161
+ # Install Ollama if requested
162
+ RUN if [ "$USE_OLLAMA" = "true" ]; then \
163
+ date +%s > /tmp/ollama_build_hash && \
164
+ echo "Cache broken at timestamp: `cat /tmp/ollama_build_hash`" && \
165
+ curl -fsSL https://ollama.com/install.sh | sh && \
166
+ rm -rf /var/lib/apt/lists/*; \
167
+ fi
168
+
169
+ # copy embedding weight from build
170
+ # RUN mkdir -p /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2
171
+ # COPY --from=build /app/onnx /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2/onnx
172
+
173
+ # copy built frontend files
174
+ COPY --chown=$UID:$GID --from=build /app/build /app/build
175
+ COPY --chown=$UID:$GID --from=build /app/CHANGELOG.md /app/CHANGELOG.md
176
+ COPY --chown=$UID:$GID --from=build /app/package.json /app/package.json
177
+
178
+ # copy backend files
179
+ COPY --chown=$UID:$GID ./backend .
180
+
181
+ EXPOSE 8080
182
+
183
+ HEALTHCHECK CMD curl --silent --fail http://localhost:${PORT:-8080}/health | jq -ne 'input.status == true' || exit 1
184
+
185
+ # Minimal, atomic permission hardening for OpenShift (arbitrary UID):
186
+ # - Group 0 owns /app and /root
187
+ # - Directories are group-writable and have SGID so new files inherit GID 0
188
+ RUN if [ "$USE_PERMISSION_HARDENING" = "true" ]; then \
189
+ set -eux; \
190
+ chgrp -R 0 /app /root || true; \
191
+ chmod -R g+rwX /app /root || true; \
192
+ find /app -type d -exec chmod g+s {} + || true; \
193
+ find /root -type d -exec chmod g+s {} + || true; \
194
+ fi
195
+
196
+ USER $UID:$GID
197
+
198
+ ARG BUILD_HASH
199
+ ENV WEBUI_BUILD_VERSION=${BUILD_HASH}
200
+ ENV DOCKER=true
201
+
202
+ CMD [ "bash", "start.sh"]
LICENSE ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Copyright (c) 2023- Open WebUI Inc. [Created by Timothy Jaeryang Baek]
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.
LICENSE_NOTICE ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Open WebUI Multi-License Notice
2
+
3
+ This repository contains code governed by multiple licenses based on the date and origin of contribution:
4
+
5
+ 1. All code committed prior to commit a76068d69cd59568b920dfab85dc573dbbb8f131 is licensed under the MIT License (see LICENSE_HISTORY).
6
+
7
+ 2. All code committed from commit a76068d69cd59568b920dfab85dc573dbbb8f131 up to and including commit 60d84a3aae9802339705826e9095e272e3c83623 is licensed under the BSD 3-Clause License (see LICENSE_HISTORY).
8
+
9
+ 3. All code contributed or modified after commit 60d84a3aae9802339705826e9095e272e3c83623 is licensed under the Open WebUI License (see LICENSE).
10
+
11
+ For details on which commits are covered by which license, refer to LICENSE_HISTORY.
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,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 Banner](./banner.png)
22
+
23
+ **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**.
24
+
25
+ Passionate about open-source AI? [Join our team →](https://careers.openwebui.com/)
26
+
27
+ ![Open WebUI Demo](./demo.png)
28
+
29
+ > [!TIP]
30
+ > **Looking for an [Enterprise Plan](https://docs.openwebui.com/enterprise)?** – **[Speak with Our Sales Team Today!](https://docs.openwebui.com/enterprise)**
31
+ >
32
+ > Get **enhanced capabilities**, including **custom theming and branding**, **Service Level Agreement (SLA) support**, **Long-Term Support (LTS) versions**, and **more!**
33
+
34
+ For more information, be sure to check out our [Open WebUI Documentation](https://docs.openwebui.com/).
35
+
36
+ ## Key Features of Open WebUI ⭐
37
+
38
+ - 🚀 **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.
39
+
40
+ - 🤝 **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**.
41
+
42
+ - 🛡️ **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.
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 using multiple Speech-to-Text providers (Local Whisper, OpenAI, Deepgram, Azure) and Text-to-Speech engines (Azure, ElevenLabs, OpenAI, Transformers, WebAPI), allowing for dynamic and interactive chat environments.
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
+ - 💾 **Persistent Artifact Storage**: Built-in key-value storage API for artifacts, enabling features like journals, trackers, leaderboards, and collaborative tools with both personal and shared data scopes across sessions.
57
+
58
+ - 📚 **Local RAG Integration**: Dive into the future of chat interactions with groundbreaking Retrieval Augmented Generation (RAG) support using your choice of 9 vector databases and multiple content extraction engines (Tika, Docling, Document Intelligence, Mistral OCR, External loaders). Load documents directly into chat or add files to your document library, effortlessly accessing them using the `#` command before a query.
59
+
60
+ - 🔍 **Web Search for RAG**: Perform web searches using 15+ providers including `SearXNG`, `Google PSE`, `Brave Search`, `Kagi`, `Mojeek`, `Tavily`, `Perplexity`, `serpstack`, `serper`, `Serply`, `DuckDuckGo`, `SearchApi`, `SerpApi`, `Bing`, `Jina`, `Exa`, `Sougou`, `Azure AI Search`, and `Ollama Cloud`, injecting results directly into your chat experience.
61
+
62
+ - 🌐 **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.
63
+
64
+ - 🎨 **Image Generation & Editing Integration**: Create and edit images using multiple engines including OpenAI's DALL-E, Gemini, ComfyUI (local), and AUTOMATIC1111 (local), with support for both generation and prompt-based editing workflows.
65
+
66
+ - ⚙️ **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.
67
+
68
+ - 🔐 **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.
69
+
70
+ - 🗄️ **Flexible Database & Storage Options**: Choose from SQLite (with optional encryption), PostgreSQL, or configure cloud storage backends (S3, Google Cloud Storage, Azure Blob Storage) for scalable deployments.
71
+
72
+ - 🔍 **Advanced Vector Database Support**: Select from 9 vector database options including ChromaDB, PGVector, Qdrant, Milvus, Elasticsearch, OpenSearch, Pinecone, S3Vector, and Oracle 23ai for optimal RAG performance.
73
+
74
+ - 🔐 **Enterprise Authentication**: Full support for LDAP/Active Directory integration, SCIM 2.0 automated provisioning, and SSO via trusted headers alongside OAuth providers. Enterprise-grade user and group provisioning through SCIM 2.0 protocol, enabling seamless integration with identity providers like Okta, Azure AD, and Google Workspace for automated user lifecycle management.
75
+
76
+ - ☁️ **Cloud-Native Integration**: Native support for Google Drive and OneDrive/SharePoint file picking, enabling seamless document import from enterprise cloud storage.
77
+
78
+ - 📊 **Production Observability**: Built-in OpenTelemetry support for traces, metrics, and logs, enabling comprehensive monitoring with your existing observability stack.
79
+
80
+ - ⚖️ **Horizontal Scalability**: Redis-backed session management and WebSocket support for multi-worker and multi-node deployments behind load balancers.
81
+
82
+ - 🌐🌍 **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!
83
+
84
+ - 🧩 **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.
85
+
86
+ - 🌟 **Continuous Updates**: We are committed to improving Open WebUI with regular updates, fixes, and new features.
87
+
88
+ Want to learn more about Open WebUI's features? Check out our [Open WebUI documentation](https://docs.openwebui.com/features) for a comprehensive overview!
89
+
90
+ ---
91
+
92
+ 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!
93
+
94
+ ## How to Install 🚀
95
+
96
+ ### Installation via Python pip 🐍
97
+
98
+ Open WebUI can be installed using pip, the Python package installer. Before proceeding, ensure you're using **Python 3.11** to avoid compatibility issues.
99
+
100
+ 1. **Install Open WebUI**:
101
+ Open your terminal and run the following command to install Open WebUI:
102
+
103
+ ```bash
104
+ pip install open-webui
105
+ ```
106
+
107
+ 2. **Running Open WebUI**:
108
+ After installation, you can start Open WebUI by executing:
109
+
110
+ ```bash
111
+ open-webui serve
112
+ ```
113
+
114
+ This will start the Open WebUI server, which you can access at [http://localhost:8080](http://localhost:8080)
115
+
116
+ ### Quick Start with Docker 🐳
117
+
118
+ > [!NOTE]
119
+ > 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.
120
+
121
+ > [!WARNING]
122
+ > 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.
123
+
124
+ > [!TIP]
125
+ > 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.
126
+
127
+ ### Installation with Default Configuration
128
+
129
+ - **If Ollama is on your computer**, use this command:
130
+
131
+ ```bash
132
+ 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
133
+ ```
134
+
135
+ - **If Ollama is on a Different Server**, use this command:
136
+
137
+ To connect to Ollama on another server, change the `OLLAMA_BASE_URL` to the server's URL:
138
+
139
+ ```bash
140
+ 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
141
+ ```
142
+
143
+ - **To run Open WebUI with Nvidia GPU support**, use this command:
144
+
145
+ ```bash
146
+ 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
147
+ ```
148
+
149
+ ### Installation for OpenAI API Usage Only
150
+
151
+ - **If you're only using OpenAI API**, use this command:
152
+
153
+ ```bash
154
+ 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
155
+ ```
156
+
157
+ ### Installing Open WebUI with Bundled Ollama Support
158
+
159
+ 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:
160
+
161
+ - **With GPU Support**:
162
+ Utilize GPU resources by running the following command:
163
+
164
+ ```bash
165
+ 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
166
+ ```
167
+
168
+ - **For CPU Only**:
169
+ If you're not using a GPU, use this command instead:
170
+
171
+ ```bash
172
+ 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
173
+ ```
174
+
175
+ 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.
176
+
177
+ After installation, you can access Open WebUI at [http://localhost:3000](http://localhost:3000). Enjoy! 😄
178
+
179
+ ### Other Installation Methods
180
+
181
+ 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.
182
+
183
+ Look at the [Local Development Guide](https://docs.openwebui.com/getting-started/advanced-topics/development) for instructions on setting up a local development environment.
184
+
185
+ ### Troubleshooting
186
+
187
+ 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).
188
+
189
+ #### Open WebUI: Server Connection Error
190
+
191
+ 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`.
192
+
193
+ **Example Docker Command**:
194
+
195
+ ```bash
196
+ 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
197
+ ```
198
+
199
+ ### Keeping Your Docker Installation Up-to-Date
200
+
201
+ Check our Updating Guide available in our [Open WebUI Documentation](https://docs.openwebui.com/getting-started/updating).
202
+
203
+ ### Using the Dev Branch 🌙
204
+
205
+ > [!WARNING]
206
+ > The `:dev` branch contains the latest unstable features and changes. Use it at your own risk as it may have bugs or incomplete features.
207
+
208
+ 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:
209
+
210
+ ```bash
211
+ 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
212
+ ```
213
+
214
+ ### Offline Mode
215
+
216
+ 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.
217
+
218
+ ```bash
219
+ export HF_HUB_OFFLINE=1
220
+ ```
221
+
222
+ ## What's Next? 🌟
223
+
224
+ Discover upcoming features on our roadmap in the [Open WebUI Documentation](https://docs.openwebui.com/roadmap/).
225
+
226
+ ## License 📜
227
+
228
+ This project contains code under multiple licenses. The current codebase includes components licensed under the Open WebUI License with an additional requirement to preserve the "Open WebUI" branding, as well as prior contributions under their respective original licenses. For a detailed record of license changes and the applicable terms for each section of the code, please refer to [LICENSE_HISTORY](./LICENSE_HISTORY). For complete and updated licensing details, please see the [LICENSE](./LICENSE) and [LICENSE_HISTORY](./LICENSE_HISTORY) files.
229
+
230
+ ## Support 💬
231
+
232
+ If you have any questions, suggestions, or need assistance, please open an issue or join our
233
+ [Open WebUI Discord community](https://discord.gg/5rJgQTnV4s) to connect with us! 🤝
234
+
235
+ ## Star History
236
+
237
+ <a href="https://star-history.com/#open-webui/open-webui&Date">
238
+ <picture>
239
+ <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=open-webui/open-webui&type=Date&theme=dark" />
240
+ <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=open-webui/open-webui&type=Date" />
241
+ <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=open-webui/open-webui&type=Date" />
242
+ </picture>
243
+ </a>
244
+
245
+ ---
246
+
247
+ 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;http://localhost:8080"
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,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ MODEL_ID_TOO_LONG = "The model id is too long. Please make sure your model id is less than 256 characters long."
42
+
43
+ INVALID_TOKEN = (
44
+ "Your session has expired or the token is invalid. Please sign in again."
45
+ )
46
+ INVALID_CRED = "The email or password provided is incorrect. Please check for typos and try logging in again."
47
+ INVALID_EMAIL_FORMAT = "The email format you entered is invalid. Please double-check and make sure you're using a valid email address (e.g., yourname@example.com)."
48
+ INCORRECT_PASSWORD = (
49
+ "The password provided is incorrect. Please check for typos and try again."
50
+ )
51
+ INVALID_TRUSTED_HEADER = "Your provider has not provided a trusted header. Please contact your administrator for assistance."
52
+
53
+ 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."
54
+
55
+ UNAUTHORIZED = "401 Unauthorized"
56
+ ACCESS_PROHIBITED = "You do not have permission to access this resource. Please contact your administrator for assistance."
57
+ ACTION_PROHIBITED = (
58
+ "The requested action has been restricted as a security measure."
59
+ )
60
+
61
+ FILE_NOT_SENT = "FILE_NOT_SENT"
62
+ 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."
63
+
64
+ NOT_FOUND = "We could not find what you're looking for :/"
65
+ USER_NOT_FOUND = "We could not find what you're looking for :/"
66
+ 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."
67
+ API_KEY_NOT_ALLOWED = "Use of API key is not enabled in the environment."
68
+
69
+ MALICIOUS = "Unusual activities detected, please try again in a few minutes."
70
+
71
+ PANDOC_NOT_INSTALLED = "Pandoc is not installed on the server. Please contact your administrator for assistance."
72
+ INCORRECT_FORMAT = (
73
+ lambda err="": f"Invalid format. Please use the correct format{err}"
74
+ )
75
+ RATE_LIMIT_EXCEEDED = "API rate limit exceeded"
76
+
77
+ MODEL_NOT_FOUND = lambda name="": f"Model '{name}' was not found"
78
+ OPENAI_NOT_FOUND = lambda name="": "OpenAI API was not found"
79
+ OLLAMA_NOT_FOUND = "WebUI could not connect to Ollama"
80
+ 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."
81
+ API_KEY_CREATION_NOT_ALLOWED = "API key creation is not allowed in the environment."
82
+
83
+ EMPTY_CONTENT = "The content provided is empty. Please ensure that there is text or data present before proceeding."
84
+
85
+ DB_NOT_SQLITE = "This feature is only available when running with SQLite databases."
86
+
87
+ INVALID_URL = (
88
+ "Oops! The URL you provided is invalid. Please double-check and try again."
89
+ )
90
+
91
+ WEB_SEARCH_ERROR = (
92
+ lambda err="": f"{err if err else 'Oops! Something went wrong while searching the web.'}"
93
+ )
94
+
95
+ OLLAMA_API_DISABLED = (
96
+ "The Ollama API is disabled. Please enable it to use this feature."
97
+ )
98
+
99
+ FILE_TOO_LARGE = (
100
+ lambda size="": f"Oops! The file you're trying to upload is too large. Please upload a file that is less than {size}."
101
+ )
102
+
103
+ DUPLICATE_CONTENT = (
104
+ "Duplicate content detected. Please provide unique content to proceed."
105
+ )
106
+ FILE_NOT_PROCESSED = "Extracted content is not available for this file. Please ensure that the file is processed before proceeding."
107
+
108
+ INVALID_PASSWORD = lambda err="": (
109
+ err if err else "The password does not meet the required validation criteria."
110
+ )
111
+
112
+
113
+ class TASKS(str, Enum):
114
+ def __str__(self) -> str:
115
+ return super().__str__()
116
+
117
+ DEFAULT = lambda task="": f"{task if task else 'generation'}"
118
+ TITLE_GENERATION = "title_generation"
119
+ FOLLOW_UP_GENERATION = "follow_up_generation"
120
+ TAGS_GENERATION = "tags_generation"
121
+ EMOJI_GENERATION = "emoji_generation"
122
+ QUERY_GENERATION = "query_generation"
123
+ IMAGE_PROMPT_GENERATION = "image_prompt_generation"
124
+ AUTOCOMPLETE_GENERATION = "autocomplete_generation"
125
+ FUNCTION_CALLING = "function_calling"
126
+ MOA_RESPONSE_GENERATION = "moa_response_generation"
backend/open_webui/env.py ADDED
@@ -0,0 +1,967 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ import re
12
+
13
+
14
+ import markdown
15
+ from bs4 import BeautifulSoup
16
+ from open_webui.constants import ERROR_MESSAGES
17
+
18
+ ####################################
19
+ # Load .env file
20
+ ####################################
21
+
22
+ # Use .resolve() to get the canonical path, removing any '..' or '.' components
23
+ ENV_FILE_PATH = Path(__file__).resolve()
24
+
25
+ # OPEN_WEBUI_DIR should be the directory where env.py resides (open_webui/)
26
+ OPEN_WEBUI_DIR = ENV_FILE_PATH.parent
27
+
28
+ # BACKEND_DIR is the parent of OPEN_WEBUI_DIR (backend/)
29
+ BACKEND_DIR = OPEN_WEBUI_DIR.parent
30
+
31
+ # BASE_DIR is the parent of BACKEND_DIR (open-webui-dev/)
32
+ BASE_DIR = BACKEND_DIR.parent
33
+
34
+ try:
35
+ from dotenv import find_dotenv, load_dotenv
36
+
37
+ load_dotenv(find_dotenv(str(BASE_DIR / ".env")))
38
+ except ImportError:
39
+ print("dotenv not installed, skipping...")
40
+
41
+ DOCKER = os.environ.get("DOCKER", "False").lower() == "true"
42
+
43
+ # device type embedding models - "cpu" (default), "cuda" (nvidia gpu required) or "mps" (apple silicon) - choosing this right can lead to better performance
44
+ USE_CUDA = os.environ.get("USE_CUDA_DOCKER", "false")
45
+
46
+ if USE_CUDA.lower() == "true":
47
+ try:
48
+ import torch
49
+
50
+ assert torch.cuda.is_available(), "CUDA not available"
51
+ DEVICE_TYPE = "cuda"
52
+ except Exception as e:
53
+ cuda_error = (
54
+ "Error when testing CUDA but USE_CUDA_DOCKER is true. "
55
+ f"Resetting USE_CUDA_DOCKER to false: {e}"
56
+ )
57
+ os.environ["USE_CUDA_DOCKER"] = "false"
58
+ USE_CUDA = "false"
59
+ DEVICE_TYPE = "cpu"
60
+ else:
61
+ DEVICE_TYPE = "cpu"
62
+
63
+ try:
64
+ import torch
65
+
66
+ if torch.backends.mps.is_available() and torch.backends.mps.is_built():
67
+ DEVICE_TYPE = "mps"
68
+ except Exception:
69
+ pass
70
+
71
+ ####################################
72
+ # LOGGING
73
+ ####################################
74
+
75
+ GLOBAL_LOG_LEVEL = os.environ.get("GLOBAL_LOG_LEVEL", "").upper()
76
+ if GLOBAL_LOG_LEVEL in logging.getLevelNamesMapping():
77
+ logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL, force=True)
78
+ else:
79
+ GLOBAL_LOG_LEVEL = "INFO"
80
+
81
+ log = logging.getLogger(__name__)
82
+ log.info(f"GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}")
83
+
84
+ if "cuda_error" in locals():
85
+ log.exception(cuda_error)
86
+ del cuda_error
87
+
88
+ SRC_LOG_LEVELS = {} # Legacy variable, do not remove
89
+
90
+ WEBUI_NAME = os.environ.get("WEBUI_NAME", "Open WebUI")
91
+ if WEBUI_NAME != "Open WebUI":
92
+ WEBUI_NAME += " (Open WebUI)"
93
+
94
+ WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png"
95
+
96
+ TRUSTED_SIGNATURE_KEY = os.environ.get("TRUSTED_SIGNATURE_KEY", "")
97
+
98
+ ####################################
99
+ # ENV (dev,test,prod)
100
+ ####################################
101
+
102
+ ENV = os.environ.get("ENV", "dev")
103
+
104
+ FROM_INIT_PY = os.environ.get("FROM_INIT_PY", "False").lower() == "true"
105
+
106
+ if FROM_INIT_PY:
107
+ PACKAGE_DATA = {"version": importlib.metadata.version("open-webui")}
108
+ else:
109
+ try:
110
+ PACKAGE_DATA = json.loads((BASE_DIR / "package.json").read_text())
111
+ except Exception:
112
+ PACKAGE_DATA = {"version": "0.0.0"}
113
+
114
+ VERSION = PACKAGE_DATA["version"]
115
+
116
+
117
+ DEPLOYMENT_ID = os.environ.get("DEPLOYMENT_ID", "")
118
+ INSTANCE_ID = os.environ.get("INSTANCE_ID", str(uuid4()))
119
+
120
+ ENABLE_DB_MIGRATIONS = os.environ.get("ENABLE_DB_MIGRATIONS", "True").lower() == "true"
121
+
122
+
123
+ # Function to parse each section
124
+ def parse_section(section):
125
+ items = []
126
+ for li in section.find_all("li"):
127
+ # Extract raw HTML string
128
+ raw_html = str(li)
129
+
130
+ # Extract text without HTML tags
131
+ text = li.get_text(separator=" ", strip=True)
132
+
133
+ # Split into title and content
134
+ parts = text.split(": ", 1)
135
+ title = parts[0].strip() if len(parts) > 1 else ""
136
+ content = parts[1].strip() if len(parts) > 1 else text
137
+
138
+ items.append({"title": title, "content": content, "raw": raw_html})
139
+ return items
140
+
141
+
142
+ try:
143
+ changelog_path = BASE_DIR / "CHANGELOG.md"
144
+ with open(str(changelog_path.absolute()), "r", encoding="utf8") as file:
145
+ changelog_content = file.read()
146
+
147
+ except Exception:
148
+ changelog_content = (pkgutil.get_data("open_webui", "CHANGELOG.md") or b"").decode()
149
+
150
+ # Convert markdown content to HTML
151
+ html_content = markdown.markdown(changelog_content)
152
+
153
+ # Parse the HTML content
154
+ soup = BeautifulSoup(html_content, "html.parser")
155
+
156
+ # Initialize JSON structure
157
+ changelog_json = {}
158
+
159
+ # Iterate over each version
160
+ for version in soup.find_all("h2"):
161
+ version_number = version.get_text().strip().split(" - ")[0][1:-1] # Remove brackets
162
+ date = version.get_text().strip().split(" - ")[1]
163
+
164
+ version_data = {"date": date}
165
+
166
+ # Find the next sibling that is a h3 tag (section title)
167
+ current = version.find_next_sibling()
168
+
169
+ while current and current.name != "h2":
170
+ if current.name == "h3":
171
+ section_title = current.get_text().lower() # e.g., "added", "fixed"
172
+ section_items = parse_section(current.find_next_sibling("ul"))
173
+ version_data[section_title] = section_items
174
+
175
+ # Move to the next element
176
+ current = current.find_next_sibling()
177
+
178
+ changelog_json[version_number] = version_data
179
+
180
+ CHANGELOG = changelog_json
181
+
182
+ ####################################
183
+ # SAFE_MODE
184
+ ####################################
185
+
186
+ SAFE_MODE = os.environ.get("SAFE_MODE", "false").lower() == "true"
187
+
188
+
189
+ ####################################
190
+ # ENABLE_FORWARD_USER_INFO_HEADERS
191
+ ####################################
192
+
193
+ ENABLE_FORWARD_USER_INFO_HEADERS = (
194
+ os.environ.get("ENABLE_FORWARD_USER_INFO_HEADERS", "False").lower() == "true"
195
+ )
196
+
197
+ # Header names for user info forwarding (customizable via environment variables)
198
+ FORWARD_USER_INFO_HEADER_USER_NAME = os.environ.get(
199
+ "FORWARD_USER_INFO_HEADER_USER_NAME", "X-OpenWebUI-User-Name"
200
+ )
201
+ FORWARD_USER_INFO_HEADER_USER_ID = os.environ.get(
202
+ "FORWARD_USER_INFO_HEADER_USER_ID", "X-OpenWebUI-User-Id"
203
+ )
204
+ FORWARD_USER_INFO_HEADER_USER_EMAIL = os.environ.get(
205
+ "FORWARD_USER_INFO_HEADER_USER_EMAIL", "X-OpenWebUI-User-Email"
206
+ )
207
+ FORWARD_USER_INFO_HEADER_USER_ROLE = os.environ.get(
208
+ "FORWARD_USER_INFO_HEADER_USER_ROLE", "X-OpenWebUI-User-Role"
209
+ )
210
+
211
+ # Header name for chat ID forwarding (customizable via environment variable)
212
+ FORWARD_SESSION_INFO_HEADER_CHAT_ID = os.environ.get(
213
+ "FORWARD_SESSION_INFO_HEADER_CHAT_ID", "X-OpenWebUI-Chat-Id"
214
+ )
215
+
216
+ # Experimental feature, may be removed in future
217
+ ENABLE_STAR_SESSIONS_MIDDLEWARE = (
218
+ os.environ.get("ENABLE_STAR_SESSIONS_MIDDLEWARE", "False").lower() == "true"
219
+ )
220
+
221
+ ENABLE_EASTER_EGGS = os.environ.get("ENABLE_EASTER_EGGS", "True").lower() == "true"
222
+
223
+ ####################################
224
+ # WEBUI_BUILD_HASH
225
+ ####################################
226
+
227
+ WEBUI_BUILD_HASH = os.environ.get("WEBUI_BUILD_HASH", "dev-build")
228
+
229
+ ####################################
230
+ # DATA/FRONTEND BUILD DIR
231
+ ####################################
232
+
233
+ DATA_DIR = Path(os.getenv("DATA_DIR", BACKEND_DIR / "data")).resolve()
234
+
235
+ if FROM_INIT_PY:
236
+ NEW_DATA_DIR = Path(os.getenv("DATA_DIR", OPEN_WEBUI_DIR / "data")).resolve()
237
+ NEW_DATA_DIR.mkdir(parents=True, exist_ok=True)
238
+
239
+ # Check if the data directory exists in the package directory
240
+ if DATA_DIR.exists() and DATA_DIR != NEW_DATA_DIR:
241
+ log.info(f"Moving {DATA_DIR} to {NEW_DATA_DIR}")
242
+ for item in DATA_DIR.iterdir():
243
+ dest = NEW_DATA_DIR / item.name
244
+ if item.is_dir():
245
+ shutil.copytree(item, dest, dirs_exist_ok=True)
246
+ else:
247
+ shutil.copy2(item, dest)
248
+
249
+ # Zip the data directory
250
+ shutil.make_archive(DATA_DIR.parent / "open_webui_data", "zip", DATA_DIR)
251
+
252
+ # Remove the old data directory
253
+ shutil.rmtree(DATA_DIR)
254
+
255
+ DATA_DIR = Path(os.getenv("DATA_DIR", OPEN_WEBUI_DIR / "data"))
256
+
257
+ STATIC_DIR = Path(os.getenv("STATIC_DIR", OPEN_WEBUI_DIR / "static"))
258
+
259
+ FONTS_DIR = Path(os.getenv("FONTS_DIR", OPEN_WEBUI_DIR / "static" / "fonts"))
260
+
261
+ FRONTEND_BUILD_DIR = Path(os.getenv("FRONTEND_BUILD_DIR", BASE_DIR / "build")).resolve()
262
+
263
+ if FROM_INIT_PY:
264
+ FRONTEND_BUILD_DIR = Path(
265
+ os.getenv("FRONTEND_BUILD_DIR", OPEN_WEBUI_DIR / "frontend")
266
+ ).resolve()
267
+
268
+ ####################################
269
+ # Database
270
+ ####################################
271
+
272
+ # Check if the file exists
273
+ if os.path.exists(f"{DATA_DIR}/ollama.db"):
274
+ # Rename the file
275
+ os.rename(f"{DATA_DIR}/ollama.db", f"{DATA_DIR}/webui.db")
276
+ log.info("Database migrated from Ollama-WebUI successfully.")
277
+ else:
278
+ pass
279
+
280
+ DATABASE_URL = os.environ.get("DATABASE_URL", f"sqlite:///{DATA_DIR}/webui.db")
281
+
282
+ DATABASE_TYPE = os.environ.get("DATABASE_TYPE")
283
+ DATABASE_USER = os.environ.get("DATABASE_USER")
284
+ DATABASE_PASSWORD = os.environ.get("DATABASE_PASSWORD")
285
+
286
+ DATABASE_CRED = ""
287
+ if DATABASE_USER:
288
+ DATABASE_CRED += f"{DATABASE_USER}"
289
+ if DATABASE_PASSWORD:
290
+ DATABASE_CRED += f":{DATABASE_PASSWORD}"
291
+
292
+ DB_VARS = {
293
+ "db_type": DATABASE_TYPE,
294
+ "db_cred": DATABASE_CRED,
295
+ "db_host": os.environ.get("DATABASE_HOST"),
296
+ "db_port": os.environ.get("DATABASE_PORT"),
297
+ "db_name": os.environ.get("DATABASE_NAME"),
298
+ }
299
+
300
+ if all(DB_VARS.values()):
301
+ DATABASE_URL = f"{DB_VARS['db_type']}://{DB_VARS['db_cred']}@{DB_VARS['db_host']}:{DB_VARS['db_port']}/{DB_VARS['db_name']}"
302
+ elif DATABASE_TYPE == "sqlite+sqlcipher" and not os.environ.get("DATABASE_URL"):
303
+ # Handle SQLCipher with local file when DATABASE_URL wasn't explicitly set
304
+ DATABASE_URL = f"sqlite+sqlcipher:///{DATA_DIR}/webui.db"
305
+
306
+ # Replace the postgres:// with postgresql://
307
+ if "postgres://" in DATABASE_URL:
308
+ DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql://")
309
+
310
+ DATABASE_SCHEMA = os.environ.get("DATABASE_SCHEMA", None)
311
+
312
+ DATABASE_POOL_SIZE = os.environ.get("DATABASE_POOL_SIZE", None)
313
+
314
+ if DATABASE_POOL_SIZE != None:
315
+ try:
316
+ DATABASE_POOL_SIZE = int(DATABASE_POOL_SIZE)
317
+ except Exception:
318
+ DATABASE_POOL_SIZE = None
319
+
320
+ DATABASE_POOL_MAX_OVERFLOW = os.environ.get("DATABASE_POOL_MAX_OVERFLOW", 0)
321
+
322
+ if DATABASE_POOL_MAX_OVERFLOW == "":
323
+ DATABASE_POOL_MAX_OVERFLOW = 0
324
+ else:
325
+ try:
326
+ DATABASE_POOL_MAX_OVERFLOW = int(DATABASE_POOL_MAX_OVERFLOW)
327
+ except Exception:
328
+ DATABASE_POOL_MAX_OVERFLOW = 0
329
+
330
+ DATABASE_POOL_TIMEOUT = os.environ.get("DATABASE_POOL_TIMEOUT", 30)
331
+
332
+ if DATABASE_POOL_TIMEOUT == "":
333
+ DATABASE_POOL_TIMEOUT = 30
334
+ else:
335
+ try:
336
+ DATABASE_POOL_TIMEOUT = int(DATABASE_POOL_TIMEOUT)
337
+ except Exception:
338
+ DATABASE_POOL_TIMEOUT = 30
339
+
340
+ DATABASE_POOL_RECYCLE = os.environ.get("DATABASE_POOL_RECYCLE", 3600)
341
+
342
+ if DATABASE_POOL_RECYCLE == "":
343
+ DATABASE_POOL_RECYCLE = 3600
344
+ else:
345
+ try:
346
+ DATABASE_POOL_RECYCLE = int(DATABASE_POOL_RECYCLE)
347
+ except Exception:
348
+ DATABASE_POOL_RECYCLE = 3600
349
+
350
+ DATABASE_ENABLE_SQLITE_WAL = (
351
+ os.environ.get("DATABASE_ENABLE_SQLITE_WAL", "False").lower() == "true"
352
+ )
353
+
354
+ DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL = os.environ.get(
355
+ "DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL", None
356
+ )
357
+ if DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL is not None:
358
+ try:
359
+ DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL = float(
360
+ DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL
361
+ )
362
+ except Exception:
363
+ DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL = 0.0
364
+
365
+ # When enabled, get_db_context reuses existing sessions; set to False to always create new sessions
366
+ DATABASE_ENABLE_SESSION_SHARING = (
367
+ os.environ.get("DATABASE_ENABLE_SESSION_SHARING", "False").lower() == "true"
368
+ )
369
+
370
+ # Enable public visibility of active user count (when disabled, only admins can see it)
371
+ ENABLE_PUBLIC_ACTIVE_USERS_COUNT = (
372
+ os.environ.get("ENABLE_PUBLIC_ACTIVE_USERS_COUNT", "True").lower() == "true"
373
+ )
374
+
375
+ RESET_CONFIG_ON_START = (
376
+ os.environ.get("RESET_CONFIG_ON_START", "False").lower() == "true"
377
+ )
378
+
379
+ ENABLE_REALTIME_CHAT_SAVE = (
380
+ os.environ.get("ENABLE_REALTIME_CHAT_SAVE", "False").lower() == "true"
381
+ )
382
+
383
+ ENABLE_QUERIES_CACHE = os.environ.get("ENABLE_QUERIES_CACHE", "False").lower() == "true"
384
+
385
+ RAG_SYSTEM_CONTEXT = os.environ.get("RAG_SYSTEM_CONTEXT", "False").lower() == "true"
386
+
387
+ ####################################
388
+ # REDIS
389
+ ####################################
390
+
391
+ REDIS_URL = os.environ.get("REDIS_URL", "")
392
+ REDIS_CLUSTER = os.environ.get("REDIS_CLUSTER", "False").lower() == "true"
393
+
394
+ REDIS_KEY_PREFIX = os.environ.get("REDIS_KEY_PREFIX", "open-webui")
395
+
396
+ REDIS_SENTINEL_HOSTS = os.environ.get("REDIS_SENTINEL_HOSTS", "")
397
+ REDIS_SENTINEL_PORT = os.environ.get("REDIS_SENTINEL_PORT", "26379")
398
+
399
+ # Maximum number of retries for Redis operations when using Sentinel fail-over
400
+ REDIS_SENTINEL_MAX_RETRY_COUNT = os.environ.get("REDIS_SENTINEL_MAX_RETRY_COUNT", "2")
401
+ try:
402
+ REDIS_SENTINEL_MAX_RETRY_COUNT = int(REDIS_SENTINEL_MAX_RETRY_COUNT)
403
+ if REDIS_SENTINEL_MAX_RETRY_COUNT < 1:
404
+ REDIS_SENTINEL_MAX_RETRY_COUNT = 2
405
+ except ValueError:
406
+ REDIS_SENTINEL_MAX_RETRY_COUNT = 2
407
+
408
+
409
+ REDIS_SOCKET_CONNECT_TIMEOUT = os.environ.get("REDIS_SOCKET_CONNECT_TIMEOUT", "")
410
+ try:
411
+ REDIS_SOCKET_CONNECT_TIMEOUT = float(REDIS_SOCKET_CONNECT_TIMEOUT)
412
+ except ValueError:
413
+ REDIS_SOCKET_CONNECT_TIMEOUT = None
414
+
415
+ REDIS_RECONNECT_DELAY = os.environ.get("REDIS_RECONNECT_DELAY", "")
416
+
417
+ if REDIS_RECONNECT_DELAY == "":
418
+ REDIS_RECONNECT_DELAY = None
419
+ else:
420
+ try:
421
+ REDIS_RECONNECT_DELAY = float(REDIS_RECONNECT_DELAY)
422
+ if REDIS_RECONNECT_DELAY < 0:
423
+ REDIS_RECONNECT_DELAY = None
424
+ except Exception:
425
+ REDIS_RECONNECT_DELAY = None
426
+
427
+ ####################################
428
+ # UVICORN WORKERS
429
+ ####################################
430
+
431
+ # Number of uvicorn worker processes for handling requests
432
+ UVICORN_WORKERS = os.environ.get("UVICORN_WORKERS", "1")
433
+ try:
434
+ UVICORN_WORKERS = int(UVICORN_WORKERS)
435
+ if UVICORN_WORKERS < 1:
436
+ UVICORN_WORKERS = 1
437
+ except ValueError:
438
+ UVICORN_WORKERS = 1
439
+ log.info(f"Invalid UVICORN_WORKERS value, defaulting to {UVICORN_WORKERS}")
440
+
441
+ ####################################
442
+ # WEBUI_AUTH (Required for security)
443
+ ####################################
444
+
445
+ WEBUI_AUTH = os.environ.get("WEBUI_AUTH", "True").lower() == "true"
446
+
447
+ ENABLE_INITIAL_ADMIN_SIGNUP = (
448
+ os.environ.get("ENABLE_INITIAL_ADMIN_SIGNUP", "False").lower() == "true"
449
+ )
450
+ ENABLE_SIGNUP_PASSWORD_CONFIRMATION = (
451
+ os.environ.get("ENABLE_SIGNUP_PASSWORD_CONFIRMATION", "False").lower() == "true"
452
+ )
453
+
454
+ ####################################
455
+ # Admin Account Runtime Creation
456
+ ####################################
457
+
458
+ # Optional env vars for creating an admin account on startup
459
+ # Useful for headless/automated deployments
460
+ WEBUI_ADMIN_EMAIL = os.environ.get("WEBUI_ADMIN_EMAIL", "")
461
+ WEBUI_ADMIN_PASSWORD = os.environ.get("WEBUI_ADMIN_PASSWORD", "")
462
+ WEBUI_ADMIN_NAME = os.environ.get("WEBUI_ADMIN_NAME", "Admin")
463
+
464
+ WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get(
465
+ "WEBUI_AUTH_TRUSTED_EMAIL_HEADER", None
466
+ )
467
+ WEBUI_AUTH_TRUSTED_NAME_HEADER = os.environ.get("WEBUI_AUTH_TRUSTED_NAME_HEADER", None)
468
+ WEBUI_AUTH_TRUSTED_GROUPS_HEADER = os.environ.get(
469
+ "WEBUI_AUTH_TRUSTED_GROUPS_HEADER", None
470
+ )
471
+
472
+
473
+ ENABLE_PASSWORD_VALIDATION = (
474
+ os.environ.get("ENABLE_PASSWORD_VALIDATION", "False").lower() == "true"
475
+ )
476
+ PASSWORD_VALIDATION_REGEX_PATTERN = os.environ.get(
477
+ "PASSWORD_VALIDATION_REGEX_PATTERN",
478
+ "^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$",
479
+ )
480
+
481
+
482
+ try:
483
+ PASSWORD_VALIDATION_REGEX_PATTERN = rf"{PASSWORD_VALIDATION_REGEX_PATTERN}"
484
+ PASSWORD_VALIDATION_REGEX_PATTERN = re.compile(PASSWORD_VALIDATION_REGEX_PATTERN)
485
+ except Exception as e:
486
+ log.error(f"Invalid PASSWORD_VALIDATION_REGEX_PATTERN: {e}")
487
+ PASSWORD_VALIDATION_REGEX_PATTERN = re.compile(
488
+ r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$"
489
+ )
490
+
491
+ PASSWORD_VALIDATION_HINT = os.environ.get("PASSWORD_VALIDATION_HINT", "")
492
+
493
+
494
+ BYPASS_MODEL_ACCESS_CONTROL = (
495
+ os.environ.get("BYPASS_MODEL_ACCESS_CONTROL", "False").lower() == "true"
496
+ )
497
+
498
+ WEBUI_AUTH_SIGNOUT_REDIRECT_URL = os.environ.get(
499
+ "WEBUI_AUTH_SIGNOUT_REDIRECT_URL", None
500
+ )
501
+
502
+ ####################################
503
+ # WEBUI_SECRET_KEY
504
+ ####################################
505
+
506
+ WEBUI_SECRET_KEY = os.environ.get(
507
+ "WEBUI_SECRET_KEY",
508
+ os.environ.get(
509
+ "WEBUI_JWT_SECRET_KEY", "t0p-s3cr3t"
510
+ ), # DEPRECATED: remove at next major version
511
+ )
512
+
513
+ WEBUI_SESSION_COOKIE_SAME_SITE = os.environ.get("WEBUI_SESSION_COOKIE_SAME_SITE", "lax")
514
+
515
+ WEBUI_SESSION_COOKIE_SECURE = (
516
+ os.environ.get("WEBUI_SESSION_COOKIE_SECURE", "false").lower() == "true"
517
+ )
518
+
519
+ WEBUI_AUTH_COOKIE_SAME_SITE = os.environ.get(
520
+ "WEBUI_AUTH_COOKIE_SAME_SITE", WEBUI_SESSION_COOKIE_SAME_SITE
521
+ )
522
+
523
+ WEBUI_AUTH_COOKIE_SECURE = (
524
+ os.environ.get(
525
+ "WEBUI_AUTH_COOKIE_SECURE",
526
+ os.environ.get("WEBUI_SESSION_COOKIE_SECURE", "false"),
527
+ ).lower()
528
+ == "true"
529
+ )
530
+
531
+ if WEBUI_AUTH and WEBUI_SECRET_KEY == "":
532
+ raise ValueError(ERROR_MESSAGES.ENV_VAR_NOT_FOUND)
533
+
534
+ ENABLE_COMPRESSION_MIDDLEWARE = (
535
+ os.environ.get("ENABLE_COMPRESSION_MIDDLEWARE", "True").lower() == "true"
536
+ )
537
+
538
+ ####################################
539
+ # OAUTH Configuration
540
+ ####################################
541
+ ENABLE_OAUTH_EMAIL_FALLBACK = (
542
+ os.environ.get("ENABLE_OAUTH_EMAIL_FALLBACK", "False").lower() == "true"
543
+ )
544
+
545
+ ENABLE_OAUTH_ID_TOKEN_COOKIE = (
546
+ os.environ.get("ENABLE_OAUTH_ID_TOKEN_COOKIE", "True").lower() == "true"
547
+ )
548
+
549
+ OAUTH_CLIENT_INFO_ENCRYPTION_KEY = os.environ.get(
550
+ "OAUTH_CLIENT_INFO_ENCRYPTION_KEY", WEBUI_SECRET_KEY
551
+ )
552
+
553
+ OAUTH_SESSION_TOKEN_ENCRYPTION_KEY = os.environ.get(
554
+ "OAUTH_SESSION_TOKEN_ENCRYPTION_KEY", WEBUI_SECRET_KEY
555
+ )
556
+
557
+ # Token Exchange Configuration
558
+ # Allows external apps to exchange OAuth tokens for OpenWebUI tokens
559
+ ENABLE_OAUTH_TOKEN_EXCHANGE = (
560
+ os.environ.get("ENABLE_OAUTH_TOKEN_EXCHANGE", "False").lower() == "true"
561
+ )
562
+
563
+ ####################################
564
+ # SCIM Configuration
565
+ ####################################
566
+
567
+ ENABLE_SCIM = (
568
+ os.environ.get("ENABLE_SCIM", os.environ.get("SCIM_ENABLED", "False")).lower()
569
+ == "true"
570
+ )
571
+ SCIM_TOKEN = os.environ.get("SCIM_TOKEN", "")
572
+
573
+ ####################################
574
+ # LICENSE_KEY
575
+ ####################################
576
+
577
+ LICENSE_KEY = os.environ.get("LICENSE_KEY", "")
578
+
579
+ LICENSE_BLOB = None
580
+ LICENSE_BLOB_PATH = os.environ.get("LICENSE_BLOB_PATH", DATA_DIR / "l.data")
581
+ if LICENSE_BLOB_PATH and os.path.exists(LICENSE_BLOB_PATH):
582
+ with open(LICENSE_BLOB_PATH, "rb") as f:
583
+ LICENSE_BLOB = f.read()
584
+
585
+ LICENSE_PUBLIC_KEY = os.environ.get("LICENSE_PUBLIC_KEY", "")
586
+
587
+ pk = None
588
+ if LICENSE_PUBLIC_KEY:
589
+ pk = serialization.load_pem_public_key(f"""
590
+ -----BEGIN PUBLIC KEY-----
591
+ {LICENSE_PUBLIC_KEY}
592
+ -----END PUBLIC KEY-----
593
+ """.encode("utf-8"))
594
+
595
+
596
+ ####################################
597
+ # MODELS
598
+ ####################################
599
+
600
+ ENABLE_CUSTOM_MODEL_FALLBACK = (
601
+ os.environ.get("ENABLE_CUSTOM_MODEL_FALLBACK", "False").lower() == "true"
602
+ )
603
+
604
+ MODELS_CACHE_TTL = os.environ.get("MODELS_CACHE_TTL", "1")
605
+ if MODELS_CACHE_TTL == "":
606
+ MODELS_CACHE_TTL = None
607
+ else:
608
+ try:
609
+ MODELS_CACHE_TTL = int(MODELS_CACHE_TTL)
610
+ except Exception:
611
+ MODELS_CACHE_TTL = 1
612
+
613
+
614
+ ####################################
615
+ # CHAT
616
+ ####################################
617
+
618
+ ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION = (
619
+ os.environ.get("ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION", "False").lower()
620
+ == "true"
621
+ )
622
+
623
+ CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = os.environ.get(
624
+ "CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE", "1"
625
+ )
626
+
627
+ if CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE == "":
628
+ CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = 1
629
+ else:
630
+ try:
631
+ CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = int(
632
+ CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE
633
+ )
634
+ except Exception:
635
+ CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = 1
636
+
637
+
638
+ CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES = os.environ.get(
639
+ "CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES", "30"
640
+ )
641
+
642
+ if CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES == "":
643
+ CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES = 30
644
+ else:
645
+ try:
646
+ CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES = int(CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES)
647
+ except Exception:
648
+ CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES = 30
649
+
650
+
651
+ CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE = os.environ.get(
652
+ "CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE", ""
653
+ )
654
+
655
+ if CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE == "":
656
+ CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE = None
657
+ else:
658
+ try:
659
+ CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE = int(
660
+ CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE
661
+ )
662
+ except Exception:
663
+ CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE = None
664
+
665
+
666
+ ####################################
667
+ # WEBSOCKET SUPPORT
668
+ ####################################
669
+
670
+ ENABLE_WEBSOCKET_SUPPORT = (
671
+ os.environ.get("ENABLE_WEBSOCKET_SUPPORT", "True").lower() == "true"
672
+ )
673
+
674
+
675
+ WEBSOCKET_MANAGER = os.environ.get("WEBSOCKET_MANAGER", "")
676
+
677
+ WEBSOCKET_REDIS_OPTIONS = os.environ.get("WEBSOCKET_REDIS_OPTIONS", "")
678
+
679
+
680
+ if WEBSOCKET_REDIS_OPTIONS == "":
681
+ if REDIS_SOCKET_CONNECT_TIMEOUT:
682
+ WEBSOCKET_REDIS_OPTIONS = {
683
+ "socket_connect_timeout": REDIS_SOCKET_CONNECT_TIMEOUT
684
+ }
685
+ else:
686
+ log.debug("No WEBSOCKET_REDIS_OPTIONS provided, defaulting to None")
687
+ WEBSOCKET_REDIS_OPTIONS = None
688
+ else:
689
+ try:
690
+ WEBSOCKET_REDIS_OPTIONS = json.loads(WEBSOCKET_REDIS_OPTIONS)
691
+ except Exception:
692
+ log.warning("Invalid WEBSOCKET_REDIS_OPTIONS, defaulting to None")
693
+ WEBSOCKET_REDIS_OPTIONS = None
694
+
695
+ WEBSOCKET_REDIS_URL = os.environ.get("WEBSOCKET_REDIS_URL", REDIS_URL)
696
+ WEBSOCKET_REDIS_CLUSTER = (
697
+ os.environ.get("WEBSOCKET_REDIS_CLUSTER", str(REDIS_CLUSTER)).lower() == "true"
698
+ )
699
+
700
+ websocket_redis_lock_timeout = os.environ.get("WEBSOCKET_REDIS_LOCK_TIMEOUT", "60")
701
+
702
+ try:
703
+ WEBSOCKET_REDIS_LOCK_TIMEOUT = int(websocket_redis_lock_timeout)
704
+ except ValueError:
705
+ WEBSOCKET_REDIS_LOCK_TIMEOUT = 60
706
+
707
+ WEBSOCKET_SENTINEL_HOSTS = os.environ.get("WEBSOCKET_SENTINEL_HOSTS", "")
708
+ WEBSOCKET_SENTINEL_PORT = os.environ.get("WEBSOCKET_SENTINEL_PORT", "26379")
709
+ WEBSOCKET_SERVER_LOGGING = (
710
+ os.environ.get("WEBSOCKET_SERVER_LOGGING", "False").lower() == "true"
711
+ )
712
+ WEBSOCKET_SERVER_ENGINEIO_LOGGING = (
713
+ os.environ.get(
714
+ "WEBSOCKET_SERVER_ENGINEIO_LOGGING",
715
+ os.environ.get("WEBSOCKET_SERVER_LOGGING", "False"),
716
+ ).lower()
717
+ == "true"
718
+ )
719
+ WEBSOCKET_SERVER_PING_TIMEOUT = os.environ.get("WEBSOCKET_SERVER_PING_TIMEOUT", "20")
720
+ try:
721
+ WEBSOCKET_SERVER_PING_TIMEOUT = int(WEBSOCKET_SERVER_PING_TIMEOUT)
722
+ except ValueError:
723
+ WEBSOCKET_SERVER_PING_TIMEOUT = 20
724
+
725
+ WEBSOCKET_SERVER_PING_INTERVAL = os.environ.get("WEBSOCKET_SERVER_PING_INTERVAL", "25")
726
+ try:
727
+ WEBSOCKET_SERVER_PING_INTERVAL = int(WEBSOCKET_SERVER_PING_INTERVAL)
728
+ except ValueError:
729
+ WEBSOCKET_SERVER_PING_INTERVAL = 25
730
+
731
+
732
+ REQUESTS_VERIFY = os.environ.get("REQUESTS_VERIFY", "True").lower() == "true"
733
+
734
+ AIOHTTP_CLIENT_TIMEOUT = os.environ.get("AIOHTTP_CLIENT_TIMEOUT", "")
735
+
736
+ if AIOHTTP_CLIENT_TIMEOUT == "":
737
+ AIOHTTP_CLIENT_TIMEOUT = None
738
+ else:
739
+ try:
740
+ AIOHTTP_CLIENT_TIMEOUT = int(AIOHTTP_CLIENT_TIMEOUT)
741
+ except Exception:
742
+ AIOHTTP_CLIENT_TIMEOUT = 300
743
+
744
+
745
+ AIOHTTP_CLIENT_SESSION_SSL = (
746
+ os.environ.get("AIOHTTP_CLIENT_SESSION_SSL", "True").lower() == "true"
747
+ )
748
+
749
+ AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = os.environ.get(
750
+ "AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST",
751
+ os.environ.get("AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST", "10"),
752
+ )
753
+
754
+ if AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST == "":
755
+ AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = None
756
+ else:
757
+ try:
758
+ AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = int(AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
759
+ except Exception:
760
+ AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = 10
761
+
762
+
763
+ AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = os.environ.get(
764
+ "AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA", "10"
765
+ )
766
+
767
+ if AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA == "":
768
+ AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = None
769
+ else:
770
+ try:
771
+ AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = int(
772
+ AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA
773
+ )
774
+ except Exception:
775
+ AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = 10
776
+
777
+
778
+ AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL = (
779
+ os.environ.get("AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL", "True").lower() == "true"
780
+ )
781
+
782
+
783
+ ####################################
784
+ # SENTENCE TRANSFORMERS
785
+ ####################################
786
+
787
+
788
+ SENTENCE_TRANSFORMERS_BACKEND = os.environ.get("SENTENCE_TRANSFORMERS_BACKEND", "")
789
+ if SENTENCE_TRANSFORMERS_BACKEND == "":
790
+ SENTENCE_TRANSFORMERS_BACKEND = "torch"
791
+
792
+
793
+ SENTENCE_TRANSFORMERS_MODEL_KWARGS = os.environ.get(
794
+ "SENTENCE_TRANSFORMERS_MODEL_KWARGS", ""
795
+ )
796
+ if SENTENCE_TRANSFORMERS_MODEL_KWARGS == "":
797
+ SENTENCE_TRANSFORMERS_MODEL_KWARGS = None
798
+ else:
799
+ try:
800
+ SENTENCE_TRANSFORMERS_MODEL_KWARGS = json.loads(
801
+ SENTENCE_TRANSFORMERS_MODEL_KWARGS
802
+ )
803
+ except Exception:
804
+ SENTENCE_TRANSFORMERS_MODEL_KWARGS = None
805
+
806
+
807
+ SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND = os.environ.get(
808
+ "SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND", ""
809
+ )
810
+ if SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND == "":
811
+ SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND = "torch"
812
+
813
+
814
+ SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = os.environ.get(
815
+ "SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS", ""
816
+ )
817
+ if SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS == "":
818
+ SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = None
819
+ else:
820
+ try:
821
+ SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = json.loads(
822
+ SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS
823
+ )
824
+ except Exception:
825
+ SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = None
826
+
827
+ # Whether to apply sigmoid normalization to CrossEncoder reranking scores.
828
+ # When enabled (default), scores are normalized to 0-1 range for proper
829
+ # relevance threshold behavior with MS MARCO models.
830
+ SENTENCE_TRANSFORMERS_CROSS_ENCODER_SIGMOID_ACTIVATION_FUNCTION = (
831
+ os.environ.get(
832
+ "SENTENCE_TRANSFORMERS_CROSS_ENCODER_SIGMOID_ACTIVATION_FUNCTION", "True"
833
+ ).lower()
834
+ == "true"
835
+ )
836
+
837
+ ####################################
838
+ # OFFLINE_MODE
839
+ ####################################
840
+
841
+ ENABLE_VERSION_UPDATE_CHECK = (
842
+ os.environ.get("ENABLE_VERSION_UPDATE_CHECK", "true").lower() == "true"
843
+ )
844
+ OFFLINE_MODE = os.environ.get("OFFLINE_MODE", "false").lower() == "true"
845
+
846
+ if OFFLINE_MODE:
847
+ os.environ["HF_HUB_OFFLINE"] = "1"
848
+ ENABLE_VERSION_UPDATE_CHECK = False
849
+
850
+ ####################################
851
+ # AUDIT LOGGING
852
+ ####################################
853
+
854
+
855
+ ENABLE_AUDIT_STDOUT = os.getenv("ENABLE_AUDIT_STDOUT", "False").lower() == "true"
856
+ ENABLE_AUDIT_LOGS_FILE = os.getenv("ENABLE_AUDIT_LOGS_FILE", "True").lower() == "true"
857
+
858
+ # Where to store log file
859
+ # Defaults to the DATA_DIR/audit.log. To set AUDIT_LOGS_FILE_PATH you need to
860
+ # provide the whole path, like: /app/audit.log
861
+ AUDIT_LOGS_FILE_PATH = os.getenv("AUDIT_LOGS_FILE_PATH", f"{DATA_DIR}/audit.log")
862
+ # Maximum size of a file before rotating into a new log file
863
+ AUDIT_LOG_FILE_ROTATION_SIZE = os.getenv("AUDIT_LOG_FILE_ROTATION_SIZE", "10MB")
864
+
865
+ # Comma separated list of logger names to use for audit logging
866
+ # Default is "uvicorn.access" which is the access log for Uvicorn
867
+ # You can add more logger names to this list if you want to capture more logs
868
+ AUDIT_UVICORN_LOGGER_NAMES = os.getenv(
869
+ "AUDIT_UVICORN_LOGGER_NAMES", "uvicorn.access"
870
+ ).split(",")
871
+
872
+ # METADATA | REQUEST | REQUEST_RESPONSE
873
+ AUDIT_LOG_LEVEL = os.getenv("AUDIT_LOG_LEVEL", "NONE").upper()
874
+ try:
875
+ MAX_BODY_LOG_SIZE = int(os.environ.get("MAX_BODY_LOG_SIZE") or 2048)
876
+ except ValueError:
877
+ MAX_BODY_LOG_SIZE = 2048
878
+
879
+ # Comma separated list for urls to exclude from audit
880
+ AUDIT_EXCLUDED_PATHS = os.getenv("AUDIT_EXCLUDED_PATHS", "/chats,/chat,/folders").split(
881
+ ","
882
+ )
883
+ AUDIT_EXCLUDED_PATHS = [path.strip() for path in AUDIT_EXCLUDED_PATHS]
884
+ AUDIT_EXCLUDED_PATHS = [path.lstrip("/") for path in AUDIT_EXCLUDED_PATHS]
885
+
886
+
887
+ ####################################
888
+ # OPENTELEMETRY
889
+ ####################################
890
+
891
+ ENABLE_OTEL = os.environ.get("ENABLE_OTEL", "False").lower() == "true"
892
+ ENABLE_OTEL_TRACES = os.environ.get("ENABLE_OTEL_TRACES", "False").lower() == "true"
893
+ ENABLE_OTEL_METRICS = os.environ.get("ENABLE_OTEL_METRICS", "False").lower() == "true"
894
+ ENABLE_OTEL_LOGS = os.environ.get("ENABLE_OTEL_LOGS", "False").lower() == "true"
895
+
896
+ OTEL_EXPORTER_OTLP_ENDPOINT = os.environ.get(
897
+ "OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317"
898
+ )
899
+ OTEL_METRICS_EXPORTER_OTLP_ENDPOINT = os.environ.get(
900
+ "OTEL_METRICS_EXPORTER_OTLP_ENDPOINT", OTEL_EXPORTER_OTLP_ENDPOINT
901
+ )
902
+ OTEL_LOGS_EXPORTER_OTLP_ENDPOINT = os.environ.get(
903
+ "OTEL_LOGS_EXPORTER_OTLP_ENDPOINT", OTEL_EXPORTER_OTLP_ENDPOINT
904
+ )
905
+ OTEL_EXPORTER_OTLP_INSECURE = (
906
+ os.environ.get("OTEL_EXPORTER_OTLP_INSECURE", "False").lower() == "true"
907
+ )
908
+ OTEL_METRICS_EXPORTER_OTLP_INSECURE = (
909
+ os.environ.get(
910
+ "OTEL_METRICS_EXPORTER_OTLP_INSECURE", str(OTEL_EXPORTER_OTLP_INSECURE)
911
+ ).lower()
912
+ == "true"
913
+ )
914
+ OTEL_LOGS_EXPORTER_OTLP_INSECURE = (
915
+ os.environ.get(
916
+ "OTEL_LOGS_EXPORTER_OTLP_INSECURE", str(OTEL_EXPORTER_OTLP_INSECURE)
917
+ ).lower()
918
+ == "true"
919
+ )
920
+ OTEL_SERVICE_NAME = os.environ.get("OTEL_SERVICE_NAME", "open-webui")
921
+ OTEL_RESOURCE_ATTRIBUTES = os.environ.get(
922
+ "OTEL_RESOURCE_ATTRIBUTES", ""
923
+ ) # e.g. key1=val1,key2=val2
924
+ OTEL_TRACES_SAMPLER = os.environ.get(
925
+ "OTEL_TRACES_SAMPLER", "parentbased_always_on"
926
+ ).lower()
927
+ OTEL_BASIC_AUTH_USERNAME = os.environ.get("OTEL_BASIC_AUTH_USERNAME", "")
928
+ OTEL_BASIC_AUTH_PASSWORD = os.environ.get("OTEL_BASIC_AUTH_PASSWORD", "")
929
+
930
+ OTEL_METRICS_BASIC_AUTH_USERNAME = os.environ.get(
931
+ "OTEL_METRICS_BASIC_AUTH_USERNAME", OTEL_BASIC_AUTH_USERNAME
932
+ )
933
+ OTEL_METRICS_BASIC_AUTH_PASSWORD = os.environ.get(
934
+ "OTEL_METRICS_BASIC_AUTH_PASSWORD", OTEL_BASIC_AUTH_PASSWORD
935
+ )
936
+ OTEL_LOGS_BASIC_AUTH_USERNAME = os.environ.get(
937
+ "OTEL_LOGS_BASIC_AUTH_USERNAME", OTEL_BASIC_AUTH_USERNAME
938
+ )
939
+ OTEL_LOGS_BASIC_AUTH_PASSWORD = os.environ.get(
940
+ "OTEL_LOGS_BASIC_AUTH_PASSWORD", OTEL_BASIC_AUTH_PASSWORD
941
+ )
942
+
943
+ OTEL_OTLP_SPAN_EXPORTER = os.environ.get(
944
+ "OTEL_OTLP_SPAN_EXPORTER", "grpc"
945
+ ).lower() # grpc or http
946
+
947
+ OTEL_METRICS_OTLP_SPAN_EXPORTER = os.environ.get(
948
+ "OTEL_METRICS_OTLP_SPAN_EXPORTER", OTEL_OTLP_SPAN_EXPORTER
949
+ ).lower() # grpc or http
950
+
951
+ OTEL_LOGS_OTLP_SPAN_EXPORTER = os.environ.get(
952
+ "OTEL_LOGS_OTLP_SPAN_EXPORTER", OTEL_OTLP_SPAN_EXPORTER
953
+ ).lower() # grpc or http
954
+
955
+ ####################################
956
+ # TOOLS/FUNCTIONS PIP OPTIONS
957
+ ####################################
958
+
959
+ PIP_OPTIONS = os.getenv("PIP_OPTIONS", "").split()
960
+ PIP_PACKAGE_INDEX_OPTIONS = os.getenv("PIP_PACKAGE_INDEX_OPTIONS", "").split()
961
+
962
+
963
+ ####################################
964
+ # PROGRESSIVE WEB APP OPTIONS
965
+ ####################################
966
+
967
+ EXTERNAL_PWA_MANIFEST_URL = os.environ.get("EXTERNAL_PWA_MANIFEST_URL")
backend/open_webui/functions.py ADDED
@@ -0,0 +1,350 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.constants import ERROR_MESSAGES
23
+ from open_webui.socket.main import (
24
+ get_event_call,
25
+ get_event_emitter,
26
+ )
27
+
28
+
29
+ from open_webui.models.users import UserModel
30
+ from open_webui.models.functions import Functions
31
+ from open_webui.models.models import Models
32
+
33
+ from open_webui.utils.plugin import (
34
+ load_function_module_by_id,
35
+ get_function_module_from_cache,
36
+ )
37
+ from open_webui.utils.tools import get_tools
38
+
39
+ from open_webui.env import 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
+ logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
54
+ log = logging.getLogger(__name__)
55
+
56
+
57
+ def get_function_module_by_id(request: Request, pipe_id: str):
58
+ function_module, _, _ = get_function_module_from_cache(request, pipe_id)
59
+
60
+ if hasattr(function_module, "valves") and hasattr(function_module, "Valves"):
61
+ Valves = function_module.Valves
62
+ valves = Functions.get_function_valves_by_id(pipe_id)
63
+
64
+ if valves:
65
+ try:
66
+ function_module.valves = Valves(
67
+ **{k: v for k, v in valves.items() if v is not None}
68
+ )
69
+ except Exception as e:
70
+ log.exception(f"Error loading valves for function {pipe_id}: {e}")
71
+ raise e
72
+ else:
73
+ function_module.valves = Valves()
74
+
75
+ return function_module
76
+
77
+
78
+ async def get_function_models(request):
79
+ pipes = Functions.get_functions_by_type("pipe", active_only=True)
80
+ pipe_models = []
81
+
82
+ for pipe in pipes:
83
+ try:
84
+ function_module = get_function_module_by_id(request, pipe.id)
85
+
86
+ has_user_valves = False
87
+ if hasattr(function_module, "UserValves"):
88
+ has_user_valves = True
89
+
90
+ # Check if function is a manifold
91
+ if hasattr(function_module, "pipes"):
92
+ sub_pipes = []
93
+
94
+ # Handle pipes being a list, sync function, or async function
95
+ try:
96
+ if callable(function_module.pipes):
97
+ if asyncio.iscoroutinefunction(function_module.pipes):
98
+ sub_pipes = await function_module.pipes()
99
+ else:
100
+ sub_pipes = function_module.pipes()
101
+ else:
102
+ sub_pipes = function_module.pipes
103
+ except Exception as e:
104
+ log.exception(e)
105
+ sub_pipes = []
106
+
107
+ log.debug(
108
+ f"get_function_models: function '{pipe.id}' is a manifold of {sub_pipes}"
109
+ )
110
+
111
+ for p in sub_pipes:
112
+ sub_pipe_id = f'{pipe.id}.{p["id"]}'
113
+ sub_pipe_name = p["name"]
114
+
115
+ if hasattr(function_module, "name"):
116
+ sub_pipe_name = f"{function_module.name}{sub_pipe_name}"
117
+
118
+ pipe_flag = {"type": pipe.type}
119
+
120
+ pipe_models.append(
121
+ {
122
+ "id": sub_pipe_id,
123
+ "name": sub_pipe_name,
124
+ "object": "model",
125
+ "created": pipe.created_at,
126
+ "owned_by": "openai",
127
+ "pipe": pipe_flag,
128
+ "has_user_valves": has_user_valves,
129
+ }
130
+ )
131
+ else:
132
+ pipe_flag = {"type": "pipe"}
133
+
134
+ log.debug(
135
+ f"get_function_models: function '{pipe.id}' is a single pipe {{ 'id': {pipe.id}, 'name': {pipe.name} }}"
136
+ )
137
+
138
+ pipe_models.append(
139
+ {
140
+ "id": pipe.id,
141
+ "name": pipe.name,
142
+ "object": "model",
143
+ "created": pipe.created_at,
144
+ "owned_by": "openai",
145
+ "pipe": pipe_flag,
146
+ "has_user_valves": has_user_valves,
147
+ }
148
+ )
149
+ except Exception as e:
150
+ log.exception(e)
151
+ continue
152
+
153
+ return pipe_models
154
+
155
+
156
+ async def generate_function_chat_completion(
157
+ request, form_data, user, models: dict = {}
158
+ ):
159
+ async def execute_pipe(pipe, params):
160
+ if inspect.iscoroutinefunction(pipe):
161
+ return await pipe(**params)
162
+ else:
163
+ return pipe(**params)
164
+
165
+ async def get_message_content(res: str | Generator | AsyncGenerator) -> str:
166
+ if isinstance(res, str):
167
+ return res
168
+ if isinstance(res, Generator):
169
+ return "".join(map(str, res))
170
+ if isinstance(res, AsyncGenerator):
171
+ return "".join([str(stream) async for stream in res])
172
+
173
+ def process_line(form_data: dict, line):
174
+ if isinstance(line, BaseModel):
175
+ line = line.model_dump_json()
176
+ line = f"data: {line}"
177
+ if isinstance(line, dict):
178
+ line = f"data: {json.dumps(line)}"
179
+
180
+ try:
181
+ line = line.decode("utf-8")
182
+ except Exception:
183
+ pass
184
+
185
+ if line.startswith("data:"):
186
+ return f"{line}\n\n"
187
+ else:
188
+ line = openai_chat_chunk_message_template(form_data["model"], line)
189
+ return f"data: {json.dumps(line)}\n\n"
190
+
191
+ def get_pipe_id(form_data: dict) -> str:
192
+ pipe_id = form_data["model"]
193
+ if "." in pipe_id:
194
+ pipe_id, _ = pipe_id.split(".", 1)
195
+ return pipe_id
196
+
197
+ def get_function_params(function_module, form_data, user, extra_params=None):
198
+ if extra_params is None:
199
+ extra_params = {}
200
+
201
+ pipe_id = get_pipe_id(form_data)
202
+
203
+ # Get the signature of the function
204
+ sig = inspect.signature(function_module.pipe)
205
+ params = {"body": form_data} | {
206
+ k: v for k, v in extra_params.items() if k in sig.parameters
207
+ }
208
+
209
+ if "__user__" in params and hasattr(function_module, "UserValves"):
210
+ user_valves = Functions.get_user_valves_by_id_and_user_id(pipe_id, user.id)
211
+ try:
212
+ params["__user__"]["valves"] = function_module.UserValves(**user_valves)
213
+ except Exception as e:
214
+ log.exception(e)
215
+ params["__user__"]["valves"] = function_module.UserValves()
216
+
217
+ return params
218
+
219
+ model_id = form_data.get("model")
220
+ model_info = Models.get_model_by_id(model_id)
221
+
222
+ metadata = form_data.pop("metadata", {})
223
+
224
+ files = metadata.get("files", [])
225
+ tool_ids = metadata.get("tool_ids", [])
226
+ # Check if tool_ids is None
227
+ if tool_ids is None:
228
+ tool_ids = []
229
+
230
+ __event_emitter__ = None
231
+ __event_call__ = None
232
+ __task__ = None
233
+ __task_body__ = None
234
+
235
+ if metadata:
236
+ if all(k in metadata for k in ("session_id", "chat_id", "message_id")):
237
+ __event_emitter__ = get_event_emitter(metadata)
238
+ __event_call__ = get_event_call(metadata)
239
+ __task__ = metadata.get("task", None)
240
+ __task_body__ = metadata.get("task_body", None)
241
+
242
+ oauth_token = None
243
+ try:
244
+ if request.cookies.get("oauth_session_id", None):
245
+ oauth_token = await request.app.state.oauth_manager.get_oauth_token(
246
+ user.id,
247
+ request.cookies.get("oauth_session_id", None),
248
+ )
249
+ except Exception as e:
250
+ log.error(f"Error getting OAuth token: {e}")
251
+
252
+ extra_params = {
253
+ "__event_emitter__": __event_emitter__,
254
+ "__event_call__": __event_call__,
255
+ "__chat_id__": metadata.get("chat_id", None),
256
+ "__session_id__": metadata.get("session_id", None),
257
+ "__message_id__": metadata.get("message_id", None),
258
+ "__task__": __task__,
259
+ "__task_body__": __task_body__,
260
+ "__files__": files,
261
+ "__user__": user.model_dump() if isinstance(user, UserModel) else {},
262
+ "__metadata__": metadata,
263
+ "__oauth_token__": oauth_token,
264
+ "__request__": request,
265
+ }
266
+ extra_params["__tools__"] = await get_tools(
267
+ request,
268
+ tool_ids,
269
+ user,
270
+ {
271
+ **extra_params,
272
+ "__model__": models.get(form_data["model"], None),
273
+ "__messages__": form_data["messages"],
274
+ "__files__": files,
275
+ },
276
+ )
277
+
278
+ if model_info:
279
+ if model_info.base_model_id:
280
+ form_data["model"] = model_info.base_model_id
281
+
282
+ params = model_info.params.model_dump()
283
+
284
+ if params:
285
+ system = params.pop("system", None)
286
+ form_data = apply_model_params_to_body_openai(params, form_data)
287
+ form_data = apply_system_prompt_to_body(system, form_data, metadata, user)
288
+
289
+ pipe_id = get_pipe_id(form_data)
290
+ function_module = get_function_module_by_id(request, pipe_id)
291
+
292
+ pipe = function_module.pipe
293
+ params = get_function_params(function_module, form_data, user, extra_params)
294
+
295
+ if form_data.get("stream", False):
296
+
297
+ async def stream_content():
298
+ try:
299
+ res = await execute_pipe(pipe, params)
300
+
301
+ # Directly return if the response is a StreamingResponse
302
+ if isinstance(res, StreamingResponse):
303
+ async for data in res.body_iterator:
304
+ yield data
305
+ return
306
+ if isinstance(res, dict):
307
+ yield f"data: {json.dumps(res)}\n\n"
308
+ return
309
+
310
+ except Exception as e:
311
+ log.error(f"Error: {e}")
312
+ yield f"data: {json.dumps({'error': {'detail':str(e)}})}\n\n"
313
+ return
314
+
315
+ if isinstance(res, str):
316
+ message = openai_chat_chunk_message_template(form_data["model"], res)
317
+ yield f"data: {json.dumps(message)}\n\n"
318
+
319
+ if isinstance(res, Iterator):
320
+ for line in res:
321
+ yield process_line(form_data, line)
322
+
323
+ if isinstance(res, AsyncGenerator):
324
+ async for line in res:
325
+ yield process_line(form_data, line)
326
+
327
+ if isinstance(res, str) or isinstance(res, Generator):
328
+ finish_message = openai_chat_chunk_message_template(
329
+ form_data["model"], ""
330
+ )
331
+ finish_message["choices"][0]["finish_reason"] = "stop"
332
+ yield f"data: {json.dumps(finish_message)}\n\n"
333
+ yield "data: [DONE]"
334
+
335
+ return StreamingResponse(stream_content(), media_type="text/event-stream")
336
+ else:
337
+ try:
338
+ res = await execute_pipe(pipe, params)
339
+
340
+ except Exception as e:
341
+ log.error(f"Error: {e}")
342
+ return {"error": {"detail": str(e)}}
343
+
344
+ if isinstance(res, StreamingResponse) or isinstance(res, dict):
345
+ return res
346
+ if isinstance(res, BaseModel):
347
+ return res.model_dump()
348
+
349
+ message = await get_message_content(res)
350
+ return openai_chat_completion_message_template(form_data["model"], message)
backend/open_webui/internal/db.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ DATABASE_POOL_MAX_OVERFLOW,
13
+ DATABASE_POOL_RECYCLE,
14
+ DATABASE_POOL_SIZE,
15
+ DATABASE_POOL_TIMEOUT,
16
+ DATABASE_ENABLE_SQLITE_WAL,
17
+ DATABASE_ENABLE_SESSION_SHARING,
18
+ ENABLE_DB_MIGRATIONS,
19
+ )
20
+ from peewee_migrate import Router
21
+ from sqlalchemy import Dialect, create_engine, MetaData, event, types
22
+ from sqlalchemy.ext.declarative import declarative_base
23
+ from sqlalchemy.orm import scoped_session, sessionmaker, Session
24
+ from sqlalchemy.pool import QueuePool, NullPool
25
+ from sqlalchemy.sql.type_api import _T
26
+ from typing_extensions import Self
27
+
28
+ log = logging.getLogger(__name__)
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
+ if ENABLE_DB_MIGRATIONS:
81
+ handle_peewee_migration(DATABASE_URL)
82
+
83
+
84
+ SQLALCHEMY_DATABASE_URL = DATABASE_URL
85
+
86
+ # Handle SQLCipher URLs
87
+ if SQLALCHEMY_DATABASE_URL.startswith("sqlite+sqlcipher://"):
88
+ database_password = os.environ.get("DATABASE_PASSWORD")
89
+ if not database_password or database_password.strip() == "":
90
+ raise ValueError(
91
+ "DATABASE_PASSWORD is required when using sqlite+sqlcipher:// URLs"
92
+ )
93
+
94
+ # Extract database path from SQLCipher URL
95
+ db_path = SQLALCHEMY_DATABASE_URL.replace("sqlite+sqlcipher://", "")
96
+
97
+ # Create a custom creator function that uses sqlcipher3
98
+ def create_sqlcipher_connection():
99
+ import sqlcipher3
100
+
101
+ conn = sqlcipher3.connect(db_path, check_same_thread=False)
102
+ conn.execute(f"PRAGMA key = '{database_password}'")
103
+ return conn
104
+
105
+ engine = create_engine(
106
+ "sqlite://", # Dummy URL since we're using creator
107
+ creator=create_sqlcipher_connection,
108
+ echo=False,
109
+ )
110
+
111
+ log.info("Connected to encrypted SQLite database using SQLCipher")
112
+
113
+ elif "sqlite" in SQLALCHEMY_DATABASE_URL:
114
+ engine = create_engine(
115
+ SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
116
+ )
117
+
118
+ def on_connect(dbapi_connection, connection_record):
119
+ cursor = dbapi_connection.cursor()
120
+ if DATABASE_ENABLE_SQLITE_WAL:
121
+ cursor.execute("PRAGMA journal_mode=WAL")
122
+ else:
123
+ cursor.execute("PRAGMA journal_mode=DELETE")
124
+ cursor.close()
125
+
126
+ event.listen(engine, "connect", on_connect)
127
+ else:
128
+ if isinstance(DATABASE_POOL_SIZE, int):
129
+ if DATABASE_POOL_SIZE > 0:
130
+ engine = create_engine(
131
+ SQLALCHEMY_DATABASE_URL,
132
+ pool_size=DATABASE_POOL_SIZE,
133
+ max_overflow=DATABASE_POOL_MAX_OVERFLOW,
134
+ pool_timeout=DATABASE_POOL_TIMEOUT,
135
+ pool_recycle=DATABASE_POOL_RECYCLE,
136
+ pool_pre_ping=True,
137
+ poolclass=QueuePool,
138
+ )
139
+ else:
140
+ engine = create_engine(
141
+ SQLALCHEMY_DATABASE_URL, pool_pre_ping=True, poolclass=NullPool
142
+ )
143
+ else:
144
+ engine = create_engine(SQLALCHEMY_DATABASE_URL, pool_pre_ping=True)
145
+
146
+
147
+ SessionLocal = sessionmaker(
148
+ autocommit=False, autoflush=False, bind=engine, expire_on_commit=False
149
+ )
150
+ metadata_obj = MetaData(schema=DATABASE_SCHEMA)
151
+ Base = declarative_base(metadata=metadata_obj)
152
+ ScopedSession = scoped_session(SessionLocal)
153
+
154
+
155
+ def get_session():
156
+ db = SessionLocal()
157
+ try:
158
+ yield db
159
+ finally:
160
+ db.close()
161
+
162
+
163
+ get_db = contextmanager(get_session)
164
+
165
+
166
+ @contextmanager
167
+ def get_db_context(db: Optional[Session] = None):
168
+ if isinstance(db, Session) and DATABASE_ENABLE_SESSION_SHARING:
169
+ yield db
170
+ else:
171
+ with get_db() as session:
172
+ yield session
backend/open_webui/internal/migrations/001_initial_schema.py ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ with suppress(ImportError):
33
+ import playhouse.postgres_ext as pw_pext
34
+
35
+
36
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
37
+ """Write your migrations here."""
38
+
39
+ # We perform different migrations for SQLite and other databases
40
+ # This is because SQLite is very loose with enforcing its schema, and trying to migrate other databases like SQLite
41
+ # will require per-database SQL queries.
42
+ # Instead, we assume that because external DB support was added at a later date, it is safe to assume a newer base
43
+ # schema instead of trying to migrate from an older schema.
44
+ if isinstance(database, pw.SqliteDatabase):
45
+ migrate_sqlite(migrator, database, fake=fake)
46
+ else:
47
+ migrate_external(migrator, database, fake=fake)
48
+
49
+
50
+ def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
51
+ @migrator.create_model
52
+ class Auth(pw.Model):
53
+ id = pw.CharField(max_length=255, unique=True)
54
+ email = pw.CharField(max_length=255)
55
+ password = pw.CharField(max_length=255)
56
+ active = pw.BooleanField()
57
+
58
+ class Meta:
59
+ table_name = "auth"
60
+
61
+ @migrator.create_model
62
+ class Chat(pw.Model):
63
+ id = pw.CharField(max_length=255, unique=True)
64
+ user_id = pw.CharField(max_length=255)
65
+ title = pw.CharField()
66
+ chat = pw.TextField()
67
+ timestamp = pw.BigIntegerField()
68
+
69
+ class Meta:
70
+ table_name = "chat"
71
+
72
+ @migrator.create_model
73
+ class ChatIdTag(pw.Model):
74
+ id = pw.CharField(max_length=255, unique=True)
75
+ tag_name = pw.CharField(max_length=255)
76
+ chat_id = pw.CharField(max_length=255)
77
+ user_id = pw.CharField(max_length=255)
78
+ timestamp = pw.BigIntegerField()
79
+
80
+ class Meta:
81
+ table_name = "chatidtag"
82
+
83
+ @migrator.create_model
84
+ class Document(pw.Model):
85
+ id = pw.AutoField()
86
+ collection_name = pw.CharField(max_length=255, unique=True)
87
+ name = pw.CharField(max_length=255, unique=True)
88
+ title = pw.CharField()
89
+ filename = pw.CharField()
90
+ content = pw.TextField(null=True)
91
+ user_id = pw.CharField(max_length=255)
92
+ timestamp = pw.BigIntegerField()
93
+
94
+ class Meta:
95
+ table_name = "document"
96
+
97
+ @migrator.create_model
98
+ class Modelfile(pw.Model):
99
+ id = pw.AutoField()
100
+ tag_name = pw.CharField(max_length=255, unique=True)
101
+ user_id = pw.CharField(max_length=255)
102
+ modelfile = pw.TextField()
103
+ timestamp = pw.BigIntegerField()
104
+
105
+ class Meta:
106
+ table_name = "modelfile"
107
+
108
+ @migrator.create_model
109
+ class Prompt(pw.Model):
110
+ id = pw.AutoField()
111
+ command = pw.CharField(max_length=255, unique=True)
112
+ user_id = pw.CharField(max_length=255)
113
+ title = pw.CharField()
114
+ content = pw.TextField()
115
+ timestamp = pw.BigIntegerField()
116
+
117
+ class Meta:
118
+ table_name = "prompt"
119
+
120
+ @migrator.create_model
121
+ class Tag(pw.Model):
122
+ id = pw.CharField(max_length=255, unique=True)
123
+ name = pw.CharField(max_length=255)
124
+ user_id = pw.CharField(max_length=255)
125
+ data = pw.TextField(null=True)
126
+
127
+ class Meta:
128
+ table_name = "tag"
129
+
130
+ @migrator.create_model
131
+ class User(pw.Model):
132
+ id = pw.CharField(max_length=255, unique=True)
133
+ name = pw.CharField(max_length=255)
134
+ email = pw.CharField(max_length=255)
135
+ role = pw.CharField(max_length=255)
136
+ profile_image_url = pw.CharField(max_length=255)
137
+ timestamp = pw.BigIntegerField()
138
+
139
+ class Meta:
140
+ table_name = "user"
141
+
142
+
143
+ def migrate_external(migrator: Migrator, database: pw.Database, *, fake=False):
144
+ @migrator.create_model
145
+ class Auth(pw.Model):
146
+ id = pw.CharField(max_length=255, unique=True)
147
+ email = pw.CharField(max_length=255)
148
+ password = pw.TextField()
149
+ active = pw.BooleanField()
150
+
151
+ class Meta:
152
+ table_name = "auth"
153
+
154
+ @migrator.create_model
155
+ class Chat(pw.Model):
156
+ id = pw.CharField(max_length=255, unique=True)
157
+ user_id = pw.CharField(max_length=255)
158
+ title = pw.TextField()
159
+ chat = pw.TextField()
160
+ timestamp = pw.BigIntegerField()
161
+
162
+ class Meta:
163
+ table_name = "chat"
164
+
165
+ @migrator.create_model
166
+ class ChatIdTag(pw.Model):
167
+ id = pw.CharField(max_length=255, unique=True)
168
+ tag_name = pw.CharField(max_length=255)
169
+ chat_id = pw.CharField(max_length=255)
170
+ user_id = pw.CharField(max_length=255)
171
+ timestamp = pw.BigIntegerField()
172
+
173
+ class Meta:
174
+ table_name = "chatidtag"
175
+
176
+ @migrator.create_model
177
+ class Document(pw.Model):
178
+ id = pw.AutoField()
179
+ collection_name = pw.CharField(max_length=255, unique=True)
180
+ name = pw.CharField(max_length=255, unique=True)
181
+ title = pw.TextField()
182
+ filename = pw.TextField()
183
+ content = pw.TextField(null=True)
184
+ user_id = pw.CharField(max_length=255)
185
+ timestamp = pw.BigIntegerField()
186
+
187
+ class Meta:
188
+ table_name = "document"
189
+
190
+ @migrator.create_model
191
+ class Modelfile(pw.Model):
192
+ id = pw.AutoField()
193
+ tag_name = pw.CharField(max_length=255, unique=True)
194
+ user_id = pw.CharField(max_length=255)
195
+ modelfile = pw.TextField()
196
+ timestamp = pw.BigIntegerField()
197
+
198
+ class Meta:
199
+ table_name = "modelfile"
200
+
201
+ @migrator.create_model
202
+ class Prompt(pw.Model):
203
+ id = pw.AutoField()
204
+ command = pw.CharField(max_length=255, unique=True)
205
+ user_id = pw.CharField(max_length=255)
206
+ title = pw.TextField()
207
+ content = pw.TextField()
208
+ timestamp = pw.BigIntegerField()
209
+
210
+ class Meta:
211
+ table_name = "prompt"
212
+
213
+ @migrator.create_model
214
+ class Tag(pw.Model):
215
+ id = pw.CharField(max_length=255, unique=True)
216
+ name = pw.CharField(max_length=255)
217
+ user_id = pw.CharField(max_length=255)
218
+ data = pw.TextField(null=True)
219
+
220
+ class Meta:
221
+ table_name = "tag"
222
+
223
+ @migrator.create_model
224
+ class User(pw.Model):
225
+ id = pw.CharField(max_length=255, unique=True)
226
+ name = pw.CharField(max_length=255)
227
+ email = pw.CharField(max_length=255)
228
+ role = pw.CharField(max_length=255)
229
+ profile_image_url = pw.TextField()
230
+ timestamp = pw.BigIntegerField()
231
+
232
+ class Meta:
233
+ table_name = "user"
234
+
235
+
236
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
237
+ """Write your rollback migrations here."""
238
+
239
+ migrator.remove_model("user")
240
+
241
+ migrator.remove_model("tag")
242
+
243
+ migrator.remove_model("prompt")
244
+
245
+ migrator.remove_model("modelfile")
246
+
247
+ migrator.remove_model("document")
248
+
249
+ migrator.remove_model("chatidtag")
250
+
251
+ migrator.remove_model("chat")
252
+
253
+ migrator.remove_model("auth")
backend/open_webui/internal/migrations/002_add_local_sharing.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ with suppress(ImportError):
33
+ import playhouse.postgres_ext as pw_pext
34
+
35
+
36
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
37
+ """Write your migrations here."""
38
+
39
+ migrator.add_fields(
40
+ "chat", share_id=pw.CharField(max_length=255, null=True, unique=True)
41
+ )
42
+
43
+
44
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
45
+ """Write your rollback migrations here."""
46
+
47
+ migrator.remove_fields("chat", "share_id")
backend/open_webui/internal/migrations/003_add_auth_api_key.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ with suppress(ImportError):
33
+ import playhouse.postgres_ext as pw_pext
34
+
35
+
36
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
37
+ """Write your migrations here."""
38
+
39
+ migrator.add_fields(
40
+ "user", api_key=pw.CharField(max_length=255, null=True, unique=True)
41
+ )
42
+
43
+
44
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
45
+ """Write your rollback migrations here."""
46
+
47
+ migrator.remove_fields("user", "api_key")
backend/open_webui/internal/migrations/004_add_archived.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ with suppress(ImportError):
33
+ import playhouse.postgres_ext as pw_pext
34
+
35
+
36
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
37
+ """Write your migrations here."""
38
+
39
+ migrator.add_fields("chat", archived=pw.BooleanField(default=False))
40
+
41
+
42
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
43
+ """Write your rollback migrations here."""
44
+
45
+ migrator.remove_fields("chat", "archived")
backend/open_webui/internal/migrations/005_add_updated_at.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ with suppress(ImportError):
33
+ import playhouse.postgres_ext as pw_pext
34
+
35
+
36
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
37
+ """Write your migrations here."""
38
+
39
+ if isinstance(database, pw.SqliteDatabase):
40
+ migrate_sqlite(migrator, database, fake=fake)
41
+ else:
42
+ migrate_external(migrator, database, fake=fake)
43
+
44
+
45
+ def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
46
+ # Adding fields created_at and updated_at to the 'chat' table
47
+ migrator.add_fields(
48
+ "chat",
49
+ created_at=pw.DateTimeField(null=True), # Allow null for transition
50
+ updated_at=pw.DateTimeField(null=True), # Allow null for transition
51
+ )
52
+
53
+ # Populate the new fields from an existing 'timestamp' field
54
+ migrator.sql(
55
+ "UPDATE chat SET created_at = timestamp, updated_at = timestamp WHERE timestamp IS NOT NULL"
56
+ )
57
+
58
+ # Now that the data has been copied, remove the original 'timestamp' field
59
+ migrator.remove_fields("chat", "timestamp")
60
+
61
+ # Update the fields to be not null now that they are populated
62
+ migrator.change_fields(
63
+ "chat",
64
+ created_at=pw.DateTimeField(null=False),
65
+ updated_at=pw.DateTimeField(null=False),
66
+ )
67
+
68
+
69
+ def migrate_external(migrator: Migrator, database: pw.Database, *, fake=False):
70
+ # Adding fields created_at and updated_at to the 'chat' table
71
+ migrator.add_fields(
72
+ "chat",
73
+ created_at=pw.BigIntegerField(null=True), # Allow null for transition
74
+ updated_at=pw.BigIntegerField(null=True), # Allow null for transition
75
+ )
76
+
77
+ # Populate the new fields from an existing 'timestamp' field
78
+ migrator.sql(
79
+ "UPDATE chat SET created_at = timestamp, updated_at = timestamp WHERE timestamp IS NOT NULL"
80
+ )
81
+
82
+ # Now that the data has been copied, remove the original 'timestamp' field
83
+ migrator.remove_fields("chat", "timestamp")
84
+
85
+ # Update the fields to be not null now that they are populated
86
+ migrator.change_fields(
87
+ "chat",
88
+ created_at=pw.BigIntegerField(null=False),
89
+ updated_at=pw.BigIntegerField(null=False),
90
+ )
91
+
92
+
93
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
94
+ """Write your rollback migrations here."""
95
+
96
+ if isinstance(database, pw.SqliteDatabase):
97
+ rollback_sqlite(migrator, database, fake=fake)
98
+ else:
99
+ rollback_external(migrator, database, fake=fake)
100
+
101
+
102
+ def rollback_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
103
+ # Recreate the timestamp field initially allowing null values for safe transition
104
+ migrator.add_fields("chat", timestamp=pw.DateTimeField(null=True))
105
+
106
+ # Copy the earliest created_at date back into the new timestamp field
107
+ # This assumes created_at was originally a copy of timestamp
108
+ migrator.sql("UPDATE chat SET timestamp = created_at")
109
+
110
+ # Remove the created_at and updated_at fields
111
+ migrator.remove_fields("chat", "created_at", "updated_at")
112
+
113
+ # Finally, alter the timestamp field to not allow nulls if that was the original setting
114
+ migrator.change_fields("chat", timestamp=pw.DateTimeField(null=False))
115
+
116
+
117
+ def rollback_external(migrator: Migrator, database: pw.Database, *, fake=False):
118
+ # Recreate the timestamp field initially allowing null values for safe transition
119
+ migrator.add_fields("chat", timestamp=pw.BigIntegerField(null=True))
120
+
121
+ # Copy the earliest created_at date back into the new timestamp field
122
+ # This assumes created_at was originally a copy of timestamp
123
+ migrator.sql("UPDATE chat SET timestamp = created_at")
124
+
125
+ # Remove the created_at and updated_at fields
126
+ migrator.remove_fields("chat", "created_at", "updated_at")
127
+
128
+ # Finally, alter the timestamp field to not allow nulls if that was the original setting
129
+ migrator.change_fields("chat", timestamp=pw.BigIntegerField(null=False))