Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a0ccd3d2ea |
+1
-13
@@ -13,22 +13,10 @@ OPENAI_API_KEY=''
|
||||
# CORS_ALLOW_ORIGIN='http://localhost:5173;http://localhost:8080'
|
||||
CORS_ALLOW_ORIGIN='*'
|
||||
|
||||
# Set to false to keep memory tools enabled without adding memory context to the system context.
|
||||
ENABLE_MEMORY_SYSTEM_CONTEXT=true
|
||||
|
||||
# For production you should set this to match the proxy configuration (127.0.0.1)
|
||||
FORWARDED_ALLOW_IPS='*'
|
||||
|
||||
# DO NOT TRACK
|
||||
SCARF_NO_ANALYTICS=true
|
||||
DO_NOT_TRACK=true
|
||||
ANONYMIZED_TELEMETRY=false
|
||||
|
||||
# Valkey Vector Store (requires VECTOR_DB=valkey)
|
||||
# VALKEY_URL='valkey://localhost:6379'
|
||||
# VALKEY_COLLECTION_PREFIX='open_webui'
|
||||
# VALKEY_INDEX_TYPE='HNSW'
|
||||
# VALKEY_DISTANCE_METRIC='COSINE'
|
||||
# VALKEY_HNSW_M='16'
|
||||
# VALKEY_HNSW_EF_CONSTRUCTION='200'
|
||||
# VALKEY_HNSW_EF_RUNTIME='10'
|
||||
ANONYMIZED_TELEMETRY=false
|
||||
+1
-1
@@ -1 +1 @@
|
||||
github: open-webui
|
||||
github: tjbck
|
||||
|
||||
@@ -1,5 +1 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 🔒 Report a Security Vulnerability
|
||||
url: https://github.com/open-webui/open-webui/security
|
||||
about: Do NOT open a public issue for security vulnerabilities, suspected vulnerabilities, or any security-related concern. Please review our Security Policy and report privately via the "Report a vulnerability" button so it can be handled as a private advisory.
|
||||
|
||||
@@ -1,76 +1,82 @@
|
||||
name: Feature Request
|
||||
description: Suggest a new feature or improvement
|
||||
description: Suggest an idea for this project
|
||||
title: 'feat: '
|
||||
labels: ['triage']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Before Submitting
|
||||
## Important Notes
|
||||
### Before submitting
|
||||
|
||||
Please check the **open AND closed** [Issues](https://github.com/open-webui/open-webui/issues) AND [Discussions](https://github.com/open-webui/open-webui/discussions) to see if a similar request has been posted.
|
||||
It's likely we're already tracking it! If you’re unsure, start a discussion post first.
|
||||
|
||||
Please check **open AND closed** [Issues](https://github.com/open-webui/open-webui/issues) and [Discussions](https://github.com/open-webui/open-webui/discussions) for similar requests. If you find one, add your input there instead.
|
||||
#### Scope
|
||||
|
||||
### Scope Guidelines
|
||||
If your feature request is likely to take more than a quick coding session to implement, test and verify, then open it in the **Ideas** section of the [Discussions](https://github.com/open-webui/open-webui/discussions) instead.
|
||||
**We will close and force move your feature request to the Ideas section, if we believe your feature request is not trivial/quick to implement.**
|
||||
This is to ensure the issues tab is used only for issues, quickly addressable feature requests and tracking tickets by the maintainers.
|
||||
Other feature requests belong in the **Ideas** section of the [Discussions](https://github.com/open-webui/open-webui/discussions).
|
||||
|
||||
If your feature request might impact others in the community, definitely open a discussion instead and evaluate whether and how to implement it.
|
||||
|
||||
This will help us efficiently focus on improving the project.
|
||||
|
||||
### Collaborate respectfully
|
||||
We value a **constructive attitude**, so please be mindful of your communication. If negativity is part of your approach, our capacity to engage may be limited. We're here to help if you're **open to learning** and **communicating positively**.
|
||||
|
||||
Feature requests that require significant implementation effort should be posted in the **Ideas** section of [Discussions](https://github.com/open-webui/open-webui/discussions) instead. We will move oversized feature requests to Discussions to keep the Issues tab focused on actionable items.
|
||||
Remember:
|
||||
- Open WebUI is a **volunteer-driven project**
|
||||
- It's managed by a **single maintainer**
|
||||
- It's supported by contributors who also have **full-time jobs**
|
||||
|
||||
If your request might impact the broader community, please open a Discussion first so others can weigh in on the design.
|
||||
|
||||
### Be Respectful
|
||||
|
||||
Open WebUI is a volunteer-driven project maintained by a small team. We value constructive, positive communication. Please be mindful of maintainers' time and energy.
|
||||
We appreciate your time and ask that you **respect ours**.
|
||||
|
||||
### Contributing
|
||||
If you encounter an issue, we highly encourage you to submit a pull request or fork the project. We actively work to prevent contributor burnout to maintain the quality and continuity of Open WebUI.
|
||||
|
||||
If you encounter an issue, we encourage you to submit a pull request or fork the project. We actively work to prevent contributor burnout and maintain project quality.
|
||||
|
||||
### Reproducibility
|
||||
|
||||
If a bug cannot be reproduced with a `:main` or `:dev` Docker setup, or a `pip install` with Python 3.11, it may be moved to the "Issues" section in Discussions for community assistance.
|
||||
### Bug reproducibility
|
||||
If a bug cannot be reproduced with a `:main` or `:dev` Docker setup, or a `pip install` with Python 3.11, it may require additional help from the community. In such cases, we will move it to the "[issues](https://github.com/open-webui/open-webui/discussions/categories/issues)" Discussions section due to our limited resources. We encourage the community to assist with these issues. Remember, it’s not that the issue doesn’t exist; we need your help!
|
||||
|
||||
- type: checkboxes
|
||||
id: existing-issue
|
||||
attributes:
|
||||
label: Check Existing Issues
|
||||
description: Confirm you have searched for similar requests.
|
||||
description: Please confirm that you've checked for existing similar requests
|
||||
options:
|
||||
- label: I have searched all existing **open AND closed** issues and discussions and found none comparable to my request.
|
||||
- label: I have searched for all existing **open AND closed** issues and discussions for similar requests. I have found none that is comparable to my request.
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: feature-scope
|
||||
attributes:
|
||||
label: Verify Feature Scope
|
||||
description: Confirm this request belongs in Issues rather than Discussions.
|
||||
description: Please confirm the feature's scope is within the described scope
|
||||
options:
|
||||
- label: I believe this feature request is appropriately scoped for the Issues section as described above.
|
||||
- label: I have read through and understood the scope definition for feature requests in the Issues section. I believe my feature request meets the definition and belongs in the Issues section instead of the Discussions.
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: problem-description
|
||||
attributes:
|
||||
label: Problem Description
|
||||
description: Is this related to a problem? Describe the pain point clearly.
|
||||
placeholder: "e.g., I'm frustrated when..."
|
||||
description: Is your feature request related to a problem? Please provide a clear and concise description of what the problem is.
|
||||
placeholder: "Ex. I'm always frustrated when... / Not related to a problem"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: solution-description
|
||||
attributes:
|
||||
label: Proposed Solution
|
||||
description: Describe what you would like to happen.
|
||||
label: Desired Solution you'd like
|
||||
description: Clearly describe what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives-considered
|
||||
attributes:
|
||||
label: Alternatives Considered
|
||||
description: Describe any alternative solutions or workarounds you have considered.
|
||||
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context, mockups, or screenshots about the feature request.
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
|
||||
+5
-10
@@ -4,22 +4,17 @@ updates:
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: monthly
|
||||
target-branch: dev
|
||||
target-branch: 'dev'
|
||||
|
||||
- package-ecosystem: pip
|
||||
directory: '/backend'
|
||||
schedule:
|
||||
interval: monthly
|
||||
target-branch: dev
|
||||
target-branch: 'dev'
|
||||
|
||||
- package-ecosystem: github-actions
|
||||
- package-ecosystem: 'github-actions'
|
||||
directory: '/'
|
||||
schedule:
|
||||
# Check for updates to GitHub Actions every week
|
||||
interval: monthly
|
||||
target-branch: dev
|
||||
|
||||
- package-ecosystem: npm
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: monthly
|
||||
target-branch: dev
|
||||
target-branch: 'dev'
|
||||
|
||||
@@ -18,75 +18,75 @@ The most impactful way to contribute to Open WebUI is through well-written bug r
|
||||
|
||||
**Before submitting, make sure you've checked the following:**
|
||||
|
||||
- [ ] **Linked Issue/Discussion:** This PR references an existing [Issue](https://github.com/open-webui/open-webui/issues) or [Discussion](https://github.com/open-webui/open-webui/discussions) — `Closes #___` / `Relates to #___`. If one does not exist, create one first. PRs without a linked issue or discussion may be closed without review.
|
||||
- [ ] **Target branch:** The pull request targets the `dev` branch. **PRs targeting `main` will be immediately closed.**
|
||||
- [ ] **Description:** A concise description of the changes is provided below.
|
||||
- [ ] **Changelog:** A changelog entry following [Keep a Changelog](https://keepachangelog.com/) format is included at the bottom.
|
||||
- [ ] **Documentation:** Relevant documentation has been added or updated in the [Open WebUI Docs Repository](https://github.com/open-webui/docs).
|
||||
- [ ] **Dependencies:** Any new or updated dependencies are explained, tested, and documented.
|
||||
- [ ] **Testing:** Manual tests have been performed to verify the fix/feature works correctly and does not introduce regressions. Screenshots or recordings are included where applicable.
|
||||
- [ ] **No Unchecked AI Code:** This PR is either human-written or has undergone thorough human review AND manual testing. Unreviewed AI-generated PRs may be closed immediately.
|
||||
- [ ] **Self-Review:** A self-review of the code has been performed, ensuring adherence to project coding standards.
|
||||
- [ ] **Architecture:** Smart defaults are preferred over new settings. Local state is used for ephemeral UI logic. Major architectural or UX changes have been discussed first.
|
||||
- [ ] **Git Hygiene:** The PR is atomic (one logical change), rebased on `dev`, and contains no unrelated commits.
|
||||
- [ ] **Title Prefix:** The PR title uses one of the following prefixes:
|
||||
- **BREAKING CHANGE**: Changes affecting backward compatibility
|
||||
- **build**: Build system or dependency changes
|
||||
- **ci**: CI/CD workflow changes
|
||||
- **chore**: Refactoring, cleanup, or non-functional changes
|
||||
- **docs**: Documentation additions or updates
|
||||
- **feat**: New features or enhancements
|
||||
- **fix**: Bug fixes or corrections
|
||||
- [ ] **Target branch:** Verify that the pull request targets the `dev` branch. **PRs targeting `main` will be immediately closed.**
|
||||
- [ ] **Description:** Provide a concise description of the changes made in this pull request down below.
|
||||
- [ ] **Changelog:** Ensure a changelog entry following the format of [Keep a Changelog](https://keepachangelog.com/) is added at the bottom of the PR description.
|
||||
- [ ] **Documentation:** 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.
|
||||
- [ ] **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.
|
||||
- [ ] **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**.
|
||||
- [ ] **Agentic AI Code:** Confirm this Pull Request is **not written by any AI Agent** or has at least **gone through additional human review AND manual testing**. If any AI Agent is the co-author of this PR, it may lead to immediate closure of the PR.
|
||||
- [ ] **Code review:** Have you performed a self-review of your code, addressing any coding standard issues and ensuring adherence to the project's coding standards?
|
||||
- [ ] **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.
|
||||
- [ ] **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.
|
||||
- [ ] **Title Prefix:** To clearly categorize this pull request, prefix the pull request title using one of the following:
|
||||
- **BREAKING CHANGE**: Significant changes that may affect compatibility
|
||||
- **build**: Changes that affect the build system or external dependencies
|
||||
- **ci**: Changes to our continuous integration processes or workflows
|
||||
- **chore**: Refactor, cleanup, or other non-functional code changes
|
||||
- **docs**: Documentation update or addition
|
||||
- **feat**: Introduces a new feature or enhancement to the codebase
|
||||
- **fix**: Bug fix or error correction
|
||||
- **i18n**: Internationalization or localization changes
|
||||
- **perf**: Performance improvements
|
||||
- **refactor**: Code restructuring
|
||||
- **style**: Formatting changes (whitespace, semicolons, etc.)
|
||||
- **test**: Test additions or corrections
|
||||
- **WIP**: Work in progress
|
||||
- **perf**: Performance improvement
|
||||
- **refactor**: Code restructuring for better maintainability, readability, or scalability
|
||||
- **style**: Changes that do not affect the meaning of the code (white space, formatting, missing semi-colons, etc.)
|
||||
- **test**: Adding missing tests or correcting existing tests
|
||||
- **WIP**: Work in progress, a temporary label for incomplete or ongoing work
|
||||
|
||||
# Changelog Entry
|
||||
|
||||
### Description
|
||||
|
||||
- [Describe the changes, including motivation and impact]
|
||||
- [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)]
|
||||
|
||||
### Added
|
||||
|
||||
- [New features, functionalities, or additions]
|
||||
- [List any new features, functionalities, or additions]
|
||||
|
||||
### Changed
|
||||
|
||||
- [Changes, updates, refactorings, or optimizations]
|
||||
- [List any changes, updates, refactorings, or optimizations]
|
||||
|
||||
### Deprecated
|
||||
|
||||
- [Deprecated functionality or features]
|
||||
- [List any deprecated functionality or features that have been removed]
|
||||
|
||||
### Removed
|
||||
|
||||
- [Removed features, files, or functionalities]
|
||||
- [List any removed features, files, or functionalities]
|
||||
|
||||
### Fixed
|
||||
|
||||
- [Bug fixes or corrections]
|
||||
- [List any fixes, corrections, or bug fixes]
|
||||
|
||||
### Security
|
||||
|
||||
- [Security-related changes or vulnerability fixes]
|
||||
- [List any new or updated security-related changes, including vulnerability fixes]
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- **BREAKING CHANGE**: [Changes affecting compatibility or functionality]
|
||||
- **BREAKING CHANGE**: [List any breaking changes affecting compatibility or functionality]
|
||||
|
||||
---
|
||||
|
||||
### Additional Information
|
||||
|
||||
- [Any additional context, notes, or references to related issues/commits]
|
||||
- [Insert any additional context, notes, or explanations for the changes]
|
||||
- [Reference any related issues, commits, or other relevant information]
|
||||
|
||||
### Screenshots or Videos
|
||||
|
||||
- [Attach relevant screenshots or videos demonstrating the changes]
|
||||
- [Attach any relevant screenshots or videos demonstrating the changes]
|
||||
|
||||
### Contributor License Agreement
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Backend CI — Python formatting checks via Ruff
|
||||
# Runs on pushes and PRs to main/dev when backend files change
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
name: Python CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, dev]
|
||||
paths: ['backend/**', 'pyproject.toml', 'uv.lock', '.github/workflows/backend.yaml']
|
||||
pull_request:
|
||||
branches: [main, dev]
|
||||
paths: ['backend/**', 'pyproject.toml', 'uv.lock', '.github/workflows/backend.yaml']
|
||||
|
||||
concurrency:
|
||||
group: backend-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# ── Ruff format check across supported Python versions ───────────────────
|
||||
format-check:
|
||||
name: Ruff Format (${{ matrix.python-version }})
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ['3.11', '3.12']
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install formatter
|
||||
run: pip install "ruff>=0.15.5"
|
||||
|
||||
- name: Verify formatting
|
||||
run: ruff format --check . --exclude .venv --exclude venv
|
||||
|
||||
- name: Detect logic errors
|
||||
run: ruff check --select=F --ignore=F401,F403,F405,F541,F811,F841 --output-format=github .
|
||||
@@ -0,0 +1,61 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main # or whatever branch you want to use
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Check for changes in package.json
|
||||
run: |
|
||||
git diff --cached --diff-filter=d package.json || {
|
||||
echo "No changes to package.json"
|
||||
exit 1
|
||||
}
|
||||
|
||||
- name: Get version number from package.json
|
||||
id: get_version
|
||||
run: |
|
||||
VERSION=$(jq -r '.version' package.json)
|
||||
echo "::set-output name=version::$VERSION"
|
||||
|
||||
- name: Extract latest CHANGELOG entry
|
||||
run: |
|
||||
VERSION="${{ steps.get_version.outputs.version }}"
|
||||
awk "/^## \[${VERSION}\]/{found=1; next} /^## \[/{if(found) exit} found{print}" CHANGELOG.md > /tmp/release-notes.md
|
||||
|
||||
- name: Create GitHub release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh release create "v${{ steps.get_version.outputs.version }}" \
|
||||
--title "v${{ steps.get_version.outputs.version }}" \
|
||||
--notes-file /tmp/release-notes.md
|
||||
|
||||
- name: Upload package to GitHub release
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: package
|
||||
path: |
|
||||
.
|
||||
!.git
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Trigger Docker build workflow
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
github.rest.actions.createWorkflowDispatch({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: 'docker-build.yaml',
|
||||
ref: 'v${{ steps.get_version.outputs.version }}',
|
||||
})
|
||||
@@ -0,0 +1,917 @@
|
||||
name: Create and publish Docker images with specific build args
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
tags:
|
||||
- v*
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
|
||||
jobs:
|
||||
build-main-image:
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
|
||||
steps:
|
||||
# GitHub Packages requires the entire repository name to be in lowercase
|
||||
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
||||
- name: Set repository and image name to lowercase
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker images (default latest tag)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=sha,prefix=git-
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
flavor: |
|
||||
latest=${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Extract metadata for Docker cache
|
||||
id: cache-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
|
||||
flavor: |
|
||||
prefix=cache-${{ matrix.platform }}-
|
||||
latest=false
|
||||
|
||||
- name: Build Docker image (latest)
|
||||
uses: docker/build-push-action@v5
|
||||
id: build
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
|
||||
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
|
||||
sbom: true
|
||||
build-args: |
|
||||
BUILD_HASH=${{ github.sha }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-main-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
build-cuda-image:
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
|
||||
steps:
|
||||
# GitHub Packages requires the entire repository name to be in lowercase
|
||||
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
||||
- name: Set repository and image name to lowercase
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Delete huge unnecessary tools folder
|
||||
run: rm -rf /opt/hostedtoolcache
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker images (cuda tag)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=sha,prefix=git-
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda
|
||||
flavor: |
|
||||
latest=${{ github.ref == 'refs/heads/main' }}
|
||||
suffix=-cuda,onlatest=true
|
||||
|
||||
- name: Extract metadata for Docker cache
|
||||
id: cache-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
|
||||
flavor: |
|
||||
prefix=cache-cuda-${{ matrix.platform }}-
|
||||
latest=false
|
||||
|
||||
- name: Build Docker image (cuda)
|
||||
uses: docker/build-push-action@v5
|
||||
id: build
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
|
||||
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
|
||||
sbom: true
|
||||
build-args: |
|
||||
BUILD_HASH=${{ github.sha }}
|
||||
USE_CUDA=true
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-cuda-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
build-cuda126-image:
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
|
||||
steps:
|
||||
# GitHub Packages requires the entire repository name to be in lowercase
|
||||
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
||||
- name: Set repository and image name to lowercase
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Delete huge unnecessary tools folder
|
||||
run: rm -rf /opt/hostedtoolcache
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker images (cuda126 tag)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=sha,prefix=git-
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda126
|
||||
flavor: |
|
||||
latest=${{ github.ref == 'refs/heads/main' }}
|
||||
suffix=-cuda126,onlatest=true
|
||||
|
||||
- name: Extract metadata for Docker cache
|
||||
id: cache-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
|
||||
flavor: |
|
||||
prefix=cache-cuda126-${{ matrix.platform }}-
|
||||
latest=false
|
||||
|
||||
- name: Build Docker image (cuda126)
|
||||
uses: docker/build-push-action@v5
|
||||
id: build
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
|
||||
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
|
||||
sbom: true
|
||||
build-args: |
|
||||
BUILD_HASH=${{ github.sha }}
|
||||
USE_CUDA=true
|
||||
USE_CUDA_VER=cu126
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-cuda126-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
build-ollama-image:
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
|
||||
steps:
|
||||
# GitHub Packages requires the entire repository name to be in lowercase
|
||||
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
||||
- name: Set repository and image name to lowercase
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker images (ollama tag)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=sha,prefix=git-
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=ollama
|
||||
flavor: |
|
||||
latest=${{ github.ref == 'refs/heads/main' }}
|
||||
suffix=-ollama,onlatest=true
|
||||
|
||||
- name: Extract metadata for Docker cache
|
||||
id: cache-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
|
||||
flavor: |
|
||||
prefix=cache-ollama-${{ matrix.platform }}-
|
||||
latest=false
|
||||
|
||||
- name: Build Docker image (ollama)
|
||||
uses: docker/build-push-action@v5
|
||||
id: build
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
|
||||
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
|
||||
sbom: true
|
||||
build-args: |
|
||||
BUILD_HASH=${{ github.sha }}
|
||||
USE_OLLAMA=true
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-ollama-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
build-slim-image:
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
|
||||
steps:
|
||||
# GitHub Packages requires the entire repository name to be in lowercase
|
||||
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
||||
- name: Set repository and image name to lowercase
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker images (slim tag)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=sha,prefix=git-
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=slim
|
||||
flavor: |
|
||||
latest=${{ github.ref == 'refs/heads/main' }}
|
||||
suffix=-slim,onlatest=true
|
||||
|
||||
- name: Extract metadata for Docker cache
|
||||
id: cache-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
|
||||
flavor: |
|
||||
prefix=cache-slim-${{ matrix.platform }}-
|
||||
latest=false
|
||||
|
||||
- name: Build Docker image (slim)
|
||||
uses: docker/build-push-action@v5
|
||||
id: build
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
|
||||
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
|
||||
sbom: true
|
||||
build-args: |
|
||||
BUILD_HASH=${{ github.sha }}
|
||||
USE_SLIM=true
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-slim-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge-main-images:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-main-image]
|
||||
steps:
|
||||
# GitHub Packages requires the entire repository name to be in lowercase
|
||||
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
||||
- name: Set repository and image name to lowercase
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: digests-main-*
|
||||
path: /tmp/digests
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker images (default latest tag)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=sha,prefix=git-
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
flavor: |
|
||||
latest=${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
||||
|
||||
merge-cuda-images:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-cuda-image]
|
||||
steps:
|
||||
# GitHub Packages requires the entire repository name to be in lowercase
|
||||
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
||||
- name: Set repository and image name to lowercase
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: digests-cuda-*
|
||||
path: /tmp/digests
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker images (default latest tag)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=sha,prefix=git-
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda
|
||||
flavor: |
|
||||
latest=${{ github.ref == 'refs/heads/main' }}
|
||||
suffix=-cuda,onlatest=true
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
||||
|
||||
merge-cuda126-images:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-cuda126-image]
|
||||
steps:
|
||||
# GitHub Packages requires the entire repository name to be in lowercase
|
||||
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
||||
- name: Set repository and image name to lowercase
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: digests-cuda126-*
|
||||
path: /tmp/digests
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker images (default latest tag)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=sha,prefix=git-
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda126
|
||||
flavor: |
|
||||
latest=${{ github.ref == 'refs/heads/main' }}
|
||||
suffix=-cuda126,onlatest=true
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
||||
|
||||
merge-ollama-images:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-ollama-image]
|
||||
steps:
|
||||
# GitHub Packages requires the entire repository name to be in lowercase
|
||||
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
||||
- name: Set repository and image name to lowercase
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: digests-ollama-*
|
||||
path: /tmp/digests
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker images (default ollama tag)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=sha,prefix=git-
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=ollama
|
||||
flavor: |
|
||||
latest=${{ github.ref == 'refs/heads/main' }}
|
||||
suffix=-ollama,onlatest=true
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
||||
|
||||
merge-slim-images:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-slim-image]
|
||||
steps:
|
||||
# GitHub Packages requires the entire repository name to be in lowercase
|
||||
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
||||
- name: Set repository and image name to lowercase
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: digests-slim-*
|
||||
path: /tmp/digests
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker images (default slim tag)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=sha,prefix=git-
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=slim
|
||||
flavor: |
|
||||
latest=${{ github.ref == 'refs/heads/main' }}
|
||||
suffix=-slim,onlatest=true
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
||||
|
||||
# Copy images from GHCR to Docker Hub (best-effort, won't block GHCR)
|
||||
copy-to-dockerhub:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
|
||||
needs: [merge-main-images, merge-cuda-images, merge-cuda126-images, merge-ollama-images, merge-slim-images]
|
||||
continue-on-error: true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- variant: main
|
||||
suffix: ""
|
||||
- variant: cuda
|
||||
suffix: "-cuda"
|
||||
- variant: cuda126
|
||||
suffix: "-cuda126"
|
||||
- variant: ollama
|
||||
suffix: "-ollama"
|
||||
- variant: slim
|
||||
suffix: "-slim"
|
||||
steps:
|
||||
- name: Set repository and image name to lowercase
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Determine source and destination tags
|
||||
id: tags
|
||||
run: |
|
||||
DOCKERHUB_IMAGE="openwebui/open-webui"
|
||||
SUFFIX="${{ matrix.suffix }}"
|
||||
|
||||
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
|
||||
# For version tags: copy version tag and major.minor tag
|
||||
VERSION="${{ github.ref_name }}"
|
||||
VERSION="${VERSION#v}"
|
||||
MAJOR_MINOR="${VERSION%.*}"
|
||||
|
||||
echo "tags<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "${VERSION}${SUFFIX}" >> $GITHUB_OUTPUT
|
||||
echo "${MAJOR_MINOR}${SUFFIX}" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
else
|
||||
# For main branch
|
||||
if [ -z "$SUFFIX" ]; then
|
||||
echo "tags=latest" >> $GITHUB_OUTPUT
|
||||
else
|
||||
# e.g. latest-cuda -> also tag as just "cuda"
|
||||
VARIANT_NAME="${SUFFIX#-}"
|
||||
echo "tags<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "latest${SUFFIX}" >> $GITHUB_OUTPUT
|
||||
echo "${VARIANT_NAME}" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "dockerhub_image=${DOCKERHUB_IMAGE}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Copy images from GHCR to Docker Hub
|
||||
run: |
|
||||
DOCKERHUB_IMAGE="${{ steps.tags.outputs.dockerhub_image }}"
|
||||
SUFFIX="${{ matrix.suffix }}"
|
||||
|
||||
# Determine the source tag on GHCR
|
||||
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
|
||||
VERSION="${{ github.ref_name }}"
|
||||
VERSION="${VERSION#v}"
|
||||
SOURCE_TAG="${VERSION}${SUFFIX}"
|
||||
else
|
||||
if [ -z "$SUFFIX" ]; then
|
||||
SOURCE_TAG="latest"
|
||||
else
|
||||
SOURCE_TAG="latest${SUFFIX}"
|
||||
fi
|
||||
fi
|
||||
|
||||
SOURCE="${{ env.FULL_IMAGE_NAME }}:${SOURCE_TAG}"
|
||||
|
||||
echo "Copying from ${SOURCE} to Docker Hub..."
|
||||
|
||||
# Copy each destination tag
|
||||
while IFS= read -r TAG; do
|
||||
[ -z "$TAG" ] && continue
|
||||
DEST="${DOCKERHUB_IMAGE}:${TAG}"
|
||||
echo " -> ${DEST}"
|
||||
docker buildx imagetools create -t "${DEST}" "${SOURCE}"
|
||||
done <<< "${{ steps.tags.outputs.tags }}"
|
||||
@@ -1,395 +0,0 @@
|
||||
name: Create and publish Docker images with specific build args
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
tags:
|
||||
- v*
|
||||
|
||||
concurrency:
|
||||
group: docker-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.platform.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
- arch: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- arch: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
variant:
|
||||
- name: main
|
||||
suffix: ""
|
||||
build_args: ""
|
||||
free_disk: false
|
||||
- name: cuda
|
||||
suffix: "-cuda"
|
||||
build_args: "USE_CUDA=true"
|
||||
free_disk: true
|
||||
- name: cuda126
|
||||
suffix: "-cuda126"
|
||||
build_args: |
|
||||
USE_CUDA=true
|
||||
USE_CUDA_VER=cu126
|
||||
free_disk: true
|
||||
- name: ollama
|
||||
suffix: "-ollama"
|
||||
build_args: "USE_OLLAMA=true"
|
||||
free_disk: false
|
||||
- name: slim
|
||||
suffix: "-slim"
|
||||
build_args: "USE_SLIM=true"
|
||||
free_disk: false
|
||||
|
||||
steps:
|
||||
- name: Prepare environment
|
||||
run: |
|
||||
echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >> ${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=${REGISTRY}/${GITHUB_REPOSITORY,,}" >> ${GITHUB_ENV}
|
||||
platform=${{ matrix.platform.arch }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> ${GITHUB_ENV}
|
||||
|
||||
- name: Free disk space
|
||||
if: matrix.variant.free_disk
|
||||
run: rm -rf /opt/hostedtoolcache
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker images
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=sha,prefix=git-
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
${{ matrix.variant.suffix != '' && format('type=raw,enable={0},prefix=,suffix=,value={1}', github.ref == 'refs/heads/main', matrix.variant.name) || '' }}
|
||||
flavor: |
|
||||
latest=${{ github.ref == 'refs/heads/main' }}
|
||||
${{ matrix.variant.suffix != '' && format('suffix={0},onlatest=true', matrix.variant.suffix) || '' }}
|
||||
|
||||
- name: Extract metadata for Docker cache
|
||||
id: cache-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
|
||||
flavor: |
|
||||
prefix=cache-${{ matrix.variant.name }}-${{ matrix.platform.arch }}-
|
||||
latest=false
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
id: build
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: ${{ matrix.platform.arch }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
|
||||
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
|
||||
sbom: true
|
||||
build-args: |
|
||||
BUILD_HASH=${{ github.sha }}
|
||||
${{ matrix.variant.build_args }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-${{ matrix.variant.name }}-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build]
|
||||
if: ${{ !cancelled() }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
variant:
|
||||
- name: main
|
||||
suffix: ""
|
||||
- name: cuda
|
||||
suffix: "-cuda"
|
||||
- name: cuda126
|
||||
suffix: "-cuda126"
|
||||
- name: ollama
|
||||
suffix: "-ollama"
|
||||
- name: slim
|
||||
suffix: "-slim"
|
||||
|
||||
steps:
|
||||
- name: Prepare environment
|
||||
run: |
|
||||
echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >> ${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=${REGISTRY}/${GITHUB_REPOSITORY,,}" >> ${GITHUB_ENV}
|
||||
|
||||
- name: Download digests
|
||||
id: download
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: digests-${{ matrix.variant.name }}-*
|
||||
path: /tmp/digests
|
||||
merge-multiple: true
|
||||
continue-on-error: true
|
||||
|
||||
- name: Check digests
|
||||
id: check
|
||||
run: |
|
||||
count=$(find /tmp/digests -type f 2>/dev/null | wc -l | tr -d ' ')
|
||||
echo "digest_count=$count" >> $GITHUB_OUTPUT
|
||||
if [ "$count" -lt 2 ]; then
|
||||
echo "::warning::${{ matrix.variant.name }}: found $count digest(s), need 2 (one per arch). Skipping merge."
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker images
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=sha,prefix=git-
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
${{ matrix.variant.suffix != '' && format('type=raw,enable={0},prefix=,suffix=,value={1}', github.ref == 'refs/heads/main', matrix.variant.name) || '' }}
|
||||
flavor: |
|
||||
latest=${{ github.ref == 'refs/heads/main' }}
|
||||
${{ matrix.variant.suffix != '' && format('suffix={0},onlatest=true', matrix.variant.suffix) || '' }}
|
||||
|
||||
- name: Create manifest list and push
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
||||
|
||||
notify-helm-charts:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [merge]
|
||||
if: ${{ !cancelled() && needs.merge.result == 'success' && (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/v')) }}
|
||||
steps:
|
||||
- name: Create Helm charts app token
|
||||
id: helm-app-token
|
||||
uses: actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: ${{ secrets.HELM_CHARTS_APP_ID }}
|
||||
private-key: ${{ secrets.HELM_CHARTS_APP_PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
repositories: helm-charts
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Verify published Open WebUI image
|
||||
id: image
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
image_name="ghcr.io/${GITHUB_REPOSITORY,,}"
|
||||
ref_name="${GITHUB_REF_NAME}"
|
||||
|
||||
if [ "${GITHUB_REF}" = "refs/heads/dev" ]; then
|
||||
image_tag="dev"
|
||||
else
|
||||
image_tag="${ref_name#v}"
|
||||
fi
|
||||
|
||||
docker buildx imagetools inspect "${image_name}:${image_tag}"
|
||||
echo "tag=${image_tag}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Dispatch Helm chart automation
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{ steps.helm-app-token.outputs.token }}
|
||||
script: |
|
||||
const isDev = context.ref === 'refs/heads/dev';
|
||||
const eventType = isDev
|
||||
? 'open-webui-dev-image-published'
|
||||
: 'open-webui-release-published';
|
||||
const refName = context.ref.replace('refs/heads/', '').replace('refs/tags/', '');
|
||||
const appVersion = refName.startsWith('v') ? refName.slice(1) : refName;
|
||||
const payload = {
|
||||
image_tag: isDev ? 'dev' : appVersion,
|
||||
source_ref: context.ref,
|
||||
source_sha: context.sha,
|
||||
source_run_id: String(context.runId),
|
||||
source_repository: context.repo.repo,
|
||||
};
|
||||
|
||||
if (!isDev) {
|
||||
payload.app_version = appVersion;
|
||||
}
|
||||
|
||||
await github.rest.repos.createDispatchEvent({
|
||||
owner: context.repo.owner,
|
||||
repo: 'helm-charts',
|
||||
event_type: eventType,
|
||||
client_payload: payload,
|
||||
});
|
||||
|
||||
copy-to-dockerhub:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ !cancelled() && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) }}
|
||||
needs: [merge]
|
||||
continue-on-error: true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- variant: main
|
||||
suffix: ""
|
||||
- variant: cuda
|
||||
suffix: "-cuda"
|
||||
- variant: cuda126
|
||||
suffix: "-cuda126"
|
||||
- variant: ollama
|
||||
suffix: "-ollama"
|
||||
- variant: slim
|
||||
suffix: "-slim"
|
||||
|
||||
steps:
|
||||
- name: Prepare environment
|
||||
run: |
|
||||
echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >> ${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=${REGISTRY}/${GITHUB_REPOSITORY,,}" >> ${GITHUB_ENV}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Determine source and destination tags
|
||||
id: tags
|
||||
run: |
|
||||
DOCKERHUB_IMAGE="openwebui/open-webui"
|
||||
SUFFIX="${{ matrix.suffix }}"
|
||||
|
||||
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
|
||||
VERSION="${{ github.ref_name }}"
|
||||
VERSION="${VERSION#v}"
|
||||
MAJOR_MINOR="${VERSION%.*}"
|
||||
|
||||
echo "tags<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "${VERSION}${SUFFIX}" >> $GITHUB_OUTPUT
|
||||
echo "${MAJOR_MINOR}${SUFFIX}" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
else
|
||||
if [ -z "$SUFFIX" ]; then
|
||||
echo "tags=latest" >> $GITHUB_OUTPUT
|
||||
else
|
||||
VARIANT_NAME="${SUFFIX#-}"
|
||||
echo "tags<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "latest${SUFFIX}" >> $GITHUB_OUTPUT
|
||||
echo "${VARIANT_NAME}" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "dockerhub_image=${DOCKERHUB_IMAGE}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Copy images from GHCR to Docker Hub
|
||||
run: |
|
||||
DOCKERHUB_IMAGE="${{ steps.tags.outputs.dockerhub_image }}"
|
||||
SUFFIX="${{ matrix.suffix }}"
|
||||
|
||||
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
|
||||
VERSION="${{ github.ref_name }}"
|
||||
VERSION="${VERSION#v}"
|
||||
SOURCE_TAG="${VERSION}${SUFFIX}"
|
||||
else
|
||||
if [ -z "$SUFFIX" ]; then
|
||||
SOURCE_TAG="latest"
|
||||
else
|
||||
SOURCE_TAG="latest${SUFFIX}"
|
||||
fi
|
||||
fi
|
||||
|
||||
SOURCE="${{ env.FULL_IMAGE_NAME }}:${SOURCE_TAG}"
|
||||
|
||||
echo "Copying from ${SOURCE} to Docker Hub..."
|
||||
|
||||
while IFS= read -r TAG; do
|
||||
[ -z "$TAG" ] && continue
|
||||
DEST="${DOCKERHUB_IMAGE}:${TAG}"
|
||||
echo " -> ${DEST}"
|
||||
docker buildx imagetools create -t "${DEST}" "${SOURCE}"
|
||||
done <<< "${{ steps.tags.outputs.tags }}"
|
||||
@@ -0,0 +1,46 @@
|
||||
name: Python CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
paths:
|
||||
- 'backend/**'
|
||||
- 'pyproject.toml'
|
||||
- 'uv.lock'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
paths:
|
||||
- 'backend/**'
|
||||
- 'pyproject.toml'
|
||||
- 'uv.lock'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: 'Format Backend'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
python-version:
|
||||
- 3.11.x
|
||||
- 3.12.x
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '${{ matrix.python-version }}'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install "ruff>=0.15.5"
|
||||
|
||||
- name: Ruff format check
|
||||
run: ruff format --check . --exclude .venv --exclude venv
|
||||
@@ -0,0 +1,65 @@
|
||||
name: Frontend Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
paths-ignore:
|
||||
- 'backend/**'
|
||||
- 'pyproject.toml'
|
||||
- 'uv.lock'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
paths-ignore:
|
||||
- 'backend/**'
|
||||
- 'pyproject.toml'
|
||||
- 'uv.lock'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: 'Format & Build Frontend'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm install --force
|
||||
|
||||
- name: Format Frontend
|
||||
run: npm run format
|
||||
|
||||
- name: Run i18next
|
||||
run: npm run i18n:parse
|
||||
|
||||
- name: Check for Changes After Format
|
||||
run: git diff --exit-code
|
||||
|
||||
- name: Build Frontend
|
||||
run: npm run build
|
||||
|
||||
test-frontend:
|
||||
name: 'Frontend Unit Tests'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm ci --force
|
||||
|
||||
- name: Run vitest
|
||||
run: npm run test:frontend
|
||||
@@ -1,63 +0,0 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Frontend CI — Lint, format check, build, and unit tests
|
||||
# Runs on pushes and PRs to main/dev, skipping backend-only changes
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
name: Frontend Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, dev]
|
||||
paths-ignore: ['backend/**', 'pyproject.toml', 'uv.lock']
|
||||
pull_request:
|
||||
branches: [main, dev]
|
||||
paths-ignore: ['backend/**', 'pyproject.toml', 'uv.lock']
|
||||
|
||||
concurrency:
|
||||
group: frontend-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# ── Format, i18n, and production build ────────────────────────────────────
|
||||
format-and-build:
|
||||
name: Format & Build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install --force
|
||||
|
||||
- name: Verify code formatting
|
||||
run: npm run format
|
||||
|
||||
- name: Verify i18n strings
|
||||
run: npm run i18n:parse
|
||||
|
||||
- name: Ensure working tree is clean
|
||||
run: git diff --exit-code
|
||||
|
||||
- name: Production build
|
||||
run: npm run build
|
||||
|
||||
# ── Vitest unit tests ────────────────────────────────────────────────────
|
||||
unit-tests:
|
||||
name: Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Install dependencies (frozen lockfile)
|
||||
run: npm ci --force
|
||||
|
||||
- name: Execute test suite
|
||||
run: npm run test:frontend
|
||||
@@ -0,0 +1,255 @@
|
||||
name: Integration Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
cypress-run:
|
||||
name: Run Cypress Integration Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Maximize build space
|
||||
uses: AdityaGarg8/remove-unwanted-software@v4.1
|
||||
with:
|
||||
remove-android: 'true'
|
||||
remove-haskell: 'true'
|
||||
remove-codeql: 'true'
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build and run Compose Stack
|
||||
run: |
|
||||
docker compose \
|
||||
--file docker-compose.yaml \
|
||||
--file docker-compose.api.yaml \
|
||||
--file docker-compose.a1111-test.yaml \
|
||||
up --detach --build
|
||||
|
||||
- name: Delete Docker build cache
|
||||
run: |
|
||||
docker builder prune --all --force
|
||||
|
||||
- name: Wait for Ollama to be up
|
||||
timeout-minutes: 5
|
||||
run: |
|
||||
until curl --output /dev/null --silent --fail http://localhost:11434; do
|
||||
printf '.'
|
||||
sleep 1
|
||||
done
|
||||
echo "Service is up!"
|
||||
|
||||
- name: Preload Ollama model
|
||||
run: |
|
||||
docker exec ollama ollama pull qwen:0.5b-chat-v1.5-q2_K
|
||||
|
||||
- name: Cypress run
|
||||
uses: cypress-io/github-action@v6
|
||||
env:
|
||||
LIBGL_ALWAYS_SOFTWARE: 1
|
||||
with:
|
||||
browser: chrome
|
||||
wait-on: 'http://localhost:3000'
|
||||
config: baseUrl=http://localhost:3000
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
name: Upload Cypress videos
|
||||
with:
|
||||
name: cypress-videos
|
||||
path: cypress/videos
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: Extract Compose logs
|
||||
if: always()
|
||||
run: |
|
||||
docker compose logs > compose-logs.txt
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
name: Upload Compose logs
|
||||
with:
|
||||
name: compose-logs
|
||||
path: compose-logs.txt
|
||||
if-no-files-found: ignore
|
||||
|
||||
# pytest:
|
||||
# name: Run Backend Tests
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
|
||||
# - name: Set up Python
|
||||
# uses: actions/setup-python@v5
|
||||
# with:
|
||||
# python-version: ${{ matrix.python-version }}
|
||||
|
||||
# - name: Install dependencies
|
||||
# run: |
|
||||
# python -m pip install --upgrade pip
|
||||
# pip install -r backend/requirements.txt
|
||||
|
||||
# - name: pytest run
|
||||
# run: |
|
||||
# ls -al
|
||||
# cd backend
|
||||
# PYTHONPATH=. pytest . -o log_cli=true -o log_cli_level=INFO
|
||||
|
||||
migration_test:
|
||||
name: Run Migration Tests
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
# mysql:
|
||||
# image: mysql
|
||||
# env:
|
||||
# MYSQL_ROOT_PASSWORD: mysql
|
||||
# MYSQL_DATABASE: mysql
|
||||
# options: >-
|
||||
# --health-cmd "mysqladmin ping -h localhost"
|
||||
# --health-interval 10s
|
||||
# --health-timeout 5s
|
||||
# --health-retries 5
|
||||
# ports:
|
||||
# - 3306:3306
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Set up uv
|
||||
uses: yezz123/setup-uv@v4
|
||||
with:
|
||||
uv-venv: venv
|
||||
|
||||
- name: Activate virtualenv
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
echo PATH=$PATH >> $GITHUB_ENV
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
uv pip install -r backend/requirements.txt
|
||||
|
||||
- name: Test backend with SQLite
|
||||
id: sqlite
|
||||
env:
|
||||
WEBUI_SECRET_KEY: secret-key
|
||||
GLOBAL_LOG_LEVEL: debug
|
||||
run: |
|
||||
cd backend
|
||||
uvicorn open_webui.main:app --port "8080" --forwarded-allow-ips '*' &
|
||||
UVICORN_PID=$!
|
||||
# Wait up to 40 seconds for the server to start
|
||||
for i in {1..40}; do
|
||||
curl -s http://localhost:8080/api/config > /dev/null && break
|
||||
sleep 1
|
||||
if [ $i -eq 40 ]; then
|
||||
echo "Server failed to start"
|
||||
kill -9 $UVICORN_PID
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
# Check that the server is still running after 5 seconds
|
||||
sleep 5
|
||||
if ! kill -0 $UVICORN_PID; then
|
||||
echo "Server has stopped"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Test backend with Postgres
|
||||
if: success() || steps.sqlite.conclusion == 'failure'
|
||||
env:
|
||||
WEBUI_SECRET_KEY: secret-key
|
||||
GLOBAL_LOG_LEVEL: debug
|
||||
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
|
||||
DATABASE_POOL_SIZE: 10
|
||||
DATABASE_POOL_MAX_OVERFLOW: 10
|
||||
DATABASE_POOL_TIMEOUT: 30
|
||||
run: |
|
||||
cd backend
|
||||
uvicorn open_webui.main:app --port "8081" --forwarded-allow-ips '*' &
|
||||
UVICORN_PID=$!
|
||||
# Wait up to 20 seconds for the server to start
|
||||
for i in {1..20}; do
|
||||
curl -s http://localhost:8081/api/config > /dev/null && break
|
||||
sleep 1
|
||||
if [ $i -eq 20 ]; then
|
||||
echo "Server failed to start"
|
||||
kill -9 $UVICORN_PID
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
# Check that the server is still running after 5 seconds
|
||||
sleep 5
|
||||
if ! kill -0 $UVICORN_PID; then
|
||||
echo "Server has stopped"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check that service will reconnect to postgres when connection will be closed
|
||||
status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health/db)
|
||||
if [[ "$status_code" -ne 200 ]] ; then
|
||||
echo "Server has failed before postgres reconnect check"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Terminating all connections to postgres..."
|
||||
python -c "import os, psycopg2 as pg2; \
|
||||
conn = pg2.connect(dsn=os.environ['DATABASE_URL'].replace('+pool', '')); \
|
||||
cur = conn.cursor(); \
|
||||
cur.execute('SELECT pg_terminate_backend(psa.pid) FROM pg_stat_activity psa WHERE datname = current_database() AND pid <> pg_backend_pid();')"
|
||||
|
||||
status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health/db)
|
||||
if [[ "$status_code" -ne 200 ]] ; then
|
||||
echo "Server has not reconnected to postgres after connection was closed: returned status $status_code"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# - name: Test backend with MySQL
|
||||
# if: success() || steps.sqlite.conclusion == 'failure' || steps.postgres.conclusion == 'failure'
|
||||
# env:
|
||||
# WEBUI_SECRET_KEY: secret-key
|
||||
# GLOBAL_LOG_LEVEL: debug
|
||||
# DATABASE_URL: mysql://root:mysql@localhost:3306/mysql
|
||||
# run: |
|
||||
# cd backend
|
||||
# uvicorn open_webui.main:app --port "8083" --forwarded-allow-ips '*' &
|
||||
# UVICORN_PID=$!
|
||||
# # Wait up to 20 seconds for the server to start
|
||||
# for i in {1..20}; do
|
||||
# curl -s http://localhost:8083/api/config > /dev/null && break
|
||||
# sleep 1
|
||||
# if [ $i -eq 20 ]; then
|
||||
# echo "Server failed to start"
|
||||
# kill -9 $UVICORN_PID
|
||||
# exit 1
|
||||
# fi
|
||||
# done
|
||||
# # Check that the server is still running after 5 seconds
|
||||
# sleep 5
|
||||
# if ! kill -0 $UVICORN_PID; then
|
||||
# echo "Server has stopped"
|
||||
# exit 1
|
||||
# fi
|
||||
@@ -1,74 +0,0 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Release — Create GitHub release from CHANGELOG, trigger Docker builds
|
||||
# Runs on pushes to main when package.json version changes
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: release-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
# ── Create release and trigger downstream workflows ──────────────────────
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Abort if package.json unchanged
|
||||
run: |
|
||||
git diff --cached --diff-filter=d package.json || {
|
||||
echo "package.json not modified — skipping release"
|
||||
exit 1
|
||||
}
|
||||
|
||||
- name: Read version
|
||||
id: pkg
|
||||
run: echo "version=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Extract release notes from CHANGELOG
|
||||
run: |
|
||||
VER="${{ steps.pkg.outputs.version }}"
|
||||
awk "/^## \[${VER}\]/{found=1; next} /^## \[/{if(found) exit} found{print}" \
|
||||
CHANGELOG.md > /tmp/release-notes.md
|
||||
|
||||
- name: Publish GitHub release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
if gh release view "v${{ steps.pkg.outputs.version }}" &>/dev/null; then
|
||||
echo "Release v${{ steps.pkg.outputs.version }} already exists — skipping creation"
|
||||
else
|
||||
gh release create "v${{ steps.pkg.outputs.version }}" \
|
||||
--title "v${{ steps.pkg.outputs.version }}" \
|
||||
--notes-file /tmp/release-notes.md
|
||||
fi
|
||||
|
||||
- name: Archive source
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-archive
|
||||
path: |
|
||||
.
|
||||
!.git
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Trigger Docker build
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
github.rest.actions.createWorkflowDispatch({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: 'docker.yaml',
|
||||
ref: 'v${{ steps.pkg.outputs.version }}',
|
||||
})
|
||||
@@ -310,4 +310,3 @@ dist
|
||||
cypress/videos
|
||||
cypress/screenshots
|
||||
.vscode/settings.json
|
||||
.cptr
|
||||
|
||||
-535
@@ -5,541 +5,6 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.10.2] - 2026-07-01
|
||||
|
||||
### Added
|
||||
|
||||
- 💭 **Streamed reasoning display.** Models that emit thinking or reasoning now show that content as it streams, and it renders correctly in the chat overview and in exported conversations. [Commit](https://github.com/open-webui/open-webui/commit/0b75445ff9a42e37640c034812d0de9b84039e60), [Commit](https://github.com/open-webui/open-webui/commit/af1c0eee89810fa4c36e3eb7e4eba6de685bd7ca), [Commit](https://github.com/open-webui/open-webui/commit/4b08d65597e5b634b7191b0bd6d28feeafcc2a48), [Commit](https://github.com/open-webui/open-webui/commit/fa2abe4cb6a085a4c9045bf8d8ff4b6beffdef6a)
|
||||
- 🗂️ **Folder uploads to knowledge bases.** Dragging a folder into a knowledge base, or syncing one, now recreates its subfolder structure instead of flattening everything into loose files. [#26130](https://github.com/open-webui/open-webui/issues/26130), [Commit](https://github.com/open-webui/open-webui/commit/2ed8934f5b2bc8a11c74ef2f34cdb62ef024809e)
|
||||
- 🧠 **Memory system context toggle.** Administrators can now keep memory tools available while choosing not to add stored memories to the system context, using the new 'Memory System Context' toggle in admin settings. [Commit](https://github.com/open-webui/open-webui/commit/4067e357b2ff9e2fb59866d24656e832908fb6fe)
|
||||
- 🧹 **Tidier automatic memories.** Automatically saved memories now focus on enduring details like preferences and goals and skip one-off things like meals, routine events, or passing mood unless you ask to remember them. [Commit](https://github.com/open-webui/open-webui/commit/80af65c24adac5140a39a2b5687a3b669b86719f)
|
||||
- 🎙️ **Speech-to-text request format.** OpenAI-compatible speech-to-text can now send audio as either a multipart upload or base64 JSON, selectable in admin audio settings. [Commit](https://github.com/open-webui/open-webui/commit/989c6c13f5d4c5cc255aec77aea14725104d9cb3)
|
||||
- 🧰 **API configs via environment.** Administrators can now set per-connection Ollama and OpenAI API configurations through the "OLLAMA_API_CONFIGS" and "OPENAI_API_CONFIGS" environment variables. [Commit](https://github.com/open-webui/open-webui/commit/19d8f03bd2c64013b510f2a0eeb3513d452814a3)
|
||||
- 📡 **Provider failure events.** Failed Ollama and OpenAI-compatible provider requests now emit a structured event describing the error type, provider, and status, giving administrators clearer visibility into upstream failures. [Commit](https://github.com/open-webui/open-webui/commit/4351c78b1e45bb0c5824f9d9eb911b395e343646)
|
||||
- 🏟️ **Arena models via environment.** Administrators can now define evaluation arena models through the "EVALUATION_ARENA_MODELS" environment variable. [#26174](https://github.com/open-webui/open-webui/issues/26174)
|
||||
- ♿ **Clearer high-contrast sidebar selection.** With high-contrast mode enabled, the currently selected chat in the sidebar now stands out with stronger colors, making it easier to tell which chat is active. [#26469](https://github.com/open-webui/open-webui/issues/26469), [Commit](https://github.com/open-webui/open-webui/commit/52ee5cb1b3aa8a8ad40b23d72d82db7c3522dbf8)
|
||||
- 🔄 **General improvements.** Various improvements were implemented across the application to enhance performance, stability, and security.
|
||||
- 🌐 **Translation updates.** Translations for Thai, Portuguese (Brazil), Catalan, and Spanish were enhanced and expanded.
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🛡️ **Security Advisory**: This release includes security and access-control fixes. We recommend updating production deployments at your earliest convenience. Not all security fixes in this version may be enumerated in the fixed section — some may be withheld for a short time to give administrators time to upgrade. [Advisories](https://github.com/open-webui/open-webui/security)
|
||||
- 🔐 **Fewer unexpected logouts.** A single request returning an authorization error no longer signs you out while your session is still valid, since the app now confirms the session status before redirecting to login. [Commit](https://github.com/open-webui/open-webui/commit/56ee875e21cb3b137a5aa1eca6eb3c0731b14045)
|
||||
- 🔒 **Web search domain filtering.** Domain allow and block rules for web search results are now matched against the host, closing a gap where some URLs could slip past the filter. [Commit](https://github.com/open-webui/open-webui/commit/688bda09fbe26619613aa487beda8059a5fd55ef)
|
||||
- 🕵️ **Image prompt log privacy.** Image generation workflows are no longer written to server logs at the default log level, keeping user-authored prompt content out of operator-visible logs. [#26400](https://github.com/open-webui/open-webui/issues/26400), [Commit](https://github.com/open-webui/open-webui/commit/64b92ff08a09b1bf338d5d95b7bf6f9db0d7e857)
|
||||
- 🗄️ **Safer database upgrades.** Upgrading an existing SQLite database no longer crashes during the user-table migration or corrupts saved user settings, resolving failures that could block startup or break login after an upgrade. [#26403](https://github.com/open-webui/open-webui/issues/26403), [Commit](https://github.com/open-webui/open-webui/commit/c416c6cad69cc4ce44faacdad87cc65862625fb4)
|
||||
- ⚙️ **Saving settings as a non-admin.** Non-admin users can once again save their interface settings, such as the default model and theme, which previously failed with a server error while the interface incorrectly reported success. [#26627](https://github.com/open-webui/open-webui/issues/26627), [Commit](https://github.com/open-webui/open-webui/commit/9866a02863c12c466100aed832f0006225759493)
|
||||
- 🕒 **Sidebar chat timestamps.** Chats in the sidebar now show when they were last active instead of when they were created, so the time label matches their position in the list and refreshes after each new message. [#26454](https://github.com/open-webui/open-webui/pull/26454), [#26451](https://github.com/open-webui/open-webui/issues/26451)
|
||||
- 🎯 **Default model after refresh.** Your selected model is no longer cleared when you reload the page, as model selection now waits for the model list to finish loading. [Commit](https://github.com/open-webui/open-webui/commit/b6d4baeb7ea9bc83e5486ca98b3cae07d3869aa7)
|
||||
- ⏳ **Tool dialogs no longer hang.** Dismissing a tool or function input dialog by clicking outside it now cancels the pending request instead of leaving the chat spinning indefinitely. [#26417](https://github.com/open-webui/open-webui/issues/26417), [Commit](https://github.com/open-webui/open-webui/commit/0016266c0652757e32aa44f2e1fb7311ac08db51)
|
||||
- 🐍 **Reliable code execution loading.** Running Python code in chat now loads its runtime reliably, fixing sandbox startup failures that broke code execution and the Pyodide file viewer in recent releases. [#26625](https://github.com/open-webui/open-webui/pull/26625), [#26390](https://github.com/open-webui/open-webui/issues/26390)
|
||||
- 🤖 **Models with null capabilities.** Chatting with a model whose capabilities are unset no longer fails with an error when the memory feature or automations are involved. [#26412](https://github.com/open-webui/open-webui/issues/26412), [Commit](https://github.com/open-webui/open-webui/commit/650b81792582268345073ce3757ac14356189d75), [Commit](https://github.com/open-webui/open-webui/commit/0016266c0652757e32aa44f2e1fb7311ac08db51)
|
||||
- 🔎 **Searchable responses.** Chat search again finds assistant messages whose text is stored as structured output, which were previously skipped. [#26405](https://github.com/open-webui/open-webui/pull/26405)
|
||||
- 🔔 **Chat notification previews.** Background chat completion notifications and toasts now show a clean response preview instead of appearing blank for messages stored as structured output. [Commit](https://github.com/open-webui/open-webui/commit/c98d8ecaccc7284573b050e88b3857c4aeba3860)
|
||||
- 💾 **Banner and config startup.** Setting configuration such as "WEBUI_BANNERS" no longer causes a startup failure, since admin configuration values are now stored correctly regardless of their data type. [#26431](https://github.com/open-webui/open-webui/issues/26431), [Commit](https://github.com/open-webui/open-webui/commit/ab22fe64bdd10ba86845dcd46dfd7f619b22366f)
|
||||
- 📑 **RAG Template visibility.** The RAG Template editor now stays visible in admin document settings even when Bypass Embedding and Retrieval is enabled, since the template still applies to document content in that mode. [#26126](https://github.com/open-webui/open-webui/issues/26126), [Commit](https://github.com/open-webui/open-webui/commit/8fe480250f649824441a7b4d725a502683134713)
|
||||
- 🧬 **Editing derived models.** Editing a workspace model no longer clears its base model, including when that base is a preset or the model itself. [Commit](https://github.com/open-webui/open-webui/commit/092b5857bbda01be53619e26f42b1fa9b64a2b44), [Commit](https://github.com/open-webui/open-webui/commit/54f31c630afa75e9c81acaa048e1bd901254ba92)
|
||||
|
||||
### Changed
|
||||
|
||||
- ⚠️ **Database Migrations**: This release includes database schema changes; we strongly recommend backing up your database and all associated data before upgrading in production environments. If you are running a multi-worker, multi-server, or load-balanced deployment, all instances must be updated simultaneously, rolling updates are not supported and will cause application failures due to schema incompatibility.
|
||||
|
||||
## [0.10.1] - 2026-06-29
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🤝 **Shared folder read-only chats no longer sign users out.** Opening or reading chats from shared folders now keeps the current session active when a resource-level access error is returned, instead of incorrectly showing "Session expired. Please sign in again."
|
||||
|
||||
## [0.10.0] - 2026-06-29
|
||||
|
||||
### Added
|
||||
|
||||
- 🤝 **Share folders with your team.** You can now share a folder and the chats inside it with specific users, groups, or everyone, with read or write access; people you share with see shared folders in their sidebar and open the chats in a read-only view when they are not the owner, and administrators control who is allowed to share folders with a new "Folders Sharing" permission that is off by default. [Commit](https://github.com/open-webui/open-webui/commit/5019af79a0c45743ede8c9ff37d68f768e7f6174), [Commit](https://github.com/open-webui/open-webui/commit/38920c0ed1f6ad5fe3bb9d12898fa968ead3634a), [Commit](https://github.com/open-webui/open-webui/commit/d65ac445a43348c5f0323d54c37397ae7f483cb8), [Commit](https://github.com/open-webui/open-webui/commit/c783fd30f20d6be5028cf337bc5e5c2f9afbd3f8), [Commit](https://github.com/open-webui/open-webui/commit/45fcf272ef51c84cb01c1454589da3f98e4adc2c), [Commit](https://github.com/open-webui/open-webui/commit/76854d14246660af8222a5302513020e1f36c4f3), [Commit](https://github.com/open-webui/open-webui/commit/084d040e220ee39f62757d928d839646e813fb25), [Commit](https://github.com/open-webui/open-webui/commit/10558173fb155c63403aa8d80f16f8a3ccfa72a6)
|
||||
- 🗜️ **Automatic context compaction for long chats.** Conversations that grow past a configurable token threshold can now be summarized automatically so they stay within a model's context window, with a notification shown while it happens; administrators can enable it, set the threshold, customize the summarization prompt, and lower the threshold per model. It is off by default. [Commit](https://github.com/open-webui/open-webui/commit/3f0c0e0a0ddff841b015f96f9649c6999a435c73), [Commit](https://github.com/open-webui/open-webui/commit/7f08376f0c06e1a7fba983a2fa93deb8dfbe7cb0), [Commit](https://github.com/open-webui/open-webui/commit/8934bfb04bf366aece872028944e280c25e36d3e), [#19594](https://github.com/open-webui/open-webui/issues/19594)
|
||||
- 🖥️ **Open WebUI Computer agent support.** Open WebUI can now connect to Open WebUI Computer through its OpenAI-compatible gateway, letting chats run full agent sessions on your own machine with file, terminal, git, and web access. [GitHub](https://github.com/open-webui/computer)
|
||||
- 🚀 **Much faster hybrid search on large knowledge bases.** Hybrid search now runs natively in the database on pgvector setups instead of loading an entire collection into memory, so querying large knowledge bases is dramatically faster. [Commit](https://github.com/open-webui/open-webui/commit/223f484ded01d092979693341dc03351a9fa17fa), [#20737](https://github.com/open-webui/open-webui/discussions/20737)
|
||||
- 🗂️ **External knowledge bases.** Knowledge bases can now be backed by an external retrieval source through configurable external knowledge connections, so you can search an existing external system from chat instead of only Open WebUI's built-in store. [Commit](https://github.com/open-webui/open-webui/commit/15c7e374384488effc3d6059d09b3a8aa79c618d)
|
||||
- 🧠 **Reworked memory system.** Memory has been overhauled with distinct memory types — long-lived personal memories and per-conversation context — managed through a structured add, update, and delete flow, giving models a more reliable way to remember and apply what they've learned about you. [Commit](https://github.com/open-webui/open-webui/commit/dbdcfd8c6080c284024052a482e589de226dbf05), [Commit](https://github.com/open-webui/open-webui/commit/7e13fd7ad19c28ba34502bde6c709665f7a6808c), [Commit](https://github.com/open-webui/open-webui/commit/2560533c1a8a2b2a0f7b47becf3703031053ad30), [Commit](https://github.com/open-webui/open-webui/commit/8977a10a2b1e150393635dc5c24e59660d0f2da9), [Commit](https://github.com/open-webui/open-webui/commit/260f3c3a22c55f15ca8f06c5314b23f2a9eb1739), [Commit](https://github.com/open-webui/open-webui/commit/b0487dd6dd942a757828a25aba92e9feef685275), [Commit](https://github.com/open-webui/open-webui/commit/a285a390c12e27e614d1ba9ffb92d1a0e49e7dfa), [Commit](https://github.com/open-webui/open-webui/commit/70e4ffcc6526c1bc90dbfdc0287283574b12c18b), [Commit](https://github.com/open-webui/open-webui/commit/2c4e1fce8f40b0cb5028f1afcb184b6e58c33041), [Commit](https://github.com/open-webui/open-webui/commit/c7e634776d7e77556d149b6cc884ed64363648a9)
|
||||
- 🧩 **New plugin primitive: the Event function.** Where pipe, filter, and action functions all run inside a conversation, the new Event function is the first primitive that hooks into the system itself: it runs your own Python in response to events emitted across the whole application — sign-ups, configuration changes, file uploads, role changes, deletions, startup and shutdown, and more. That makes a new class of behavior possible directly inside Open WebUI, from onboarding and access control to auditing, lifecycle automation, and external integrations. Comes with starter boilerplate in the function editor. [Commit](https://github.com/open-webui/open-webui/commit/e124c2656a4c2092b070e570e35b2f0fb7f584de), [Docs](https://docs.openwebui.com/features/extensibility/plugin/functions/event)
|
||||
- 🔔 **New event system with webhooks.** Open WebUI now emits events for a wide range of system activity — sign-ins, configuration changes, startup, and actions across chats, knowledge, files, and more. Administrators can send these as outbound webhooks, route them to specific users or groups, and manage which events go where from a new event settings admin page. [Commit](https://github.com/open-webui/open-webui/commit/b5c43968db0ea1556b228d143ae5946dc4e944ba), [Commit](https://github.com/open-webui/open-webui/commit/745396867888718289a2dfcf0809b3e162e00629), [Commit](https://github.com/open-webui/open-webui/commit/5576e6ed8a80a4032b7c6cb3ee0cda0254019355), [Commit](https://github.com/open-webui/open-webui/commit/7b55a63fc7ee323e9114713ce1d2f3f688aa37e6), [Commit](https://github.com/open-webui/open-webui/commit/1a8e1a993928a28b9d814c77b2aef0361b630f27), [Commit](https://github.com/open-webui/open-webui/commit/ede39d82de05eeb7591329679c8cab97753e5ff0), [Commit](https://github.com/open-webui/open-webui/commit/8f890f0b43aed3d42b9e3d954e25e54e37d526d0), [Commit](https://github.com/open-webui/open-webui/commit/741b64edb6c2ff04c4b787528cfca1c5b66a1a27), [Commit](https://github.com/open-webui/open-webui/commit/303c426c3fffafc2369205021a82659ee8715a85), [#1240](https://github.com/open-webui/open-webui/issues/1240), [#16426](https://github.com/open-webui/open-webui/pull/16426)
|
||||
- 🔐 **Configure authentication from the admin panel.** LDAP and OAuth/OIDC settings now have a dedicated Authentication settings page, so providers can be configured from the admin interface. [Commit](https://github.com/open-webui/open-webui/commit/5cdcdbaeec9fc8156721c38c33ec37956962871c), [#12945](https://github.com/open-webui/open-webui/pull/12945)
|
||||
- 🏷️ **More custom header variables.** Custom request headers now support "{{USER_MESSAGE_ID}}", "{{USER_MESSAGE_PARENT_ID}}", and "{{TASK}}", letting connected services tell apart real user messages from automated background requests like title, tag, and follow-up generation. [Commit](https://github.com/open-webui/open-webui/commit/f85cb27ef835aa76aff7de6176bf2159ba392061)
|
||||
- 📄 **File details forwarded to external document extractors.** External custom document-extraction servers now receive the file's ID, name, and content type, and these are also available as custom header variables, so extraction can be tailored per file. [Commit](https://github.com/open-webui/open-webui/commit/b1c2536ed2f8639efade04618018e6de9b332df2), [#26259](https://github.com/open-webui/open-webui/issues/26259)
|
||||
- 🎰 **Last model pre-selected for new slots.** When you add another model to a multi-model chat, the slot now defaults to the model you last picked instead of starting empty. [#25974](https://github.com/open-webui/open-webui/pull/25974)
|
||||
- ⚡ **Faster model overview.** The admin model overview now loads its feedback history and tags through batched queries, so it opens noticeably faster on instances with many chats. [Commit](https://github.com/open-webui/open-webui/commit/40c09167cd6de1c853a5dd03c88b4fdcb279dfe1)
|
||||
- 🏎️ **Lighter channel profile previews.** Profile previews in channels now load a person's details only when you hover to open one, rather than fetching them for every message up front. [Commit](https://github.com/open-webui/open-webui/commit/4f69c33de0e9a8fde4f16d0b2f1ed8aac8741772)
|
||||
- ↩️ **Reset permissions to defaults.** The group and default permission dialogs now include a button to restore all permissions back to their built-in defaults in one step. [#25931](https://github.com/open-webui/open-webui/pull/25931)
|
||||
- 📥 **Chat import permission.** Administrators can now control whether users are allowed to import or clone chats, with a new "Allow Chat Import" permission. [Commit](https://github.com/open-webui/open-webui/commit/edf3ae920989b01383be543e7379d3eade03c0b6), [Commit](https://github.com/open-webui/open-webui/commit/9ccda6715c3b2dc2cbc1302d2396b2f0233bdea8), [Commit](https://github.com/open-webui/open-webui/commit/ed4cb358a06fc6962b378f20ef846fd2b0af90bc), [#25927](https://github.com/open-webui/open-webui/pull/25927)
|
||||
- 🔔 **Per-group user webhook permission.** Administrators can now control which users may set a personal notification webhook, with a new "User Webhooks" permission. [#25923](https://github.com/open-webui/open-webui/pull/25923)
|
||||
- ✍️ **Customizable autocomplete prompt.** Administrators can now set a custom prompt template for autocomplete generation from the admin interface settings. [Commit](https://github.com/open-webui/open-webui/commit/4dbb2f94a66d6e0035e2da857ddb6a841a68f862), [#25879](https://github.com/open-webui/open-webui/pull/25879)
|
||||
- 🔑 **Configurable secret key length.** The auto-generated secret key length can now be set with a new environment variable, instead of always using a fixed length. [Commit](https://github.com/open-webui/open-webui/commit/e473ab1231abedcb188c259b42ae7f2390739223), [#25906](https://github.com/open-webui/open-webui/pull/25906)
|
||||
- 🏟️ **Arena evaluation models configurable via environment.** Arena evaluation models can now be defined through an environment variable, which previously could not be set that way. [Commit](https://github.com/open-webui/open-webui/commit/fd56086e793a0eceb07a55ff972a5492d8f8a285)
|
||||
- ✏️ **Edit prompts from the menu.** The prompts list now has an Edit option in each prompt's menu, taking you straight to its editor. [#25789](https://github.com/open-webui/open-webui/pull/25789)
|
||||
- 📋 **Clone automations.** Automations now have a Clone option in their menu, so you can duplicate one as a starting point. [#25790](https://github.com/open-webui/open-webui/pull/25790)
|
||||
- 🔁 **Recurring calendar events.** The calendar event editor now includes a repeat option, so events can recur on a schedule. [#25865](https://github.com/open-webui/open-webui/pull/25865)
|
||||
- 🧷 **Separate skills import and export permissions.** Administrators can now control importing and exporting skills independently, with new skills import and export permissions. [#25921](https://github.com/open-webui/open-webui/pull/25921)
|
||||
- 🏷️ **Filter admin models by tag.** The admin Models settings page now has a tag filter for narrowing the model list by base-model tags. [Commit](https://github.com/open-webui/open-webui/commit/2bdd2ab94eefd3d75dd6511c445e302f82221b5d)
|
||||
- 📊 **Sortable analytics chat list.** The model chat list in analytics now has sortable column headers, so you can order it by title, last updated, or user. [Commit](https://github.com/open-webui/open-webui/commit/3730a9eaac68dff60b3ae5b4ed160b91480d66eb), [#26168](https://github.com/open-webui/open-webui/pull/26168)
|
||||
- 🔐 **Argon2 password hashing option.** Password hashing can now use Argon2 through a configurable algorithm setting, removing the 72-byte password length limit that came with the previous default. [Commit](https://github.com/open-webui/open-webui/commit/33cd199e6dffddd4ee8974af41ebb894871d74c1), [Commit](https://github.com/open-webui/open-webui/commit/a70a6589afad0b429cdd77afa62163391f406a87), [#25656](https://github.com/open-webui/open-webui/pull/25656)
|
||||
- 🔐 **Optional encryption of valve values at rest.** Tool and function valve values can now be encrypted at rest through a new opt-in setting, with existing stored values migrated automatically, so sensitive settings like API keys aren't kept in plaintext. [Commit](https://github.com/open-webui/open-webui/commit/b4073f6378392b23a3954e33031bf5e1d98e090a), [#23721](https://github.com/open-webui/open-webui/pull/23721)
|
||||
- 🗄️ **AWS RDS IAM database authentication.** The database connection can now authenticate using AWS RDS IAM tokens through a new opt-in setting, instead of only a static password. [Commit](https://github.com/open-webui/open-webui/commit/c0c6c2181a8dc57b62e8a5eabd550bf89db7ffed), [#23580](https://github.com/open-webui/open-webui/pull/23580)
|
||||
- 🔓 **Automatic auth for models with OAuth 2.1 tools.** When a model uses tools that require OAuth 2.1, Open WebUI now initiates the authorization flow automatically instead of failing the request. [Commit](https://github.com/open-webui/open-webui/commit/ae5d23f2267845922c2acb507a4a432908d03b41), [#23325](https://github.com/open-webui/open-webui/pull/23325), [#23272](https://github.com/open-webui/open-webui/issues/23272)
|
||||
- 🔤 **Custom tokenizer for token-based text splitting.** Token-based document splitting can now use a configurable Hugging Face tokenizer model, so chunking can match the tokenizer of the model you use. [Commit](https://github.com/open-webui/open-webui/commit/bb6b2db88b1e82395531f67db4f6accd49d8b9eb), [#24139](https://github.com/open-webui/open-webui/pull/24139)
|
||||
- 🔒 **Restrict OAuth scopes requested from MCP servers.** A new setting lets administrators limit which OAuth scopes Open WebUI requests when connecting to MCP servers. [Commit](https://github.com/open-webui/open-webui/commit/7be009649a0a94008484335c492ab0af18fde41f), [#25981](https://github.com/open-webui/open-webui/pull/25981), [#25978](https://github.com/open-webui/open-webui/issues/25978)
|
||||
- 🧩 **Filter Outlet Hook can now run on API requests and responses.** A filter function's outlet hook now runs for direct API callers, including streaming responses, so response post-processing isn't limited to the web interface; this is controlled by a new setting and on by default. [Commit](https://github.com/open-webui/open-webui/commit/390e200f76877b185002c88b4dda27b123d29e83), [#25650](https://github.com/open-webui/open-webui/pull/25650)
|
||||
- 🖥️ **Setting for terminal sidebar auto-open.** A new interface setting controls whether the files sidebar opens automatically when you select a terminal. [Commit](https://github.com/open-webui/open-webui/commit/958237473f8cbde97eb0df8c21f2a4de088c4459), [#25628](https://github.com/open-webui/open-webui/pull/25628)
|
||||
- 📌 **Reorder pinned notes by dragging.** Pinned notes in the sidebar can now be dragged to reorder them. [#25677](https://github.com/open-webui/open-webui/pull/25677)
|
||||
- 🔎 **Chat actions in search.** The search dialog now offers a context menu on each result, so you can act on a chat directly from search. [#25490](https://github.com/open-webui/open-webui/pull/25490)
|
||||
- 🔎 **Snippets in chat search results.** Searching your chats now shows a snippet of the matching content in each result, so you can tell results apart at a glance. [Commit](https://github.com/open-webui/open-webui/commit/0eba3df1199f56e8ac77213772a41313a3237296), [Commit](https://github.com/open-webui/open-webui/commit/67a7b23b85d2e3ce6b094b682ba9f07dc453d355), [Commit](https://github.com/open-webui/open-webui/commit/8927c9bb3d4b04f4fc8e443f42089f2a551276bc), [#25178](https://github.com/open-webui/open-webui/pull/25178)
|
||||
- 📝 **Formatted valve descriptions.** Valve descriptions for tools and functions now render Markdown, so they can include formatting and links. [Commit](https://github.com/open-webui/open-webui/commit/7c0b0e42f5afb0e9c39bd2d42e4822d64d0c7b3e)
|
||||
- 🔽 **Dropdown inputs for valve options.** Valve and confirmation inputs can now present a set of options as a dropdown instead of free text, making fixed-choice settings easier to configure. [Commit](https://github.com/open-webui/open-webui/commit/422a4768ea7428b5dd6d401ccfba00e1e86eb98a), [#26278](https://github.com/open-webui/open-webui/pull/26278)
|
||||
- 🔌 **Control the OAuth resource parameter for MCP connectors.** MCP connectors can now be set to always send, never send, or automatically decide whether to include the OAuth resource parameter, so they work with providers that reject it. [Commit](https://github.com/open-webui/open-webui/commit/5576e6ed8a80a4032b7c6cb3ee0cda0254019355)
|
||||
- 🔎 **SERPHouse web search.** SERPHouse can now be used as a web search provider. [Commit](https://github.com/open-webui/open-webui/commit/3a232f5e9a4d31a6b74cb34d007b581feeb2f005), [Commit](https://github.com/open-webui/open-webui/commit/dd4f43bfdb793c4276e6468b65fc32d9b881578a), [#26254](https://github.com/open-webui/open-webui/pull/26254)
|
||||
- 🔎 **Microsoft Web IQ web search.** Microsoft Web IQ can now be used as a web search provider, with a matching page-browse loader. [#26178](https://github.com/open-webui/open-webui/pull/26178)
|
||||
- ⚠️ **Optional web search confirmation.** Administrators can now require users to confirm before a web search runs, with a banner and message making it clear when search is about to be used. [Commit](https://github.com/open-webui/open-webui/commit/fa76764c3b7f99c5adacd34dabd51ead09542c13), [#24942](https://github.com/open-webui/open-webui/pull/24942)
|
||||
- 🪪 **Client User-Agent forwarded to model backends.** The browser's User-Agent is now passed through to all model backends, so upstream services can see the originating client. [#26333](https://github.com/open-webui/open-webui/pull/26333)
|
||||
- 🖐️ **Drag items from the sidebar into chat.** Folders, notes, and models — including pinned notes — can now be dragged from the sidebar into the chat input. [#25771](https://github.com/open-webui/open-webui/pull/25771), [Commit](https://github.com/open-webui/open-webui/commit/dc1bc41d2e), [#26384](https://github.com/open-webui/open-webui/pull/26384)
|
||||
- 🏷️ **Tag suggestions in the model editor.** The model editor now suggests existing tags as you type, making it easier to reuse a consistent set. [Commit](https://github.com/open-webui/open-webui/commit/b58b0ea7ca849b89d217e1077498a8a3fc92471f), [#25703](https://github.com/open-webui/open-webui/pull/25703)
|
||||
- 🗣️ **Voice suggestions in the model editor.** The model editor now offers a dropdown of available text-to-speech voices, making it easier to pick one. [Commit](https://github.com/open-webui/open-webui/commit/a5c945940134b957dbad47790b5baafeecdac6c4), [#25706](https://github.com/open-webui/open-webui/pull/25706)
|
||||
- 🎛️ **Unified model picker for workspace base model.** Choosing a base model in the model editor now uses the searchable model selector instead of a plain field, making it easier to find and pick the right model. [Commit](https://github.com/open-webui/open-webui/commit/c89fd237b822877bffbb33a37402622983c7189d), [#24576](https://github.com/open-webui/open-webui/issues/24576)
|
||||
- 🔍 **Searchable pickers in the model editor.** Attaching actions, filters, tools, knowledge, and skills to a model now uses type-to-search pickers instead of long checkbox lists, making large libraries easier to manage. [Commit](https://github.com/open-webui/open-webui/commit/61cee42ded4e84e31d7cc9b168994ef165efa8ca)
|
||||
- 🖼️ **iPhone images work with OpenAI image editing.** Uploaded images are now normalized before being sent to OpenAI image editing, fixing edits that failed for certain iPhone photo formats, with a new admin toggle to control the behavior. [Commit](https://github.com/open-webui/open-webui/commit/39837e0a3afd17b7ff617d97dddf4c5d6446e42d), [Commit](https://github.com/open-webui/open-webui/commit/2d3035a1122123df471ac9d0a591a9465a8212e2), [#26252](https://github.com/open-webui/open-webui/pull/26252), [#26249](https://github.com/open-webui/open-webui/issues/26249)
|
||||
- 🟢 **Loaded-model indicator for llama.cpp.** Models served through llama.cpp now report whether they're currently loaded in memory, including the sleeping state, so the loaded indicator works for them too. [Commit](https://github.com/open-webui/open-webui/commit/b696c5deff15d4c85c84c5bac244f062b1bc879a)
|
||||
- 🧱 **Structured model output rendered on the client.** Reasoning, tool calls, and server-side tool steps such as web and file search are now rendered in the browser from the model's structured output instead of being flattened into the message text on the server, giving more accurate and editable rendering of these items. [Commit](https://github.com/open-webui/open-webui/commit/0443ab3a61492799f1aaa449f89cbd8aa5912f57), [Commit](https://github.com/open-webui/open-webui/commit/c33fadc26671190c94d86485e6e2ef2f6fd486a3)
|
||||
- 📜 **Custom CA bundle for outbound connections.** A new environment variable lets you point Open WebUI at a custom CA certificate bundle, and the per-connection SSL settings now accept a bundle path, so deployments behind a corporate or internal CA can keep certificate verification on instead of disabling it. [Commit](https://github.com/open-webui/open-webui/commit/a54878b14f044d4aa1d8cf5be6f8ce9fc4285438), [Commit](https://github.com/open-webui/open-webui/commit/8b9e28b50354307a314111262f6737b6d8aa4685)
|
||||
- 🖥️ **More terminal server orchestrator controls.** Admins connecting an orchestrator terminal server can now configure session lifecycle policies and refresh or reset running terminal sessions, including targeting only idle ones, from the connection settings. [Commit](https://github.com/open-webui/open-webui/commit/7e8153e889a59afe4cf77261ea5e1ef5a66665f1)
|
||||
- 📁 **Terminal file browser can stay within a root folder.** The terminal file navigator now anchors to a defined root and home directory, so users can be kept within their workspace instead of browsing into system folders by accident. [Commit](https://github.com/open-webui/open-webui/commit/a0c2ec3d2cf8d696ede479211330eec2da360d39)
|
||||
- 🧠 **Memory toggle follows the server default.** When a user hasn't set their own memory preference, it now follows the admin's global memory setting instead of defaulting to off. [#25909](https://github.com/open-webui/open-webui/pull/25909)
|
||||
- 🧹 **Unshare all shared chats at once.** The Shared Chats dialog now has a button to stop sharing every shared chat in one action. [#25848](https://github.com/open-webui/open-webui/pull/25848)
|
||||
- 📈 **Richer analytics with a date picker.** The analytics dashboard now lets you choose a date range and shows additional columns. [#25922](https://github.com/open-webui/open-webui/pull/25922), [#25919](https://github.com/open-webui/open-webui/issues/25919)
|
||||
- 🔢 **Chat and file counts in their dialogs.** The Chats and Files dialogs now show the total number of chats and files in their titles. [#25872](https://github.com/open-webui/open-webui/pull/25872), [#25873](https://github.com/open-webui/open-webui/pull/25873)
|
||||
- ⚡ **Faster math rendering.** Rendered math is now cached and reused, so messages with repeated or unchanged math expressions render more efficiently. [#25847](https://github.com/open-webui/open-webui/pull/25847)
|
||||
- ⚡ **Lighter Markdown setup.** Markdown extension setup now runs once instead of on every render, avoiding repeated work and extension stacking. [#25837](https://github.com/open-webui/open-webui/pull/25837)
|
||||
- ⚡ **Snappier read-only code blocks.** Read-only code blocks now skip language auto-detection, so they render faster. [#25824](https://github.com/open-webui/open-webui/pull/25824)
|
||||
- ⚡ **Non-blocking audio model loading.** Loading speech models no longer blocks the server, keeping it responsive while they initialize. [#25806](https://github.com/open-webui/open-webui/pull/25806)
|
||||
- ⚡ **Faster URL safety checks.** The safety check on fetched URLs now resolves addresses off the main loop, so it no longer blocks other work. [#25825](https://github.com/open-webui/open-webui/pull/25825)
|
||||
- ⚡ **Fewer queries for channel reactions and replies.** Channel reactions and thread replies now load through batched queries, reducing database load on busy channels. [#25831](https://github.com/open-webui/open-webui/pull/25831)
|
||||
- ⚡ **Lighter streaming.** Streaming responses now skip re-processing message content that hasn't changed, reducing work on every update. [#26325](https://github.com/open-webui/open-webui/pull/26325), [#26326](https://github.com/open-webui/open-webui/pull/26326)
|
||||
- ⚡ **Smoother tool-call rendering.** Displaying tool calls now parses their content iteratively, avoiding slowdowns on deeply nested data. [#26146](https://github.com/open-webui/open-webui/pull/26146)
|
||||
- ⚡ **Hidden tool-call details cost nothing.** When tool-call arguments are collapsed, they are no longer rendered behind the scenes, noticeably speeding up chats with heavy tool use. [Commit](https://github.com/open-webui/open-webui/commit/b7934e918223ec0a9e972accd647a8654e503156), [#26147](https://github.com/open-webui/open-webui/pull/26147)
|
||||
- ⚡ **Leaner knowledge-file reading for agents.** The built-in tools that let a model read knowledge files now return output in bounded, paginated chunks with a default and a hard cap, instead of potentially returning an entire large file at once, sharply reducing token usage. [Commit](https://github.com/open-webui/open-webui/commit/a285a390c12e27e614d1ba9ffb92d1a0e49e7dfa), [#26139](https://github.com/open-webui/open-webui/issues/26139)
|
||||
- ⚡ **Lighter, faster file search on large knowledge bases.** Listing and searching files no longer returns each file's full extracted text by default, and content matching is now length-bounded, so these requests are far lighter and searching across very large knowledge bases is dramatically faster. [Commit](https://github.com/open-webui/open-webui/commit/36d08fa2a7), [Commit](https://github.com/open-webui/open-webui/commit/46c1d6591badb6ab567ba1b8fae23475d5da105a), [Commit](https://github.com/open-webui/open-webui/commit/ab84bbf08c5935f1a19044ef581986f83311da8b), [#25774](https://github.com/open-webui/open-webui/pull/25774), [#25741](https://github.com/open-webui/open-webui/issues/25741), [#26145](https://github.com/open-webui/open-webui/pull/26145), [#25867](https://github.com/open-webui/open-webui/issues/25867)
|
||||
- ⚡ **Faster password hashing and bulk user import.** Password hashing and verification no longer block the server, and importing users from a CSV is now processed in a single batch, keeping large imports and sign-ins responsive. [Commit](https://github.com/open-webui/open-webui/commit/6fdf9b4340), [#25804](https://github.com/open-webui/open-webui/pull/25804), [#25805](https://github.com/open-webui/open-webui/pull/25805)
|
||||
- ⚡ **Non-blocking model downloads.** Downloading large Ollama models no longer blocks the server on file reads and checksums, keeping it responsive during big downloads. [#25829](https://github.com/open-webui/open-webui/pull/25829)
|
||||
- ⚡ **Non-blocking uploads and link fetches.** Hashing uploaded files and fetching URLs now run off the main loop, so large uploads and link previews don't hold up other requests. [#25822](https://github.com/open-webui/open-webui/pull/25822)
|
||||
- ⚡ **More blocking work moved off the main loop.** Additional blocking operations in audio, pipelines, and plugin handling now run in worker threads, keeping the server responsive under load. [#26381](https://github.com/open-webui/open-webui/pull/26381)
|
||||
- ⚡ **Unreachable backends don't stall model loading.** Loading models and tool servers no longer blocks on backends that are down or slow to respond, so the model list stays responsive when one connection is unreachable. [#26289](https://github.com/open-webui/open-webui/pull/26289)
|
||||
- ⚡ **Batched streaming updates.** Streaming responses now group small updates of the same type before sending them, reducing overhead during fast token streams and tool-call output. [Commit](https://github.com/open-webui/open-webui/commit/7240517807a8b0097065f7cbbb384d34084f90fd), [#26202](https://github.com/open-webui/open-webui/pull/26202)
|
||||
- 🔄 **General improvements.** Various improvements were implemented across the application to enhance performance, stability, and security.
|
||||
- 🌐 **Updated translations.** Catalan, Brazilian Portuguese (pt-BR), Irish, German (de-DE), and Spanish (es-ES) translations were updated.
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🛡️ **Security Advisory**: This release includes security and access-control fixes. We recommend updating production deployments at your earliest convenience. Not all security fixes in this version may be enumerated in the fixed section — some may be withheld for a short time to give administrators time to upgrade. [Advisories](https://github.com/open-webui/open-webui/security)
|
||||
- 🔐 **Knowledge base write access enforced on upload.** Attaching an uploaded file to a knowledge base now requires the same write access as the rest of the knowledge API, so users without write access can no longer add files to a collection by referencing its ID. [#26001](https://github.com/open-webui/open-webui/pull/26001)
|
||||
- 🗝️ **API key permission enforced on all key endpoints.** Viewing and deleting API keys now respects the API keys permission, matching the protection already applied to key creation. [#25992](https://github.com/open-webui/open-webui/pull/25992)
|
||||
- 🔊 **Text-to-speech permission enforced on the speech endpoint.** The OpenAI speech proxy now honors the text-to-speech permission, so it can no longer be used by people who are not allowed to use that feature. [#25993](https://github.com/open-webui/open-webui/pull/25993)
|
||||
- 🎲 **Model access enforced on arena fallback.** Reaching a model indirectly through an arena model on background and task requests now enforces that model's access rules, closing a path that could otherwise bypass them. [#26046](https://github.com/open-webui/open-webui/pull/26046)
|
||||
- ⏰ **Scheduled automations stop for deactivated accounts.** Scheduled automations now re-check the owner's account status and permissions before each run, so they stop when an account is deactivated or has automations access revoked. [#26047](https://github.com/open-webui/open-webui/pull/26047)
|
||||
- 🚧 **Heavily encoded paths rejected behind the proxy.** Request paths that remain encoded after repeated decoding are now rejected instead of forwarded, preventing a path traversal that could otherwise slip through. [#26050](https://github.com/open-webui/open-webui/pull/26050)
|
||||
- 🌐 **Image URL fetches hardened against DNS rebinding.** Fetching user-supplied image URLs now re-checks the destination address at connection time, closing a path that could be used to reach internal addresses behind a public hostname. [#25960](https://github.com/open-webui/open-webui/pull/25960)
|
||||
- 🛂 **Web fetch blocklist matches on hostname.** The web fetch filter now matches entries against the request's hostname on domain boundaries, so blocked hosts can no longer slip through with an added path and lookalike domains are no longer mistaken for allowed ones. [#25949](https://github.com/open-webui/open-webui/pull/25949)
|
||||
- 🪪 **MCP connectors request least-privilege scopes.** MCP connectors that register dynamically over OAuth now request only the scopes for the specific resource rather than the authorization server's full catalog. [#25958](https://github.com/open-webui/open-webui/pull/25958)
|
||||
- 🙈 **Channel member lists no longer expose private data.** Viewing a channel's members now returns only basic profile details, instead of also exposing other members' settings, linked-account data, and personal information. [Commit](https://github.com/open-webui/open-webui/commit/fbcdcf146b99b5002705060a8243eee769108f9e)
|
||||
- 🛟 **SCIM sync can't demote an admin.** A SCIM provisioning sync that marks a user inactive can no longer strip an existing administrator's role, preventing an instance from being locked out of its own administration. [#25948](https://github.com/open-webui/open-webui/pull/25948)
|
||||
- 👻 **Collaborative notes reject unauthenticated presence events.** The remaining real-time note-collaboration events now require an authenticated session, so presence and cursors can no longer be spoofed by someone who only knows a note's ID. [#25946](https://github.com/open-webui/open-webui/pull/25946)
|
||||
- ⏱️ **Login timing no longer reveals which accounts exist.** Sign-in now takes the same amount of time whether or not an account exists, removing a timing difference that could be used to discover valid accounts. [Commit](https://github.com/open-webui/open-webui/commit/993e74912199c66c522f08ec81abe31d76985e39), [Commit](https://github.com/open-webui/open-webui/commit/7b29834d4216e5db70b68f3598fa1ad654d3512b)
|
||||
- 🔌 **Terminal connections can't be redirected to another user.** Terminal session identifiers are now safely encoded before being passed upstream, closing a way to tamper with the connection's user identity. [#26042](https://github.com/open-webui/open-webui/pull/26042)
|
||||
- 📡 **Real-time events only reach your own session.** The server now verifies that a real-time event is delivered only to the requesting user's own active session, instead of trusting a client-supplied session identifier. [#25763](https://github.com/open-webui/open-webui/pull/25763)
|
||||
- 🔓 **Revoked sessions are rejected on real-time connections.** Real-time and terminal WebSocket connections now honor token revocation and expiry, so a signed-out or expired session can no longer keep a live connection open. [Commit](https://github.com/open-webui/open-webui/commit/33b91bd8ae8a100a5a306c91441a7d0b422c4cde), [#25764](https://github.com/open-webui/open-webui/pull/25764), [#25686](https://github.com/open-webui/open-webui/pull/25686)
|
||||
- 🕳️ **Another DNS-rebinding gap closed in URL fetching.** Fetching a URL's content now re-checks the destination address at connection time, closing another path that could reach internal addresses behind a public hostname. [#25775](https://github.com/open-webui/open-webui/pull/25775)
|
||||
- 🗣️ **Azure speech input is escaped.** Voice and language values are now escaped when building Azure text-to-speech requests, preventing malformed or injected markup. [#25776](https://github.com/open-webui/open-webui/pull/25776)
|
||||
- ⚙️ **Interface settings update respects its permission.** Saving interface settings now enforces the interface permission, so users without it can no longer change those settings through the API. [#25996](https://github.com/open-webui/open-webui/pull/25996)
|
||||
- 🗄️ **Unknown knowledge collections are denied by default.** Retrieval now rejects unknown or unscoped collection names by default, closing a legacy path that could be used to reach collections outside the normal access checks. [Commit](https://github.com/open-webui/open-webui/commit/d99ac7d3f83b25161ca775229150c8f7c74cceee)
|
||||
- 🙈 **Error responses no longer leak internals.** Server error responses now return sanitized messages instead of raw exception text, so internal details aren't exposed to signed-in users. [Commit](https://github.com/open-webui/open-webui/commit/ee5de69e374aabf5631da18a5bbc1c285ee6f7a1), [Commit](https://github.com/open-webui/open-webui/commit/0cc331d1c60341bb06b78ceeecfb2db86179c93e), [Commit](https://github.com/open-webui/open-webui/commit/396d9ac18193d43e40fb9d068075d4b780e971d7), [Commit](https://github.com/open-webui/open-webui/commit/0883638027a9b3cb7c9851f031c4f5fc1af1f25d), [#26375](https://github.com/open-webui/open-webui/pull/26375), [#26374](https://github.com/open-webui/open-webui/issues/26374)
|
||||
- 📏 **Upload size limit enforced on the server.** The maximum upload size is now enforced server-side, so it can't be bypassed by a client that ignores the limit. [Commit](https://github.com/open-webui/open-webui/commit/f8ec63203c4408c46bb06698ae624d17b01b9301), [Commit](https://github.com/open-webui/open-webui/commit/d3676b4f71bfdbaf4e4d76943c51117e18932ccf), [#25869](https://github.com/open-webui/open-webui/pull/25869)
|
||||
- 🖼️ **OAuth profile pictures are validated.** Profile picture URLs from OAuth providers are now validated and their type checked when stored, preventing unsafe image sources. [Commit](https://github.com/open-webui/open-webui/commit/eb53281c9acb3660e09554a8dbde0a0b42646b70), [#24548](https://github.com/open-webui/open-webui/pull/24548)
|
||||
- 📦 **Security updates to frontend dependencies.** Several frontend dependencies were updated to patch known security vulnerabilities. [#26281](https://github.com/open-webui/open-webui/pull/26281)
|
||||
- 🤝 **Chat sharing respects the user-sharing permission.** The share-chat dialog now hides the option to share with specific users from people who lack that permission, matching the access rules enforced elsewhere. [#25915](https://github.com/open-webui/open-webui/pull/25915)
|
||||
- 📤 **Chat export respects its permission everywhere.** Every chat export menu now checks the export permission, so users without it can no longer export chats through one of the dropdown menus. [#25914](https://github.com/open-webui/open-webui/pull/25914)
|
||||
- 📂 **File write access requires real ownership.** Editing or deleting a file through a knowledge base or workspace model now requires that the object's owner actually owns the file, so a read-only file can no longer gain write access by being referenced from an object you control. [#26032](https://github.com/open-webui/open-webui/pull/26032)
|
||||
- 🖌️ **Image edit endpoint enforces permission.** The image-edit endpoint now checks the image-edit switch and the image-generation permission, matching image generation, so it can't be called by users who lack access. [#26009](https://github.com/open-webui/open-webui/pull/26009)
|
||||
- 📁 **Folder permission enforced on all folder actions.** Every folder operation now checks the folders permission, so the setting is respected consistently instead of only when listing folders. [Commit](https://github.com/open-webui/open-webui/commit/19a176fd36bea15c49d7f2d1539b4832e57a8bc2)
|
||||
- 🧩 **Code Execution settings collapse when off.** The Code Execution settings section now collapses when the toggle is disabled, keeping the settings page tidy. [#25970](https://github.com/open-webui/open-webui/pull/25970)
|
||||
- 📅 **German date format in Notes.** Dates in the Notes view now display correctly for German, where they previously failed to render. [#25985](https://github.com/open-webui/open-webui/pull/25985)
|
||||
- 🎙️ **ElevenLabs speech keeps working when voices can't load.** Text-to-speech through ElevenLabs no longer fails when the available-voice list can't be fetched, instead of rejecting every voice. [Commit](https://github.com/open-webui/open-webui/commit/bb1419328b11b801b4c939dfc112700ba6f6fdab), [#26075](https://github.com/open-webui/open-webui/issues/26075)
|
||||
- 🪟 **Default Permissions modal resets on close.** Closing the Default Permissions dialog without saving now discards unsaved edits instead of keeping them around the next time you open it. [Commit](https://github.com/open-webui/open-webui/commit/78a5015846a9e55ff2bc9d6cc98f880437abe8ed)
|
||||
- 👯 **Side-by-side chat with the same model.** Running two panes with the same model no longer leaves one pane stuck waiting or showing the other pane's reply after a reload, since each pane's messages are now tracked separately. [Commit](https://github.com/open-webui/open-webui/commit/56ae99e96a845289b5787d2dd26a3d828f2295e7), [#25982](https://github.com/open-webui/open-webui/issues/25982)
|
||||
- 💾 **Model edits no longer lost when changing access.** Adjusting a model's access no longer auto-saves on its own and discards your other unsaved changes to that model. [#26004](https://github.com/open-webui/open-webui/pull/26004)
|
||||
- 🔧 **Parallel tool calls over the Anthropic-compatible API.** External Anthropic-compatible clients calling Open WebUI's messages endpoint now receive tool calls reliably when a model issues several at once or returns them in its final message. [Commit](https://github.com/open-webui/open-webui/commit/4210cae68e30173d7902582d32128dd699d5628a), [#25963](https://github.com/open-webui/open-webui/pull/25963), [#25964](https://github.com/open-webui/open-webui/discussions/25964)
|
||||
- 🗃️ **Prompt caching preserved over the Anthropic-compatible API.** Requests through the Anthropic-compatible API now keep their prompt-caching markers instead of having them stripped, so clients that rely on caching work as intended. [Commit](https://github.com/open-webui/open-webui/commit/caedcbae4988ef59ea7052b2a3198e2da4b5291a), [#25998](https://github.com/open-webui/open-webui/pull/25998), [#25964](https://github.com/open-webui/open-webui/discussions/25964)
|
||||
- 🔁 **Fewer redundant data loads.** Several views no longer fire duplicate background fetches at once, avoiding occasional glitches from overlapping requests. [#25943](https://github.com/open-webui/open-webui/pull/25943), [#25942](https://github.com/open-webui/open-webui/pull/25942), [#25934](https://github.com/open-webui/open-webui/pull/25934), [#25935](https://github.com/open-webui/open-webui/pull/25935), [#25838](https://github.com/open-webui/open-webui/pull/25838), [Commit](https://github.com/open-webui/open-webui/commit/e8d55c0a8beac9de0b2a0fe90f0bc0f9b64c1c1f)
|
||||
- 🔎 **Steadier search boxes across admin and workspace.** Search fields for users, knowledge, prompts, tools, and similar lists now run only as you type and reset to the first page correctly, instead of occasionally re-searching on their own. [Commit](https://github.com/open-webui/open-webui/commit/fc9c2ea1915accd1f6edca467e965283dff71cd7), [#25938](https://github.com/open-webui/open-webui/pull/25938)
|
||||
- 📊 **Admin feedback list loads again on PostgreSQL.** The admin feedback list no longer fails to load on PostgreSQL setups, where it previously returned a server error. [Commit](https://github.com/open-webui/open-webui/commit/7ee75a0c04a31528954903e88c9213d5fbb31aa7), [#25953](https://github.com/open-webui/open-webui/issues/25953)
|
||||
- 🗂️ **Deleting nested folders checks chats correctly.** Deleting a folder that contains subfolders now accounts for the chats inside those subfolders when applying the delete-permission check, instead of only the top-level folder's chats. [Commit](https://github.com/open-webui/open-webui/commit/232421f40b84590e6d6fdecab4e43274aac37add), [#25920](https://github.com/open-webui/open-webui/issues/25920)
|
||||
- 🖱️ **Dragging chats into folders is more reliable.** Dragging a chat into a folder no longer throws an error in cases where the chat couldn't be resolved. [#25928](https://github.com/open-webui/open-webui/pull/25928)
|
||||
- 🛠️ **Workspace menu shows for the skills permission.** Users who only have the skills permission now see the Workspace entry in their menu, which previously appeared only for other workspace permissions. [#25925](https://github.com/open-webui/open-webui/pull/25925)
|
||||
- 🧠 **Admins can always reach memories.** Administrators can now use the memories endpoints regardless of the memories permission toggle, matching how admin access works for other features. [#25924](https://github.com/open-webui/open-webui/pull/25924)
|
||||
- 🖼️ **Image settings page survives a config load failure.** The admin image settings page no longer crashes when its configuration fails to load, showing the page instead. [#25933](https://github.com/open-webui/open-webui/pull/25933)
|
||||
- 🧵 **Code blocks render in channel threads.** Code blocks now display correctly in a channel's thread view, where duplicated message identifiers previously broke their rendering. [Commit](https://github.com/open-webui/open-webui/commit/7d1f9415807a47e0da4f862327e9802a3b839753), [#25917](https://github.com/open-webui/open-webui/pull/25917)
|
||||
- 🔵 **No more false unread badges on chats.** Chats no longer show an unread indicator after automatic changes like title generation or pinning, archiving, and moving them between folders, and newly created chats are marked read correctly so they don't appear unread after a refresh. [#25912](https://github.com/open-webui/open-webui/pull/25912), [#25782](https://github.com/open-webui/open-webui/pull/25782), [#25108](https://github.com/open-webui/open-webui/issues/25108)
|
||||
- 📌 **Pinned notes stay in sync.** Pinning, unpinning, or deleting a note now updates the sidebar's pinned list consistently, instead of showing a stale pin state. [#25918](https://github.com/open-webui/open-webui/pull/25918), [#25640](https://github.com/open-webui/open-webui/pull/25640)
|
||||
- 📅 **All-day calendar events keep their date.** Saving an all-day calendar event no longer shifts it by a day for users in certain time zones. [#25864](https://github.com/open-webui/open-webui/pull/25864)
|
||||
- 🧷 **Damaged chat history recovers more reliably.** When a chat's current position is missing or points at a malformed message, Open WebUI now repairs it from the latest valid message — on both the client and the server — instead of risking a broken history view. [Commit](https://github.com/open-webui/open-webui/commit/2308b59f135e4c2da11eabdf2306e55a5dd4e9fb), [Commit](https://github.com/open-webui/open-webui/commit/a146e17bdcaf94fee3a98aa36b4f401e1f06c1d4), [#26298](https://github.com/open-webui/open-webui/pull/26298), [#26258](https://github.com/open-webui/open-webui/pull/26258), [#26257](https://github.com/open-webui/open-webui/issues/26257)
|
||||
- 💾 **Saving a chat no longer drops messages.** Chat updates are now merged with the existing history on the server, with explicit tracking of deleted messages, instead of overwriting it, preventing message loss from concurrent or partial saves. [Commit](https://github.com/open-webui/open-webui/commit/22a44e67a8ba781feb8f2a267fed0c40213d8432), [Commit](https://github.com/open-webui/open-webui/commit/3319b6410e1b600b7a885a5fb78573e9a2061c22), [Commit](https://github.com/open-webui/open-webui/commit/24b8619f64731788ac38813768abbb64405effa4), [#25657](https://github.com/open-webui/open-webui/pull/25657)
|
||||
- 📺 **Channel message updates stay in their channel.** Streaming updates to a channel message are now skipped if the message no longer exists or belongs to a different channel, preventing stray updates. [Commit](https://github.com/open-webui/open-webui/commit/ac3449cac91e62b08a7c28e54fcd044d14dea791)
|
||||
- 📌 **Pinned channel messages update for everyone.** Pinning or unpinning a channel message now updates live for all members and works from thread views, instead of only changing for the person who pinned it. [Commit](https://github.com/open-webui/open-webui/commit/7ea7680f563da30b121258e5a7d7123185c4da2a)
|
||||
- 📄 **Mistral OCR uploads work again.** Document OCR through Mistral has been repaired after an upstream library change broke its file uploads. [#25779](https://github.com/open-webui/open-webui/pull/25779)
|
||||
- 🗂️ **Chroma collection detection fixed.** Open WebUI now correctly detects existing Chroma collections, fixing a case where it always reported them as missing. [#25780](https://github.com/open-webui/open-webui/pull/25780)
|
||||
- 📊 **Vega-Lite charts render reliably.** Vega-Lite charts in chat are now detected by their code block language tag, so they render correctly. [#25843](https://github.com/open-webui/open-webui/pull/25843)
|
||||
- 🏷️ **Long chat tag lists scroll.** The tags section in the chat menu now scrolls instead of overflowing when a chat has many tags. [#26031](https://github.com/open-webui/open-webui/pull/26031)
|
||||
- ⌨️ **Enter key shows correctly on iOS.** The Enter key symbol in the keyboard shortcuts list no longer renders as an emoji on iOS. [#26173](https://github.com/open-webui/open-webui/pull/26173)
|
||||
- 🔗 **Whitespace in names no longer breaks MCP connections.** User name and info headers are now trimmed before being forwarded, fixing MCP connection failures when a display name contained leading or trailing whitespace. [#26182](https://github.com/open-webui/open-webui/pull/26182), [#26181](https://github.com/open-webui/open-webui/issues/26181)
|
||||
- 🈳 **Search no longer fires mid-composition.** Typing in search with an input method editor (such as Japanese, Chinese, or Korean) no longer triggers a search when you press Enter to confirm a composition. [#26238](https://github.com/open-webui/open-webui/pull/26238), [#26285](https://github.com/open-webui/open-webui/pull/26285), [#26172](https://github.com/open-webui/open-webui/issues/26172)
|
||||
- 🧰 **Valves icon stays visible.** The icon for configuring valves no longer disappears, so user-configurable tool and function settings remain reachable. [#26256](https://github.com/open-webui/open-webui/pull/26256)
|
||||
- 🎛️ **Chat controls persist across navigation.** Edits to chat controls are now kept when navigating between chats, and reverting a control to the chat's saved value persists correctly, instead of being lost. [#26336](https://github.com/open-webui/open-webui/pull/26336), [#25793](https://github.com/open-webui/open-webui/pull/25793)
|
||||
- 🔍 **Chat search tool handles empty queries.** The built-in chat search tool no longer crashes when called with an empty query. [Commit](https://github.com/open-webui/open-webui/commit/b854eb09b13216f914ce5fd07ab717b8f752882b), [#26310](https://github.com/open-webui/open-webui/issues/26310)
|
||||
- 📑 **More robust MinerU document processing.** Document processing through MinerU now handles its ZIP results more safely, including very large outputs. [Commit](https://github.com/open-webui/open-webui/commit/23d03d6aaebcced6c1e39e98dfff76ab73df8804), [#26263](https://github.com/open-webui/open-webui/pull/26263)
|
||||
- ⏰ **Scheduled automations with session-auth tools work.** Automations that use session-authenticated tools or terminals now authenticate correctly when running on a schedule, instead of failing. [Commit](https://github.com/open-webui/open-webui/commit/5b1c42e81a3ef3ad5ce5852dbf84020cb5e2498c), [#26247](https://github.com/open-webui/open-webui/pull/26247), [#26137](https://github.com/open-webui/open-webui/issues/26137)
|
||||
- 📝 **Model system prompt preserved with knowledge.** A model's system prompt is no longer dropped when knowledge retrieval runs with native tool calling. [Commit](https://github.com/open-webui/open-webui/commit/cfb49c4c181a96d5df07fbfacd819639baef0bab), [#26217](https://github.com/open-webui/open-webui/pull/26217)
|
||||
- 🔑 **Expired sessions return you to sign-in.** When a request fails because your session has expired, Open WebUI now redirects you to the sign-in page instead of leaving you on a broken view. [Commit](https://github.com/open-webui/open-webui/commit/5922727402593900758d84004f950071c701f6de), [#26237](https://github.com/open-webui/open-webui/pull/26237)
|
||||
- 🎯 **Ejecting a workspace model unloads the right model.** Unloading a workspace model now resolves to its underlying base model, so the correct model is freed from memory. [Commit](https://github.com/open-webui/open-webui/commit/464e703e4716812d015966582152ddd8a2c71572), [#26269](https://github.com/open-webui/open-webui/pull/26269)
|
||||
- 🔄 **Edited models refresh in the admin list.** After editing a model in the admin settings, the models list now updates right away instead of needing a manual reload. [Commit](https://github.com/open-webui/open-webui/commit/b34d6c836ee43d0e9721fa4fd6457d934e3e2a17)
|
||||
- 🗂️ **Workspace model bulk actions and search work across pages.** Bulk actions on workspace models now apply across all of them, and search results paginate correctly. [#26274](https://github.com/open-webui/open-webui/pull/26274)
|
||||
- 🧩 **MCP resource results come through.** Tool results that return resource content — including binary blobs and URI references — are no longer silently dropped, and image results are attached as files. [#25260](https://github.com/open-webui/open-webui/pull/25260), [#24038](https://github.com/open-webui/open-webui/issues/24038), [Commit](https://github.com/open-webui/open-webui/commit/783205a965c556815fae84b64d74f26a2e5e5729)
|
||||
- 🔗 **Broader MCP server compatibility for OAuth.** Open WebUI now discovers an MCP server's protected resource metadata even when the server doesn't advertise it, and recognizes more OAuth preflight variations, so more MCP servers connect. [#25980](https://github.com/open-webui/open-webui/pull/25980), [#25954](https://github.com/open-webui/open-webui/issues/25954), [Commit](https://github.com/open-webui/open-webui/commit/45fea34bd0c8ce54b0822499c40e3e6964220354), [#26068](https://github.com/open-webui/open-webui/pull/26068)
|
||||
- 📤 **Clearer upload error messages.** Failed uploads now show a readable explanation instead of an opaque error stub. [#25961](https://github.com/open-webui/open-webui/pull/25961)
|
||||
- 📋 **Cloned prompts get a proper title.** Cloning a prompt now adds the clone suffix to the correct field, so the duplicate is named as expected. [#25800](https://github.com/open-webui/open-webui/pull/25800)
|
||||
- 📐 **Long default group names don't overflow.** A long default group name no longer overflows its row in the admin authentication settings. [#25685](https://github.com/open-webui/open-webui/pull/25685)
|
||||
- 🖐️ **Sidebar drags don't trigger uploads.** Dragging a chat item in the sidebar no longer shows the file-upload overlay. [#25675](https://github.com/open-webui/open-webui/pull/25675)
|
||||
- 🔁 **Recovers from a stuck streaming response.** If the signal that a response finished is missed — for example after a mobile app is backgrounded mid-stream — Open WebUI now recovers the chat instead of leaving it stuck in a streaming state. [Commit](https://github.com/open-webui/open-webui/commit/aa851d93c63e7da6e94292d0b7586674339d47e5), [Commit](https://github.com/open-webui/open-webui/commit/edf2c6c8f76e7f6a5917e991f371a604adc34c5f), [Commit](https://github.com/open-webui/open-webui/commit/2856def6c05b2fb8c55b4e7170f05db0c4f956f1), [#26320](https://github.com/open-webui/open-webui/pull/26320), [#26315](https://github.com/open-webui/open-webui/issues/26315)
|
||||
- 🧠 **Model skills load on demand instead of filling the prompt.** A model's attached skills are now presented to the model as a manifest it can load when needed, rather than having their full content inserted into the system prompt; skills you mention inline still get their content included directly. [Commit](https://github.com/open-webui/open-webui/commit/e6d35fc4cca4f4b1e5cad97d7b7e3089ef832018), [Commit](https://github.com/open-webui/open-webui/commit/44b9463498085741669e6f5d92e21b5ecc5fd795), [#25592](https://github.com/open-webui/open-webui/issues/25592), [#25599](https://github.com/open-webui/open-webui/pull/25599)
|
||||
- 🗂️ **Empty metadata no longer breaks Chroma indexing.** Document metadata with empty values is now filtered out before indexing, fixing a case that could fail on Chroma. [Commit](https://github.com/open-webui/open-webui/commit/118549caf3), [#26342](https://github.com/open-webui/open-webui/pull/26342), [#26339](https://github.com/open-webui/open-webui/issues/26339)
|
||||
- 🔁 **Updating a knowledge file won't break the knowledge base.** When a file's content is updated, its new embeddings are now added before the old ones are removed, so a failed reindex leaves the knowledge base intact and usable instead of empty. [Commit](https://github.com/open-webui/open-webui/commit/248315de14d4537e0f2ec3f94dee8a7334cad248), [#23789](https://github.com/open-webui/open-webui/pull/23789), [#23787](https://github.com/open-webui/open-webui/issues/23787)
|
||||
- 🔤 **Documents with special tokens index correctly.** Measuring chunk sizes no longer fails when a document contains text that looks like a special token. [#26210](https://github.com/open-webui/open-webui/pull/26210)
|
||||
- 📝 **Note file attachments stay in sync.** Updating the files attached to a note now keeps the editor and saved note in sync. [Commit](https://github.com/open-webui/open-webui/commit/5055fb85aa8c8d5ef785daea7438498e36ddf33f)
|
||||
- 📱 **Better banner layout on mobile.** Notification banners now lay out correctly on small screens. [Commit](https://github.com/open-webui/open-webui/commit/4ed45ce84394c435405f93d03f07dabd797cdec3), [#24912](https://github.com/open-webui/open-webui/pull/24912)
|
||||
- 📂 **Knowledge file listing includes attached files.** Listing files through the knowledge tools now also shows files attached directly to a model, not only those inside a knowledge base, fixing cases where listing returned no results for a model with a single attached file. [Commit](https://github.com/open-webui/open-webui/commit/40b655e99e2c6dd802654ec0cdac38a4bcda08b3), [#26301](https://github.com/open-webui/open-webui/issues/26301)
|
||||
- 🏷️ **Chat titles generate after long first responses.** A new chat now gets its title even when the first response takes a long time, such as one with extensive reasoning or many tool calls, instead of staying "New Chat". [Commit](https://github.com/open-webui/open-webui/commit/754787f43dffad3dce2c90e4fd0417b1f9dbb3c0), [#26240](https://github.com/open-webui/open-webui/issues/26240)
|
||||
- 🔌 **Cancelling an MCP request no longer errors.** Stopping a response that was using MCP tools now shuts the connection down cleanly instead of surfacing a server error. [Commit](https://github.com/open-webui/open-webui/commit/ff5cec43bd360829cfdcc6a5253d1ba63f236b7f)
|
||||
- 🧠 **Reasoning details preserved across turns.** Models that return structured or encrypted reasoning data, such as Gemini, no longer have their assistant message split mid-stream, keeping reasoning continuity across turns. [Commit](https://github.com/open-webui/open-webui/commit/75db531c1238af113bb2b211882713e5e2f459cf), [#23852](https://github.com/open-webui/open-webui/pull/23852)
|
||||
- 📡 **Error messages show for non-standard streaming responses.** Providers that send errors over non-standard server-sent events now surface a readable error instead of nothing. [#23228](https://github.com/open-webui/open-webui/pull/23228)
|
||||
- 🔑 **Whitespace in terminal server keys no longer breaks auth.** Terminal server API keys are now trimmed before use, so a key with stray leading or trailing whitespace still authenticates. [Commit](https://github.com/open-webui/open-webui/commit/fe3300bd6581aa469c2cdf757700ecc65a200df4), [Commit](https://github.com/open-webui/open-webui/commit/d6cda4a04b2e3a48855fc91abb2376cfd3a0378d)
|
||||
- 🔥 **One bad URL no longer fails Firecrawl scraping.** When fetching multiple pages through Firecrawl, a single failing URL is now skipped instead of aborting the whole batch, and rate limits are respected between requests. [Commit](https://github.com/open-webui/open-webui/commit/6f8221df58b17334233ac6bfe069b8f837f677d6), [#24183](https://github.com/open-webui/open-webui/pull/24183)
|
||||
- 📱 **Usable chat input on mobile with many tools.** When skills, tools, terminal, web search, and image generation buttons fill the chat input, the row of buttons now scrolls horizontally while the menu, voice, and send controls stay reachable, instead of pushing them off-screen. [Commit](https://github.com/open-webui/open-webui/commit/6f8221df58b17334233ac6bfe069b8f837f677d6), [#26142](https://github.com/open-webui/open-webui/issues/26142)
|
||||
- 👤 **Owner avatars only show on shared folders.** Chat owner avatars in a folder's chat list now appear only when the folder is actually shared, instead of showing whenever owner information happened to be present. [Commit](https://github.com/open-webui/open-webui/commit/9802b0d13563b3535b86a350bab000d84686b1e9)
|
||||
- 📜 **No stray scrollbar on the About page.** Extra spacing that caused an unnecessary scrollbar on the About settings page has been removed. [#25802](https://github.com/open-webui/open-webui/pull/25802)
|
||||
- 🚪 **Sign out works from the Account Pending page.** Signing out while your account is pending now goes through the proper sign-out flow, so single sign-on sessions are ended and you are no longer left stuck on the pending screen. [#25681](https://github.com/open-webui/open-webui/pull/25681), [#25644](https://github.com/open-webui/open-webui/issues/25644)
|
||||
- 🔢 **Built-in tools accept numeric arguments.** Built-in tools no longer crash when a model passes a number or a string where a specific scalar type is expected; values are now coerced to the declared type. [Commit](https://github.com/open-webui/open-webui/commit/c4688b958d7f7929f5f4303493ca311c2c121683), [#25638](https://github.com/open-webui/open-webui/pull/25638), [#25731](https://github.com/open-webui/open-webui/pull/25731), [#25641](https://github.com/open-webui/open-webui/issues/25641)
|
||||
- ⏱️ **MinerU timeout saves.** The MinerU API timeout can now be saved from the admin settings, accepting a numeric value. [Commit](https://github.com/open-webui/open-webui/commit/3fd0384ffcd0eddd6f4c688475f8ad5d3b4de510), [#25604](https://github.com/open-webui/open-webui/pull/25604), [#25603](https://github.com/open-webui/open-webui/issues/25603)
|
||||
- 🔧 **Background completion no longer clears active tasks.** Finishing a chat in the background no longer wipes the set of active tasks, fixing a case where ongoing task indicators could be lost. [Commit](https://github.com/open-webui/open-webui/commit/388f62f8a002b789887d016892a1bf152c9d90af), [#25217](https://github.com/open-webui/open-webui/issues/25217)
|
||||
- 👁️ **Workspace base model selector respects visibility.** The base model selector in the workspace now hides models you don't have access to, matching their visibility settings. [#25668](https://github.com/open-webui/open-webui/pull/25668)
|
||||
- 🧵 **Channel threads bind to the right channel.** A channel thread's parent and replies are now tied to the channel in the URL, preventing mismatches when switching channels. [#25766](https://github.com/open-webui/open-webui/pull/25766)
|
||||
- 🗑️ **Unsharing cleans up orphaned rows.** Unsharing a chat now handles leftover shared-chat records, avoiding stale entries. [#25632](https://github.com/open-webui/open-webui/pull/25632)
|
||||
- 🔎 **Web search results reach the model with retrieval on.** Web search results are now passed to the model even when embedding and retrieval are enabled, instead of being left out. [#25600](https://github.com/open-webui/open-webui/pull/25600)
|
||||
- 🔢 **Group count follows search.** The groups count now reflects the filtered search results instead of the full list. [#25689](https://github.com/open-webui/open-webui/pull/25689)
|
||||
- ␣ **Space key works when renaming.** Pressing space while renaming a file or folder no longer opens it, so spaces can be typed in names. [#25627](https://github.com/open-webui/open-webui/pull/25627)
|
||||
- 🩹 **Missing local embedding model no longer blocks startup.** A missing local embedding model now surfaces as a deferred error instead of preventing the server from starting. [#25683](https://github.com/open-webui/open-webui/pull/25683)
|
||||
- 🔤 **Consistent settings label capitalization.** Toggle labels in settings now use consistent title casing. [#25765](https://github.com/open-webui/open-webui/pull/25765)
|
||||
- ♿ **Better screen-reader labels on toggles.** Integration and switch toggles now expose proper accessibility labels and pressed state for screen readers. [#25258](https://github.com/open-webui/open-webui/pull/25258), [#25230](https://github.com/open-webui/open-webui/pull/25230)
|
||||
- 📜 **Long dropdowns scroll.** Dropdown selects now scroll when their list is long, so all options stay reachable. [Commit](https://github.com/open-webui/open-webui/commit/4bc463072185d0d7c1c9218cd4487501090eccfe), [#25608](https://github.com/open-webui/open-webui/pull/25608)
|
||||
- 🔽 **Collapsible sections don't misfire on load.** Collapsible sections no longer trigger their change action when first rendered, avoiding unintended toggles on page load. [Commit](https://github.com/open-webui/open-webui/commit/c93d4f04aad1b0d4f8a8bda7ac403b2c8ee35f38), [#25229](https://github.com/open-webui/open-webui/pull/25229)
|
||||
- ➗ **Large math expressions no longer crash rendering.** Parsing math delimiters no longer overflows on very large or deeply nested input, so messages with heavy math render instead of failing. [#25845](https://github.com/open-webui/open-webui/pull/25845)
|
||||
- 🗄️ **Oversized chunks no longer break Milvus indexing.** Overly long text chunks are now trimmed before being sent to Milvus, so a single large chunk can no longer fail the whole batch and leave a file with no embeddings. [#25857](https://github.com/open-webui/open-webui/pull/25857), [#25858](https://github.com/open-webui/open-webui/pull/25858)
|
||||
- 📝 **Code editor stays open when empty.** The code editor drawer no longer collapses when its content is empty. [#25855](https://github.com/open-webui/open-webui/pull/25855)
|
||||
- 💽 **Settings no longer lost after a restart.** Admin configuration is now stored more reliably, fixing cases where external connections and model parameters could be lost after restarting the server. [Commit](https://github.com/open-webui/open-webui/commit/5cdcdbaeec9fc8156721c38c33ec37956962871c), [Commit](https://github.com/open-webui/open-webui/commit/21f9e5295bf484169d72f4538f7c926b5519723c), [Commit](https://github.com/open-webui/open-webui/commit/8958b64b5a7e96cd8c2260571b54324ca3bfe127), [#24743](https://github.com/open-webui/open-webui/issues/24743), [#25911](https://github.com/open-webui/open-webui/pull/25911), [#25959](https://github.com/open-webui/open-webui/pull/25959)
|
||||
- 📜 **Visible chat scrollbar.** The chat area now shows a scrollbar, making it easier to scroll through long responses. [Commit](https://github.com/open-webui/open-webui/commit/d56e1cb0b9), [#25833](https://github.com/open-webui/open-webui/issues/25833)
|
||||
- 🎚️ **Default model parameters apply to requests.** Default model parameters are now applied to outbound requests, so settings like temperature and the context window take effect as configured. [Commit](https://github.com/open-webui/open-webui/commit/cd6cc39c6d), [Commit](https://github.com/open-webui/open-webui/commit/19db873603215773f9a64e03785a1a076dc6c8a8), [#24930](https://github.com/open-webui/open-webui/issues/24930), [#26209](https://github.com/open-webui/open-webui/issues/26209)
|
||||
- 🟢 **Ollama loaded-model indicator restored.** The indicator showing which Ollama model is loaded in VRAM works again after recent changes. [#25586](https://github.com/open-webui/open-webui/issues/25586), [#25732](https://github.com/open-webui/open-webui/issues/25732)
|
||||
- 🪪 **Static MCP connectors recover missing OAuth details.** MCP connectors configured with static OAuth credentials now fill in a missing scope or resource from the server's published metadata, so they connect correctly instead of failing when those values were left out. [Commit](https://github.com/open-webui/open-webui/commit/88901bfa041ddcceab1cd4a97f08f0b43835eb05), [#25898](https://github.com/open-webui/open-webui/issues/25898)
|
||||
- 📊 **Token usage and cost stats no longer wiped by background tasks.** A response's token usage and cost are now preserved when background tasks like title, tag, and follow-up generation run on the same chat, instead of being overwritten. [Commit](https://github.com/open-webui/open-webui/commit/95391221dfabbfcd9090ab472b1c02a4c75c0387)
|
||||
- 🔗 **Model share link updated.** Sharing a model now opens the current community post page, fixing the link that pointed at the old endpoint. [#25801](https://github.com/open-webui/open-webui/pull/25801)
|
||||
|
||||
### Changed
|
||||
|
||||
- ⚠️ **Database Migrations**: This update contains database migrations. Please be sure to back up your database before updating, as downgrading after the migration is not supported.
|
||||
- 🔔 **System events now fire automatically.** With the new event system, Open WebUI emits events for activity like startup, sign-ins, and configuration changes, so any webhook you already have configured may begin receiving calls for these newly emitted events after upgrading. Review your event and webhook settings after updating so you only receive the events you want. [Commit](https://github.com/open-webui/open-webui/commit/b5c43968db0ea1556b228d143ae5946dc4e944ba)
|
||||
- 🔀 **Native tool calling is now the default.** Every chat and model that had not explicitly chosen a tool-calling mode now runs Native, which relies on a model's built-in tool support, while the old behavior has been renamed "Legacy" and made the explicit opt-out; if your models depend on the previous approach you must switch them back to "Legacy" per chat, per model, or globally in your default model parameters to preserve their behavior. [Commit](https://github.com/open-webui/open-webui/commit/b1d40f340921c27eb9a965b9feeb2563856e25e2)
|
||||
- 🗂️ **Authentication settings moved to their own page.** LDAP, OAuth, and related authentication settings have moved out of the General settings page into a dedicated Authentication page in the admin panel. [Commit](https://github.com/open-webui/open-webui/commit/5cdcdbaeec9fc8156721c38c33ec37956962871c)
|
||||
- 🎓 **Several features are no longer beta.** Memories, Notes, Channels, and High Contrast Mode have graduated out of beta and no longer carry a beta label. [Commit](https://github.com/open-webui/open-webui/commit/7b55a63fc7ee323e9114713ce1d2f3f688aa37e6)
|
||||
- 🔧 **Local web fetch setting renamed.** The "ENABLE_RAG_LOCAL_WEB_FETCH" environment variable is now "ENABLE_LOCAL_WEB_FETCH", reflecting that it applies beyond retrieval; the old name still works as a deprecated alias. [Commit](https://github.com/open-webui/open-webui/commit/e3ba6984534898695b47ee4fc3d6b746e2865abc)
|
||||
- 🔧 **You.com search key renamed.** You.com web search now prefers the "YDC_API_KEY" environment variable, with the previous "YOUCOM_API_KEY" still accepted as a fallback. [Commit](https://github.com/open-webui/open-webui/commit/df634bb64f5043b0292e43c69bd1d31676c89328), [#26316](https://github.com/open-webui/open-webui/pull/26316)
|
||||
- 🧪 **Client-side Python now runs sandboxed.** Client-side Python (Pyodide) now runs in a sandboxed, opaque-origin iframe by default, isolating executed code from your session, cookies, local storage, and the app's own endpoints, while full Python, JavaScript, and external network access keep working. Code that relied on reaching same-origin Open WebUI endpoints from Pyodide will no longer be able to, and Pyodide is now marked legacy in the admin Code Execution settings. [Commit](https://github.com/open-webui/open-webui/commit/516051304e1b1f250c34438746ade673a79bd40c), [Commit](https://github.com/open-webui/open-webui/commit/c7be66626fd10c75ec35f662a709129ba1b020ec), [Commit](https://github.com/open-webui/open-webui/commit/62ae2069183109d878d72b9444a0e7c4f6c66caa), [Commit](https://github.com/open-webui/open-webui/commit/518702caae5a6484e71aa79e8ab908ec398290a7), [Commit](https://github.com/open-webui/open-webui/commit/03a8363583b7e0e04760d49f1e8d28dbbfefee4d)
|
||||
|
||||
## [0.9.6] - 2026-06-01
|
||||
|
||||
### Added
|
||||
|
||||
- 📦 **Official knowledge base sync tool.** A new companion tool from Open WebUI, oikb, keeps a knowledge base in sync with a local directory, GitHub repo, S3 bucket, Confluence space, or any of more than 40 other sources, uploading only new and changed files using the incremental sync support added in this release. [oikb](https://github.com/open-webui/oikb)
|
||||
- 📂 **Smart directory sync for knowledge bases.** Local directories can now be synced into a knowledge base in one action: file checksums are compared against what's already stored, and only added or modified files are uploaded while removed files and orphaned subdirectories are cleaned up, with the directory structure mirrored automatically and per-file progress shown throughout. [#19190](https://github.com/open-webui/open-webui/issues/19190), [#19394](https://github.com/open-webui/open-webui/issues/19394), [Commit](https://github.com/open-webui/open-webui/commit/60c9db1cb81d021589cb49bee8744a799b51211f), [Commit](https://github.com/open-webui/open-webui/commit/73bdf86766d3f44467cdec436786fe089481baf3), [Commit](https://github.com/open-webui/open-webui/commit/9835b3f1dd7aa9c92ff08a634a0f97cc2a046e42), [Commit](https://github.com/open-webui/open-webui/commit/97252fa609440573251d8e75090f347ed1e51e1d), [Commit](https://github.com/open-webui/open-webui/commit/8f2d346e10c47b57bf6b5a6aa02d453488a88b89), [Commit](https://github.com/open-webui/open-webui/commit/1527eb6e01d441225c979d12d78b7625edf1086f)
|
||||
- 🗂️ **Knowledge base folders.** Files inside a knowledge base can now be organized into nested folders, with breadcrumb navigation that makes it much easier to manage and find content in large collections. [Commit](https://github.com/open-webui/open-webui/commit/c2cbc47ca76ebfa21e1d36279dbd859283cbbfb1), [Commit](https://github.com/open-webui/open-webui/commit/ab0ee858b73738c5b77d1d455e66e66bc61df1c4), [Commit](https://github.com/open-webui/open-webui/commit/2ad327a4dce64a8eedf5eb41c73b8520344fdd82), [Commit](https://github.com/open-webui/open-webui/commit/171150c1e12b5713f77a5ea0dd19c6581c7bef2e), [Commit](https://github.com/open-webui/open-webui/commit/e7d2ddbb1d14c03a52797751d24a98132aac0cd8), [Commit](https://github.com/open-webui/open-webui/commit/32a417bbf66ed28a5ca8a759ad3a87b2a2aa358a)
|
||||
- 🧰 **Filesystem tool for knowledge bases.** A new built-in tool, enabled via the "ENABLE_KB_EXEC" environment variable, lets AI models browse and search knowledge base contents using familiar filesystem commands such as 'ls', 'cat', 'grep', 'find', 'head', 'tail', and 'sed', including pipes between them. [Commit](https://github.com/open-webui/open-webui/commit/5b125c24d4eae925d3287efa626595bab29d5c33), [Commit](https://github.com/open-webui/open-webui/commit/ecec86dd32aa396dd5a383e9be5b0f16c4346c48), [Commit](https://github.com/open-webui/open-webui/commit/9ef579ce4be5b82f9281b085a621fed5b04e0d26), [Commit](https://github.com/open-webui/open-webui/commit/2f642754ac6b8c430228a7fc289357a6c6652235), [Commit](https://github.com/open-webui/open-webui/commit/3b00e5721a60518ea317c4fd61a6d0d182961a2f), [Commit](https://github.com/open-webui/open-webui/commit/4e78b355efe0611f27fddef591c69f7c1259a9f6), [Commit](https://github.com/open-webui/open-webui/commit/74f95a9b0d6b89b241701422170d54e21e701baa), [Commit](https://github.com/open-webui/open-webui/commit/cc16e06c32de48bdf464db5814483a49f3527b63), [Commit](https://github.com/open-webui/open-webui/commit/1ea54c3217f487990b6a4c0f0d8ea675a479b6e1)
|
||||
- ✏️ **File renaming in knowledge bases.** Files inside a knowledge base can now be renamed directly from the workspace, with the new name reflected wherever the file is referenced. [Commit](https://github.com/open-webui/open-webui/commit/3127f1b46255626ea92eb0b598e45078287303f1)
|
||||
- 😀 **Emoji picker in message input.** A new emoji button in the rich text formatting toolbar lets you browse and insert emojis directly into your messages. [#24704](https://github.com/open-webui/open-webui/pull/24704)
|
||||
- 🪄 **Per-chat skills toggle.** Skills can now be turned on or off for a conversation directly from the chat Integrations menu, the same way tools and capabilities already work, instead of only through the model preset. [#25036](https://github.com/open-webui/open-webui/issues/25036), [#25037](https://github.com/open-webui/open-webui/pull/25037)
|
||||
- 🔎 **Access preview for users and groups.** Administrators can now preview exactly which models, knowledge bases, and tools a given user or group can access, making it easier to audit and verify permission setups. [Commit](https://github.com/open-webui/open-webui/commit/9c14740ffb009d550dcbd5d6c599dac57053f112)
|
||||
- 📄 **Configurable knowledge base file page size.** Administrators can now request a larger page size when listing a knowledge base's files through the API, reducing the number of requests needed to retrieve large collections instead of paging through fixed increments of 30. [#25148](https://github.com/open-webui/open-webui/issues/25148), [Commit](https://github.com/open-webui/open-webui/commit/a4d1b3e9378a61a11dd9822a8dbb525f39753081)
|
||||
- 🔃 **Persistent processing indicator for knowledge files.** Files still being processed in a knowledge base now keep showing a processing indicator across page reloads, so you can tell what's still ingesting after navigating away and back. [#25031](https://github.com/open-webui/open-webui/issues/25031), [Commit](https://github.com/open-webui/open-webui/commit/ad9f2eeb15a448147d5e47e6a391cabb9aefd0ea)
|
||||
- 📑 **MinerU file type configuration.** Administrators can now configure which file types are processed by the MinerU document loader, via the new "MINERU_FILE_EXTENSIONS" setting, extending it beyond PDF to formats like DOCX, PPTX, and XLSX. [Commit](https://github.com/open-webui/open-webui/commit/d4030a8aa5d48c2a1cb06c461566844aca2530ab)
|
||||
- 📃 **Legacy Word document support.** Older ".doc" Word files can now have their text extracted by the default document extraction engine, in addition to the modern ".docx" format. [Commit](https://github.com/open-webui/open-webui/commit/9e3e24e304f8ff210494380b46f614a2984cafb3)
|
||||
- 📁 **Create subfolders from the folder header.** Chat folders can now have subfolders created directly from the folder header in the chat view, not just from the sidebar. [Commit](https://github.com/open-webui/open-webui/commit/1f0948bcbef2af73b155535ea27762c522260afc)
|
||||
- ⚡ **Faster initial page loads.** The configuration endpoint that loads on every page visit no longer runs an unnecessary user-count query, making the initial application load lighter on the database, especially on instances with many users. [Commit](https://github.com/open-webui/open-webui/commit/0adc090dcbe636c9c9645d8ec7f6b89ea514870b)
|
||||
- 🚀 **Faster tool-enabled chat completions.** Chat completions that use multiple tools now start faster because the tools they reference are fetched from the database in a single batch query instead of one query per tool. [#24808](https://github.com/open-webui/open-webui/pull/24808), [Commit](https://github.com/open-webui/open-webui/commit/cc94a90b4d4d690bc7cb9f7124f2d6e552973970)
|
||||
- 🏎️ **More responsive web search under load.** Web search through SearXNG, Google PSE, Brave, Serper, and Serpstack now uses non-blocking network calls, so the server stays responsive to other users while a search is in flight, and concurrent multi-query searches complete faster. [Commit](https://github.com/open-webui/open-webui/commit/b94245d2ee191e8ef118bf9de1ff7539503bfec9)
|
||||
- 🐎 **Lighter Ollama backend connections.** Requests to Ollama backends now reuse a shared connection pool instead of opening a fresh session each time, reducing TCP and TLS handshake overhead for installs that poll Ollama frequently or have multiple backends configured. [Commit](https://github.com/open-webui/open-webui/commit/5d9a09a88a9094ebfcd249340be3aaee544b34d0)
|
||||
- 💽 **Fewer redundant model-list writes.** On multi-instance deployments backed by Redis, the model list is no longer rewritten when it hasn't changed, cutting a major source of redundant writes. [#25469](https://github.com/open-webui/open-webui/issues/25469), [#25474](https://github.com/open-webui/open-webui/pull/25474), [Commit](https://github.com/open-webui/open-webui/commit/fd76b51ab2ad4c4192f2c98153e47888712c2009)
|
||||
- 📉 **Faster websocket disconnect cleanup.** Disconnecting from a collaborative session no longer triggers a scan across the entire Redis keyspace, using a per-session index instead, which keeps disconnects cheap on large deployments. [#25466](https://github.com/open-webui/open-webui/issues/25466), [Commit](https://github.com/open-webui/open-webui/commit/c7de057a4a54cc80f366ad949417e78fae756d4c)
|
||||
- 📝 **Frontmatter auto-fill for tools, functions, and skills.** Opening a tool, function, or skill editor now auto-fills the name, id, and description fields from the file's frontmatter, saving you from re-entering metadata already declared in the source. [#24649](https://github.com/open-webui/open-webui/pull/24649), [Commit](https://github.com/open-webui/open-webui/commit/ef975649b26d3e7cd589c49be2fc77cee80fad8f)
|
||||
- 🪪 **More user placeholders in custom headers.** Custom-header templates for direct connections and tool servers now support "{{USER_EMAIL}}" and "{{USER_ROLE}}" alongside the existing user and session placeholders. [Commit](https://github.com/open-webui/open-webui/commit/ed73ef3d8df988b0e9646b82df5b1a453202ef8d)
|
||||
- ⏱️ **Configurable MCP connection timeout.** The timeout for the initial handshake with an MCP tool server is now configurable via the new "MCP_INITIALIZE_TIMEOUT" setting, so servers that are slow to start or expose many tools can finish connecting instead of timing out. [#25011](https://github.com/open-webui/open-webui/pull/25011), [Commit](https://github.com/open-webui/open-webui/commit/4297c02b121180e239a61c483ac8477cc557d4ef)
|
||||
- 📐 **Profile image size limit.** Administrators can now cap the size of inline profile images via the new "PROFILE_IMAGE_MAX_DATA_URI_SIZE" setting, bounding how much database and cache space inline avatars and model icons can consume. [#25468](https://github.com/open-webui/open-webui/issues/25468), [#25476](https://github.com/open-webui/open-webui/pull/25476)
|
||||
- 🎫 **Wildcard OAuth role mapping.** Administrators can now set "\*" in the allowed OAuth roles to grant the user role to any authenticated OAuth user, instead of having to enumerate every accepted role. [#25062](https://github.com/open-webui/open-webui/pull/25062), [Commit](https://github.com/open-webui/open-webui/commit/07cbc91a8eba3a9a3b39588b2ae5de916930af70)
|
||||
- 📊 **Paginated feedback history.** The feedback and evaluation history list is now paginated, keeping it responsive for instances that have accumulated large numbers of feedback entries. [Commit](https://github.com/open-webui/open-webui/commit/160a6694e4bd66fc42e6361947516e8e15414ef3)
|
||||
- 🔘 **Bulk enable or disable automations.** Automations can now be enabled or disabled in bulk from an actions menu on the automations page, instead of toggling each one individually. [Commit](https://github.com/open-webui/open-webui/commit/675e9bee5af8b9fb390fcb235a6c2c4766e78c75)
|
||||
- ➡️ **Optional auto-redirect to single sign-on.** Administrators can now enable "OAUTH_AUTO_REDIRECT" so that, on deployments with a single sign-on provider and no other login methods, users are sent straight to the provider instead of seeing a login page first. [#25067](https://github.com/open-webui/open-webui/pull/25067), [Commit](https://github.com/open-webui/open-webui/commit/d64ef1803d2f5cedb7b3a308151d08ff2cd2b8e1)
|
||||
- ☁️ **Azure AI Foundry v1 with Entra ID.** Open WebUI now supports Azure AI Foundry's OpenAI v1 endpoint together with Microsoft Entra ID authentication, so these connections work without manual workarounds. [#24761](https://github.com/open-webui/open-webui/issues/24761), [#24985](https://github.com/open-webui/open-webui/pull/24985), [Commit](https://github.com/open-webui/open-webui/commit/eb4eebc3ce1042cb0d393bf890c1895db6e08b19)
|
||||
- 🌎 **Linkup web search provider.** Administrators can now select Linkup as the web search provider from the admin settings, with options to configure the API key and search depth. [#24752](https://github.com/open-webui/open-webui/pull/24752), [Commit](https://github.com/open-webui/open-webui/commit/56c0d00e13c74d665124ec9f1cae78e83ca60b6a)
|
||||
- 🧊 **Valkey vector database support.** Valkey can now be used as the vector database backend, configurable through new "VALKEY_URL" and related settings including index type, distance metric, and HNSW tuning. [#24769](https://github.com/open-webui/open-webui/pull/24769), [Commit](https://github.com/open-webui/open-webui/commit/c0f1aa291938bbef62db8d4013dbc498b0abea15)
|
||||
- 🔄 **General improvements.** Various improvements were implemented across the application to enhance performance, stability, and security.
|
||||
- 🌐 **Translation updates.** Translations for Spanish (Spain), Swedish, German, Korean, Catalan, Russian, Irish, Simplified Chinese, Traditional Chinese, Finnish, Polish, Turkish, and Malay were enhanced and expanded.
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🛡️ **Security Advisory**: This release includes security and access-control fixes. We recommend updating production deployments at your earliest convenience. Not all security fixes in this version may be enumerated in the fixed section — some may be withheld for a short time to give administrators time to upgrade. [Advisories](https://github.com/open-webui/open-webui/security)
|
||||
- 🛡️ **Tool server permission enforcement.** The per-user permission for inline tool servers is now enforced on chat-completion requests, so users without that permission can no longer bypass the admin setting by supplying tool servers directly in their requests. [Commit](https://github.com/open-webui/open-webui/commit/5cc1eb517094e3507915664817285f1e6e37a16d)
|
||||
- 🔒 **Knowledge base access check in search tool.** The built-in knowledge search tool now verifies that the caller can access a knowledge base before searching it by id, preventing users from reading the contents of knowledge bases they have not been granted access to. [#25113](https://github.com/open-webui/open-webui/pull/25113)
|
||||
- 🗄️ **Cross-user access to retrieval collections.** Resolving the documents used for retrieval now verifies the caller's access to each referenced file and rejects client-supplied collection names, preventing a crafted request from pulling another user's files or vector collections into its context. [Commit](https://github.com/open-webui/open-webui/commit/ee47c9c833f8889f3abe99d2266c56e8f8d40230)
|
||||
- 🔣 **Collection name validation.** Vector collection names are now rejected unless they contain only safe characters, preventing malformed names from reaching the vector store or breaking out of a database query expression. [#24982](https://github.com/open-webui/open-webui/pull/24982)
|
||||
- 🚫 **Unscoped retrieval collections denied by default.** Retrieval requests for collection names that don't correspond to a known file, memory, web-search, or knowledge base are now denied for non-admins by default, with a new "ENABLE_RETRIEVAL_UNSCOPED_COLLECTIONS" setting to restore the previous behavior if needed. [Commit](https://github.com/open-webui/open-webui/commit/c93f071700520a60833b7f9616c292b11f210880)
|
||||
- 📜 **Prompt history authorization.** Comparing, deleting, and restoring prompt versions now verify the history entry belongs to the prompt you're authorized for, preventing access to or modification of another prompt's version history. [#25056](https://github.com/open-webui/open-webui/pull/25056)
|
||||
- 🚦 **Code interpreter permission on the legacy path.** The legacy code-execution path now enforces the same permission and capability checks as the current one, so users without the code interpreter permission can no longer trigger code execution through it. [#24724](https://github.com/open-webui/open-webui/pull/24724)
|
||||
- 🧱 **API key endpoint restriction bypass.** The endpoint allow-list that limits which paths an API key may reach is now matched against the routed request path directly, preventing a crafted request from slipping past the restriction. [#25123](https://github.com/open-webui/open-webui/pull/25123)
|
||||
- 🚧 **System prompt bypass via request parameter.** The flag that skips a model's configured system prompt can no longer be set by external clients through a request parameter, so admin-configured system prompts can't be bypassed from the API. [#25156](https://github.com/open-webui/open-webui/pull/25156)
|
||||
- 🚪 **Terminal proxy path traversal.** The terminal proxy now fully decodes request paths before validating them, blocking multi-encoded payloads that could otherwise escape the intended path. [#25157](https://github.com/open-webui/open-webui/pull/25157)
|
||||
- 🪤 **Cache file path traversal.** The cache file server now requires an exact directory boundary match, closing a gap where a sibling directory whose name began with the cache directory's name could be used to serve files from outside it. [#25086](https://github.com/open-webui/open-webui/pull/25086)
|
||||
- 🔀 **Ollama backend selection access check.** Requests can no longer target an arbitrary Ollama backend by index; a caller-supplied backend selector is now verified against the backends that actually serve the requested model. [Commit](https://github.com/open-webui/open-webui/commit/7139797be04030b9d1016782f1dbb251c5fe68bb)
|
||||
- 🔓 **Cross-user file exfiltration via image URLs.** When a chat message references a file by id in an "image_url" field, the server now resolves that file only for its owner, an administrator, or a user with an explicit read grant, preventing other authenticated users from extracting a file's contents by routing it through the model. [#24625](https://github.com/open-webui/open-webui/pull/24625), [Commit](https://github.com/open-webui/open-webui/commit/c75fe8e74b72617c51282cc3ea0a2e8d9cdd9140)
|
||||
- 📌 **Chat file attachment access checks.** Attaching files to a chat now links only files the caller can read, preventing a user from associating another user's file with their chat to access its contents. [#25054](https://github.com/open-webui/open-webui/pull/25054)
|
||||
- 🧾 **Model knowledge file ownership checks.** Creating or updating a model now verifies that any knowledge files attached to it are files the editor can access, preventing another user's files from being attached to a model. [#25055](https://github.com/open-webui/open-webui/pull/25055), [Commit](https://github.com/open-webui/open-webui/commit/27fb20c13a4bf8501f7a485abe0654eb5880980d)
|
||||
- 📅 **Calendar event move authorization.** Updating a calendar event to move it into a different calendar now requires write access on the destination calendar, preventing users from injecting events into calendars they cannot write to. [#24764](https://github.com/open-webui/open-webui/pull/24764)
|
||||
- 📣 **Channel chat access control.** Generating a response in a channel context now verifies the caller's access to that channel and scopes the included messages, preventing access to channels or messages the user isn't permitted to see. [#24725](https://github.com/open-webui/open-webui/pull/24725)
|
||||
- 🕸️ **Web loader SSRF gating with Playwright.** When the Playwright-based web loader is in use, page navigations and redirects are now validated the same way as the default loader, closing a gap where the Playwright path could reach internal or otherwise blocked URLs. [#24756](https://github.com/open-webui/open-webui/pull/24756)
|
||||
- 🛂 **DNS rebinding protection for URL fetches.** The IP address validated for an outbound URL fetch is now the same one used for the actual connection, closing a DNS rebinding window where an attacker-controlled hostname could resolve to a public IP during the safety check and then to a private IP when the connection was opened. [#24759](https://github.com/open-webui/open-webui/pull/24759)
|
||||
- 🪞 **OAuth profile picture redirect handling.** The OAuth profile picture fetch now follows redirects only when administrators have explicitly allowed it, closing a window where a redirect from an externally validated URL could be used to reach internal addresses. [#24809](https://github.com/open-webui/open-webui/pull/24809)
|
||||
- 🧼 **Model profile image script injection.** Model profile images are now validated on save and only served inline when they are a known-safe image type, preventing a crafted SVG profile image from running scripts in other users' browsers, while existing legacy images that fail validation are cleared gracefully instead of breaking the model list. [#25060](https://github.com/open-webui/open-webui/pull/25060), [#25173](https://github.com/open-webui/open-webui/pull/25173)
|
||||
- 🧯 **Diagram rendering script injection.** Mermaid diagrams rendered in chat are now sanitized before display, preventing a crafted diagram from running scripts in the viewer's browser. [#25219](https://github.com/open-webui/open-webui/pull/25219)
|
||||
- 🔐 **Shared-chat file write protection.** Access to a file through a shared chat now only grants read access, so users who can read a shared chat can no longer modify or delete files attached to it. [#24755](https://github.com/open-webui/open-webui/pull/24755)
|
||||
- 🔏 **Cross-origin embed prompt control.** When Open WebUI is embedded in an iframe on a different origin, the embedding page can now only drive the chat input or submit prompts if the user has explicitly opted in via the "iframe Sandbox Allow Same Origin" setting, preventing untrusted host pages from triggering confirmation dialogs or controlling the chat. [#24767](https://github.com/open-webui/open-webui/pull/24767), [Commit](https://github.com/open-webui/open-webui/commit/eb3076c1b02d90c2ce6e6d3beb08a37987c740ec)
|
||||
- 🗂️ **Chat folder ownership checks.** Creating a chat or updating a chat's folder now verifies the referenced folder belongs to the current user, preventing chats from being associated with folders owned by other people. [#24588](https://github.com/open-webui/open-webui/pull/24588)
|
||||
- 🧩 **Chat recovery from corrupted history.** Chats whose internal message graph was left in a malformed state by a failed regeneration now open and load correctly, with missing roles, parent references, and current-message pointers reconstructed automatically instead of breaking the chat. [#24424](https://github.com/open-webui/open-webui/issues/24424), [#24157](https://github.com/open-webui/open-webui/issues/24157), [#20474](https://github.com/open-webui/open-webui/issues/20474), [#24799](https://github.com/open-webui/open-webui/pull/24799), [Commit](https://github.com/open-webui/open-webui/commit/d310a0777c4c48ec772bdc9a510005d5e91b09c7)
|
||||
- 📨 **Imported chats with folders appear correctly.** Importing grouped chats no longer leaves them invisible when a referenced folder is missing; such chats now appear in the chat list instead of being silently orphaned. [#24910](https://github.com/open-webui/open-webui/issues/24910), [Commit](https://github.com/open-webui/open-webui/commit/7f7cd210186cb6a67e028c17974eb210fc7ba9fd)
|
||||
- 🎟️ **MCP tool server sessions stay connected.** OAuth-authenticated MCP tool server sessions are no longer mistakenly refreshed and deleted by the single sign-on session handler, so those connections stay active. [#24618](https://github.com/open-webui/open-webui/issues/24618), [Commit](https://github.com/open-webui/open-webui/commit/c8eb8edca4174ec68fabd071d6b08c0bc07f8117)
|
||||
- 🤝 **MCP OAuth scope discovery.** The OAuth flow for MCP tool servers now reads the scopes a server advertises through its Protected Resource Metadata, so connecting to servers that declare their own scopes succeeds. [#24730](https://github.com/open-webui/open-webui/issues/24730), [#24690](https://github.com/open-webui/open-webui/pull/24690)
|
||||
- 🔍 **Web search reliability.** Web search again fetches page content reliably with the default web loader engine, a new "USER_AGENT" environment variable lets administrators set a real browser user-agent so fetches aren't blocked by Cloudflare, Wikipedia, and other bot-detection systems, and the startup script no longer fails to launch when these new environment variables are unset. [#24560](https://github.com/open-webui/open-webui/issues/24560), [#24793](https://github.com/open-webui/open-webui/issues/24793), [#24683](https://github.com/open-webui/open-webui/pull/24683), [Commit](https://github.com/open-webui/open-webui/commit/f60733758272c4532cd032e518c9cd73f648043a)
|
||||
- 🔥 **Firecrawl web search results.** Web search using Firecrawl now returns results correctly regardless of which response format the Firecrawl version uses. [#24712](https://github.com/open-webui/open-webui/pull/24712)
|
||||
- 🦅 **Kagi web search.** Web search using Kagi works again after its API endpoint and request method were updated to match Kagi's current API. [#25015](https://github.com/open-webui/open-webui/pull/25015)
|
||||
- 🔢 **Bracketed numbers in code blocks.** Numbers in square brackets such as "[0]" inside code blocks are no longer stripped out as if they were source citations, so code displays and copies correctly. [#24948](https://github.com/open-webui/open-webui/issues/24948), [Commit](https://github.com/open-webui/open-webui/commit/e90a618f4555cea2c024bb60c1332ca04eed96da)
|
||||
- 🔌 **API chat completions reliability.** Direct calls to the chat completions API no longer fail with an internal error when no chat session identifier is supplied. [#24553](https://github.com/open-webui/open-webui/issues/24553), [#25235](https://github.com/open-webui/open-webui/issues/25235), [Commit](https://github.com/open-webui/open-webui/commit/bc244fdc90504824b76654880898bf3f6649c299), [Commit](https://github.com/open-webui/open-webui/commit/f16b5c446027eae1bd767617bac2fdf54b24d6fc)
|
||||
- 🖼️ **ComfyUI image generation and editing.** Generating and editing images via a ComfyUI backend now works again, including when ComfyUI is hosted on a private or internal network where URL validation was previously blocking the admin-configured endpoint. [#24565](https://github.com/open-webui/open-webui/issues/24565), [Commit](https://github.com/open-webui/open-webui/commit/7dcd932ad7cb5feab007737c6e0281def5cd47fc), [Commit](https://github.com/open-webui/open-webui/commit/8aa2a42dc7887512e697ff85d044220945ced40f)
|
||||
- 🖌️ **Image generation with non-standard response headers.** Image generation now works with backends that return valid JSON without a standard content-type header, instead of rejecting the response. [#24838](https://github.com/open-webui/open-webui/pull/24838)
|
||||
- 🐘 **Knowledge search on large documents.** Searching knowledge bases on PostgreSQL no longer fails when scanning across documents with very large extracted text content. [#24670](https://github.com/open-webui/open-webui/issues/24670), [Commit](https://github.com/open-webui/open-webui/commit/d74ee34d9128295faa116c919bd1dcca77744975)
|
||||
- 💬 **Chat title generation.** Automatically generated chat titles now use the model currently selected in the dropdown for the active chat and fall back to the model from the active message branch otherwise, and a clear message is shown if no model is available instead of an unhelpful error. [#24604](https://github.com/open-webui/open-webui/issues/24604), [#24745](https://github.com/open-webui/open-webui/issues/24745), [Commit](https://github.com/open-webui/open-webui/commit/e5c8f8110a88739e011d8ab23e5cb11b4768469f), [Commit](https://github.com/open-webui/open-webui/commit/3c5e7968f0b5130890e17f1f2a2a52297cb145a6)
|
||||
- 🧮 **Message search and analytics consistency.** Edits, deletions, and branch changes made in a chat are now reflected in message search results and analytics counts instead of leaving stale entries behind. [#25205](https://github.com/open-webui/open-webui/pull/25205), [Commit](https://github.com/open-webui/open-webui/commit/aa06200f789a4c3fc54ea9a141cd347d76cda4c5)
|
||||
- 🩹 **Graceful handling of in-chat task failures.** When web search query generation, image prompt generation, or a tool call fails or references a missing tool, the chat now falls back or surfaces a clear error instead of breaking partway through the response. [#25038](https://github.com/open-webui/open-webui/issues/25038), [#25144](https://github.com/open-webui/open-webui/issues/25144), [Commit](https://github.com/open-webui/open-webui/commit/b64fd988f02b8a4295193892b24b00bdf635a4dc)
|
||||
- 🎛️ **Filter changes to message output.** Filter functions that modify a message's structured output after generation now have those changes saved and displayed, instead of being discarded when only the output, not the text content, was changed. [#24884](https://github.com/open-webui/open-webui/pull/24884)
|
||||
- ⏩ **Titles and tags reflect filtered output.** Outlet filters now run before automatic title, tag, and follow-up generation, so those are based on the final filtered message instead of the unfiltered version. [#24717](https://github.com/open-webui/open-webui/pull/24717)
|
||||
- 💾 **Action-replaced message content persists.** Message content replaced by an action function through its event emitter is now kept when the chat is saved, instead of reverting to the original after a page reload. [#24585](https://github.com/open-webui/open-webui/issues/24585), [#25485](https://github.com/open-webui/open-webui/pull/25485)
|
||||
- 🏷️ **Skill mentions in messages.** Mentioning a skill in a message now keeps the skill's name as readable text instead of removing it, and selecting a skill without typing anything no longer causes an error on providers that reject empty messages. [#24929](https://github.com/open-webui/open-webui/issues/24929), [Commit](https://github.com/open-webui/open-webui/commit/01810e32ad51305ca3247e8f83da95f6e6260f0c)
|
||||
- 🧹 **Usage timer cleanup on send failure.** The background usage-stats timer started during message generation is now always cleared, even when sending a message fails, preventing leaked timers from accumulating over a session. [#25478](https://github.com/open-webui/open-webui/pull/25478)
|
||||
- 🗑️ **Background tasks stop when a chat is removed.** Deleting or archiving a chat now cancels any in-flight generation or title and tag tasks for it, instead of leaving orphaned background work running. [#25050](https://github.com/open-webui/open-webui/pull/25050), [Commit](https://github.com/open-webui/open-webui/commit/778dba1d6b8dc3962163fa7bf9d802d9a07fba26)
|
||||
- ⌨️ **Responsive knowledge file search.** Searching for knowledge files in the chat picker and model knowledge selector now matches on file names by default instead of scanning the full extracted text of every document on each keystroke, keeping the search responsive on large deployments, with content search available as an explicit opt-in. [#25082](https://github.com/open-webui/open-webui/issues/25082), [#25119](https://github.com/open-webui/open-webui/pull/25119), [Commit](https://github.com/open-webui/open-webui/commit/591e0aafa1d5e21dcd9fa1279dada2076968b9cb)
|
||||
- 📥 **Document processing with empty embeddings.** Saving documents to the vector database no longer crashes when an embedding step returns no vectors, allowing the process to continue instead of failing the whole upload. [#25166](https://github.com/open-webui/open-webui/issues/25166)
|
||||
- 🔤 **Non-UTF-8 text and CSV uploads.** Text and CSV files saved in legacy encodings, including Latin-1, Windows-1252, and Chinese encodings such as GB18030, are now detected and loaded correctly instead of being rejected as binary or failing with an empty-content error. [#25172](https://github.com/open-webui/open-webui/issues/25172), [#24973](https://github.com/open-webui/open-webui/issues/24973), [Commit](https://github.com/open-webui/open-webui/commit/6f0277db52d005420480abb0702d421525d6ea8b), [Commit](https://github.com/open-webui/open-webui/commit/1bbb2b933d4cee70a5102eca4109e77b8625f8fc)
|
||||
- 🧽 **Null bytes in nested data no longer break saves.** Data containing null bytes nested inside structured fields is now sanitized correctly before being written, preventing database errors that the previous check failed to catch. [#25018](https://github.com/open-webui/open-webui/pull/25018), [Commit](https://github.com/open-webui/open-webui/commit/e3ab4bd212e44c39f439ec4ff5df7c3dbd046895)
|
||||
- 🧠 **Clear error when no embedding model is configured.** Using knowledge or retrieval features without a loaded embedding model now returns a clear setup error explaining what to configure, instead of failing with a cryptic crash. [Commit](https://github.com/open-webui/open-webui/commit/55ca719bbf76306ed647424c728f730d0bef21f9)
|
||||
- 🧲 **Memory search quality.** Memory searches now apply the configured embedding query prefix, so retrieval works correctly with embedding models that require one for queries. [#24921](https://github.com/open-webui/open-webui/pull/24921), [Commit](https://github.com/open-webui/open-webui/commit/ce4dca47cb19a6582fd8a550806c89ab297c038d)
|
||||
- 📚 **Knowledge tool context overflow.** The built-in tool that lists a model's knowledge no longer dumps every file in every knowledge base into the model's context; it now returns summaries by default and paginates file listings only for a requested knowledge base. [#25105](https://github.com/open-webui/open-webui/pull/25105), [Commit](https://github.com/open-webui/open-webui/commit/0e73f7af099e1f9437c55a5c2d16741c97dbd57f)
|
||||
- ⏳ **Terminal session stability.** The terminal proxy no longer hangs when one direction of the connection closes before the other, so terminal sessions shut down cleanly instead of stalling. [#25464](https://github.com/open-webui/open-webui/issues/25464), [#25479](https://github.com/open-webui/open-webui/pull/25479)
|
||||
- 🧷 **Tool call continuity with strict providers.** Chats that contain incomplete tool calls or orphaned tool results no longer fail to continue when sent to providers that strictly validate tool pairings, such as Anthropic and AWS Bedrock Converse. [#24758](https://github.com/open-webui/open-webui/issues/24758), [#24940](https://github.com/open-webui/open-webui/issues/24940), [#24798](https://github.com/open-webui/open-webui/pull/24798), [Commit](https://github.com/open-webui/open-webui/commit/cfa6908d579e1f7f202a321289029996130b8411)
|
||||
- 🛑 **Stream termination for pipe functions.** Streamed responses from pipe functions now always send the standard end-of-stream marker, so chat clients and external integrations reliably detect when a response is complete instead of waiting on streams that already finished. [#24763](https://github.com/open-webui/open-webui/pull/24763)
|
||||
- 🔊 **Non-blocking text-to-speech transcoding.** Converting text-to-speech audio to MP3 no longer blocks the server's event loop, so other requests stay responsive even while a TTS response is being transcoded. [#24876](https://github.com/open-webui/open-webui/pull/24876)
|
||||
- 🎚️ **Default text-to-speech voice.** Text-to-speech requests now honor the voice specified in the request and fall back to the configured default only when none is given, instead of always using the admin default or failing. [#15143](https://github.com/open-webui/open-webui/issues/15143), [#25035](https://github.com/open-webui/open-webui/issues/25035), [Commit](https://github.com/open-webui/open-webui/commit/f16b5c446027eae1bd767617bac2fdf54b24d6fc), [Commit](https://github.com/open-webui/open-webui/commit/750604a11d4adcb5ae568b9fc93010031ad394f9)
|
||||
- 🪝 **Reliable knowledge base file linking.** Files uploaded to a knowledge collection are now linked on the server as part of the upload itself, so they remain attached to the collection even if you navigate away or close the page before processing finishes. [#24807](https://github.com/open-webui/open-webui/issues/24807), [Commit](https://github.com/open-webui/open-webui/commit/d0b17f056911ec73df2b686a0d92bb789391db27)
|
||||
- ☁️ **Azure connections on custom hostnames.** Connections marked as the Azure provider now use the Azure code path even when the endpoint does not contain "azure" in its hostname, fixing custom Azure deployments served from non-standard domains. [#24882](https://github.com/open-webui/open-webui/pull/24882), [Commit](https://github.com/open-webui/open-webui/commit/c8f851bd2de3d127f1e818fc116c27174f96d3f1)
|
||||
- 🗓️ **Clearing calendar event fields.** Removing the description or location from a calendar event now saves correctly instead of silently keeping the previous value. [#25026](https://github.com/open-webui/open-webui/issues/25026), [Commit](https://github.com/open-webui/open-webui/commit/91810f1c4e93d4f559bdf61cd085cadc288a732d), [Commit](https://github.com/open-webui/open-webui/commit/78b1637a035d71099262412e5dee3e4d65c7fb2f)
|
||||
- 💭 **Advanced parameter settings.** Custom reasoning tags and custom model parameters are now saved correctly instead of being dropped, and the presence penalty and repeat penalty no longer save the frequency penalty's value instead of their own. [#25183](https://github.com/open-webui/open-webui/pull/25183), [#25200](https://github.com/open-webui/open-webui/pull/25200), [#25204](https://github.com/open-webui/open-webui/pull/25204)
|
||||
- 📏 **Long username display.** Long usernames no longer overflow their containers in the admin user list, user modals, and sidebar. [#25185](https://github.com/open-webui/open-webui/pull/25185)
|
||||
- 🎯 **All skills selectable in the model editor.** The model editor's skills selector now lists every skill you have access to, with a search box for large lists, instead of showing only the first 30 with no way to reach the rest. [#24873](https://github.com/open-webui/open-webui/issues/24873), [Commit](https://github.com/open-webui/open-webui/commit/936d5f2676dfbd2ba763b30af33a8557dcda9b05)
|
||||
- 🔔 **Accurate knowledge upload feedback.** Dragging files into a knowledge base no longer shows an upload notification before the upload has actually been processed. [#25484](https://github.com/open-webui/open-webui/pull/25484)
|
||||
- ♿ **High-contrast timestamp readability.** The user message timestamp now uses the correct colors in high-contrast mode instead of inverted ones, keeping it readable. [#25461](https://github.com/open-webui/open-webui/pull/25461)
|
||||
- ♿ **Keyboard and screen reader access to menus.** The integrations, more-options, and user menus are now real buttons with labels and keyboard support, so they can be opened with the keyboard and announced by screen readers. [Commit](https://github.com/open-webui/open-webui/commit/346dab3d8f909fc321a49ea2be633ea5c4c4a349)
|
||||
- 🖱️ **Focus-loss handling in editors.** Workspace and admin editors for models, tools, functions, and skills again respond correctly when the browser window loses focus, after the wrong event name was being listened for. [#25459](https://github.com/open-webui/open-webui/pull/25459)
|
||||
- 🛟 **Resilience to corrupted local storage.** Corrupted data in the browser's local storage no longer crashes the interface; affected settings and dismissed-banner state now fall back to safe defaults. [#25481](https://github.com/open-webui/open-webui/pull/25481)
|
||||
- 📶 **Quieter reconnection notifications.** Brief connection interruptions, such as backgrounding a mobile tab, no longer flash a "connection lost" warning, and the "reconnected" message only appears if a disconnect was actually shown. [Commit](https://github.com/open-webui/open-webui/commit/77c8c54b1ea07189e21313f9cd401f3b956bb93a)
|
||||
- 🍎 **Safari PDF handling.** PDF processing now works in Safari, which doesn't support the stream iteration the previous code relied on. [#25151](https://github.com/open-webui/open-webui/issues/25151), [#25473](https://github.com/open-webui/open-webui/pull/25473)
|
||||
- 🎙️ **Voice mode mute shortcut listing.** The keyboard shortcut for muting voice mode now appears in the keyboard shortcuts help modal. [#25193](https://github.com/open-webui/open-webui/pull/25193)
|
||||
- 📎 **Document attachments in channel model replies.** Tagging a model in a channel thread now forwards uploaded non-image documents such as PDFs and DOCX files into the model's context, so document summarization and comparison workflows that already worked in direct chat now work in channels too. [#24896](https://github.com/open-webui/open-webui/issues/24896), [#24898](https://github.com/open-webui/open-webui/pull/24898), [Commit](https://github.com/open-webui/open-webui/commit/7e9d41d664d7065a92ac93606d18e88c45eb36b6)
|
||||
- 🙈 **Hidden models in channel mentions.** Models marked as hidden no longer appear in the channel message-input model mention selector, matching how hidden models are excluded elsewhere in the interface. [#24892](https://github.com/open-webui/open-webui/pull/24892)
|
||||
- 🧵 **Channel thread and pinned message stability.** Opening a channel thread or the pinned messages view no longer fails to render when a message or its data is missing. [#25209](https://github.com/open-webui/open-webui/pull/25209)
|
||||
- 📺 **YouTube short link transcripts.** Pasting a "youtu.be" short link into a chat now loads the video transcript correctly instead of failing with an empty-content error. [#24856](https://github.com/open-webui/open-webui/issues/24856), [Commit](https://github.com/open-webui/open-webui/commit/1e36a206008c3abce5ccdc98d5750e30a7345b98)
|
||||
- 🙉 **Hidden models in default-model and automation pickers.** The admin pickers for default models and default pinned models, and the automation model dropdown, now filter out hidden models, consistent with how hidden models are treated elsewhere. [#24869](https://github.com/open-webui/open-webui/issues/24869), [Commit](https://github.com/open-webui/open-webui/commit/1fa3050f069a72de1daaaa29233b545c4deaea51), [Commit](https://github.com/open-webui/open-webui/commit/4705c2d98812c1189c2ae4960399bc8888d4861c)
|
||||
- 🔊 **Speech-to-text SSL setting honored.** Speech-to-text requests now respect the "AIOHTTP_CLIENT_SESSION_SSL" setting, so administrators using self-signed certificates or custom SSL configurations can use STT engines that were previously failing TLS verification. [#24568](https://github.com/open-webui/open-webui/issues/24568), [#24857](https://github.com/open-webui/open-webui/pull/24857), [Commit](https://github.com/open-webui/open-webui/commit/2ca91ceeeca6a6a21e5a6ad68ea3ab8c9d9f6deb), [Commit](https://github.com/open-webui/open-webui/commit/94b66b17972e0ad77954df4b81a6bb86a7a6a04b)
|
||||
- 🔗 **Placeholders in MCP connection headers.** Custom header templates configured on MCP server connections now have their "{{USER_ID}}", "{{USER_NAME}}", "{{USER_EMAIL}}", "{{USER_ROLE}}", "{{CHAT_ID}}", and "{{MESSAGE_ID}}" placeholders interpolated at request time, matching how custom headers already work for direct connections and tool servers. [#24822](https://github.com/open-webui/open-webui/pull/24822)
|
||||
- 🪟 **Bing search CLI smoke test.** Running the Bing web-search module from the command line for a quick connectivity check no longer raises an error about missing arguments. [#24765](https://github.com/open-webui/open-webui/issues/24765), [#24768](https://github.com/open-webui/open-webui/pull/24768)
|
||||
- 🩺 **Database health check recovery.** After a transient database connection error, the health check endpoint now recovers automatically instead of staying permanently broken on the affected worker. [Commit](https://github.com/open-webui/open-webui/commit/0b81520e072bb4ed15532b8e153b43dd3243feaf)
|
||||
- 🥾 **Startup on non-Unicode consoles.** Open WebUI no longer crashes at startup when the console can't encode the banner's box-drawing characters, such as on Windows or with redirected or headless output, falling back to a plain-text banner instead. [#24965](https://github.com/open-webui/open-webui/issues/24965), [#25482](https://github.com/open-webui/open-webui/pull/25482)
|
||||
- 🆕 **First admin signup after a reset.** Creating the first administrator account is no longer blocked by a previously stored signup setting, so a fresh or reset instance can always be bootstrapped. [#24821](https://github.com/open-webui/open-webui/pull/24821)
|
||||
- 🪵 **JSON exception logging.** With JSON log formatting enabled, exceptions are now recorded correctly with a structured type, message, and stacktrace instead of being dropped, and a logging failure can no longer crash the application. [#25135](https://github.com/open-webui/open-webui/issues/25135), [Commit](https://github.com/open-webui/open-webui/commit/79bf3d28d88e78b0136ef3c3c9f8e7bb85d3cea9)
|
||||
- 🧭 **Workspace skills permission.** Users granted only the "workspace.skills" permission can now see the workspace entry in the sidebar and are correctly routed to the skills page from the workspace index. [#24729](https://github.com/open-webui/open-webui/pull/24729)
|
||||
- 🔁 **Resilient database migrations.** Database migrations now skip tables, indexes, and columns that already exist and add missing primary keys to legacy tables, so upgrades succeed even when parts of the schema were manually or partially created beforehand. [Commit](https://github.com/open-webui/open-webui/commit/81f611fb73c726cfcfc20ee57a926a543f07e95f), [Commit](https://github.com/open-webui/open-webui/commit/f0e88dadc8502ea05b1a00f3155bee7d1cf32249), [Commit](https://github.com/open-webui/open-webui/commit/bd9f82d5a681ee94bde44325033447500a0c76bf), [Commit](https://github.com/open-webui/open-webui/commit/459b1c3fda2ec3579fbd2ab408d0fdeb07c96b99), [Commit](https://github.com/open-webui/open-webui/commit/6df09a4039d181f4e2d41324e93cd36c6fb27dfd), [Commit](https://github.com/open-webui/open-webui/commit/95840e307a66429168775de389b329487a4311c4), [Commit](https://github.com/open-webui/open-webui/commit/6b1df94bf933af92f5c7d093a2d92e2e50d6fdd0), [Commit](https://github.com/open-webui/open-webui/commit/98d3b2308564e2112290773ea240b90419ebb48d), [Commit](https://github.com/open-webui/open-webui/commit/1b9d22e324181b96511a15cbacc40fdbb439ad79), [Commit](https://github.com/open-webui/open-webui/commit/dc0f8ae6f2372b6e5dd42f3520813b09c54146c0), [Commit](https://github.com/open-webui/open-webui/commit/ee3b14233a642ff1bc38aef03a2418afcf5d6c1d), [Commit](https://github.com/open-webui/open-webui/commit/1004dad2749bcc369d511315a808d4bb5277854c), [Commit](https://github.com/open-webui/open-webui/commit/db2b3d7fd86cc7d76424671ead3c62a3f6302fe7), [Commit](https://github.com/open-webui/open-webui/commit/2e1b671e8db49f47d69fd59ece2b58e109ec8b84), [Commit](https://github.com/open-webui/open-webui/commit/9a8969ca93f8c9109d956518a085be3e2afacc07), [Commit](https://github.com/open-webui/open-webui/commit/9717ada92fdb3761a217d309f322ee9fbe418d0d), [Commit](https://github.com/open-webui/open-webui/commit/d7cfc1e46a8f3e5c8f6758c12ef641f41fb51a39), [Commit](https://github.com/open-webui/open-webui/commit/9263b7568eaa83f43e4a118c98490c4f7aaba570), [Commit](https://github.com/open-webui/open-webui/commit/73d2065227e651cf90a2cfade721516815743ab4), [#24722](https://github.com/open-webui/open-webui/pull/24722)
|
||||
|
||||
### Changed
|
||||
|
||||
- ⚠️ **Database Migrations**: This release includes database schema changes; we strongly recommend backing up your database and all associated data before upgrading in production environments. If you are running a multi-worker, multi-server, or load-balanced deployment, all instances must be updated simultaneously, rolling updates are not supported and will cause application failures due to schema incompatibility.
|
||||
- ⚙️ **Tool-call iteration cap renamed and raised.** The environment variable that limits how many tool calls a single chat response may make is now "CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS", with its default raised from 30 to 256 and a new "-1" value for unlimited; the previous "CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES" name continues to work as a fallback, and chats that hit the cap now show a clear error in-chat instead of stopping silently. [#24918](https://github.com/open-webui/open-webui/pull/24918), [Commit](https://github.com/open-webui/open-webui/commit/2b99945d2726bdf5aed7b1712f9e3b7b622671df)
|
||||
- 🔐 **Reduced public "/api/config" exposure.** The "/api/config" response no longer includes several feature flags ("enable_api_keys", "enable_password_change_form", "enable_version_update_check", "enable_public_active_users_count", "enable_easter_eggs") for unauthenticated callers, reducing information disclosure to anonymous visitors. [Commit](https://github.com/open-webui/open-webui/commit/245e0ee029e9e10617f62953a9e6f67dd00ecf81), [Commit](https://github.com/open-webui/open-webui/commit/ae06e199d5d3f65296a978cc35079bdacba596d2)
|
||||
- 🔑 **"WEBUI_SECRET_KEY" is now a hard requirement even for unsupported deployments.** Deployments that start the backend in an explicitly unsupported way (such as invoking uvicorn directly) without setting "WEBUI_SECRET_KEY" will now refuse to start instead of falling back to an empty key; the supported start methods (start.sh, start_windows.bat, and "open-webui serve") still set or auto-generate it automatically, so standard deployments are unaffected. Direct Uvicorn startup is not supported. [#25218](https://github.com/open-webui/open-webui/pull/25218)
|
||||
|
||||
## [0.9.5] - 2026-05-09
|
||||
|
||||
### Added
|
||||
|
||||
- 🛡️ **Redirect-based SSRF protection.** All outbound HTTP requests now block 3xx redirects by default via a new `AIOHTTP_CLIENT_ALLOW_REDIRECTS` environment variable, preventing redirect-based SSRF where a public URL silently redirects to internal addresses (RFC 1918, loopback, cloud-metadata endpoints). Affected call sites include web fetch, image loading, OAuth discovery, tool server execution, and code interpreter login. [#24491](https://github.com/open-webui/open-webui/pull/24491)
|
||||
- 🛡️ **Iframe content security policy.** Administrators can now configure a Content-Security-Policy for all srcdoc iframes (Artifacts, tool embeds, file previews, citation modals) via the `IFRAME_CSP` environment variable, restricting what LLM-generated or user-uploaded HTML can load and execute inside previews. [Commit](https://github.com/open-webui/open-webui/commit/3bba1c227059a44c7eeefa97b8c69a63bf4f3454)
|
||||
- 🎛️ **Granular markdown rendering controls.** Users can now independently disable Markdown rendering for user messages and assistant responses from Interface settings, preventing unintended formatting when pasting text that contains Markdown-sensitive characters. [Commit](https://github.com/open-webui/open-webui/commit/4a1064cefd6f48a8b3b02cd31f77838c8802b635)
|
||||
- 🔧 **Terminal proxy response headers.** Administrators can now inject custom response headers into terminal proxy responses via the `TERMINAL_PROXY_HEADERS` environment variable (JSON object), enabling deployment-specific security headers like sandbox policies for proxied content. [Commit](https://github.com/open-webui/open-webui/commit/8d3133fe2835122bffaa4f2ce584730bc9c78981)
|
||||
- 🔌 **Channel streaming and tool support.** Mentioning a model in a Channel now streams responses in real time and supports the full chat completion pipeline, including native and default function calling, built-in tools (web search, image generation), user tools, MCP tools, filters, and RAG knowledge injection — the same capabilities available in standard chats.
|
||||
|
||||
### Fixed
|
||||
|
||||
- 📝 **Notes create and open reliability.** Creating new notes and opening existing notes no longer fails with a TypeError caused by `is_pinned` being passed to the SQLAlchemy model on create, and passed twice to `NoteResponse` on read. [#24484](https://github.com/open-webui/open-webui/issues/24484), [#24486](https://github.com/open-webui/open-webui/pull/24486)
|
||||
- 🔐 **Skill public sharing permission enforcement.** Creating or updating skills now filters access grants through the `sharing.public_skills` permission, preventing non-admin users from making skills publicly accessible without the required permission. [#24494](https://github.com/open-webui/open-webui/pull/24494)
|
||||
- 🔐 **Calendar public sharing permission enforcement.** Creating or updating calendars now filters access grants through a new `sharing.public_calendars` permission, preventing users from making calendars publicly readable or writable without explicit admin-granted sharing permission. [#24493](https://github.com/open-webui/open-webui/pull/24493)
|
||||
- 🔐 **Feedback user attribution spoofing.** Submitting evaluation feedback can no longer forge the `user_id` field through mass-assignment, preventing authenticated users from attributing ratings to other users and corrupting Elo leaderboard rankings and admin feedback exports. [#24508](https://github.com/open-webui/open-webui/pull/24508)
|
||||
- 🛡️ **Image URL redirect-based SSRF.** Chat messages containing image URLs no longer follow 3xx redirects to internal addresses during base64 conversion, closing the most reachable redirect-based SSRF variant that required no special permissions or feature flags. [#24524](https://github.com/open-webui/open-webui/pull/24524)
|
||||
- 🛡️ **Collection write access on file processing.** The `process_file` and `process_files_batch` retrieval endpoints now enforce collection write-access checks before embedding content, preventing authenticated users from injecting file content into another user's knowledge-base collection. [#24524](https://github.com/open-webui/open-webui/pull/24524)
|
||||
- 🔐 **Tool source code update authorization.** Updating a tool's Python source code now requires `workspace.tools` or `workspace.tools_import` permission, preventing users with only a write-access grant from overwriting executable tool code while still allowing metadata edits. [#24513](https://github.com/open-webui/open-webui/pull/24513)
|
||||
- 🔐 **Channel message ownership enforcement.** Updating or deleting messages in group and DM channels now requires message ownership, preventing channel members from tampering with or silently removing other members' messages. [#24506](https://github.com/open-webui/open-webui/pull/24506)
|
||||
- 🔐 **Channel pin write permission.** Pinning and unpinning messages on standard channels now requires write permission instead of read permission, preventing read-only users from modifying pinned content. [#24521](https://github.com/open-webui/open-webui/pull/24521)
|
||||
- 🛡️ **Image generation URL validation.** Generated image URLs are now validated through `validate_url()` before fetching, aligning the defense-in-depth posture with sibling image-loading paths. [#24518](https://github.com/open-webui/open-webui/pull/24518)
|
||||
- 🔐 **Model params exposure for read-only users.** The per-model API endpoint now strips the `params` dict (including system prompts) from responses to callers without write access, preventing read-only users from viewing admin-curated model configuration. [#24525](https://github.com/open-webui/open-webui/pull/24525)
|
||||
- 🛡️ **URL parser SSRF bypass.** URL validation now rejects backslash, tab, CR, and LF characters that cause urllib and requests/aiohttp to disagree on the target host, closing a parser-confusion SSRF bypass. [#24534](https://github.com/open-webui/open-webui/pull/24534)
|
||||
- 🛡️ **Profile image MIME-type allowlist.** Serving profile images from data URIs now enforces a strict MIME-type allowlist (PNG, JPEG, GIF, WEBP by default, configurable via `PROFILE_IMAGE_ALLOWED_MIME_TYPES`) and sets `X-Content-Type-Options: nosniff`, preventing stored-XSS through SVG or other executable content types. [Commit](https://github.com/open-webui/open-webui/commit/15e696691cad98692c329de62ed8a5bdb3a26d4e)
|
||||
- 🔐 **File ownership in folder and knowledge attachments.** Attaching files to folders or knowledge bases now verifies per-file read access, and folder file lists in chat middleware are filtered to entries the caller can read, preventing unauthorized file content from being injected into RAG context. [Commit](https://github.com/open-webui/open-webui/commit/2dbf7b6764a7922458d3b0139687ad6dcd7596d9)
|
||||
- 🔐 **Shared chat access for owners and admins.** Chat owners can now view and clone their own shared chats without requiring an explicit access grant, and administrators can manage shared chat access controls on any chat. [Commit](https://github.com/open-webui/open-webui/commit/3a21b334cce30226750c5c537345dc51bb8bef17), [Commit](https://github.com/open-webui/open-webui/commit/315566064aedeff071854b023d09e5f1ea0eb950)
|
||||
- 🧵 **Legacy chat history self-healing.** Loading legacy conversations now automatically detects broken parent-link graphs in migrated message records, merges missing messages from the embedded JSON history, and backfills them to the normalized table so future loads use the fast path without data loss. [Commit](https://github.com/open-webui/open-webui/commit/1388f4568b8f508c26542673dd01f1fa049e798a)
|
||||
- 🎛️ **Filter selector reactivity.** Model filter checkboxes now derive state reactively from the current filter list and selected IDs instead of capturing a one-time snapshot at mount, so checkboxes update correctly when model contexts or filter configurations change at runtime. [Commit](https://github.com/open-webui/open-webui/commit/d1ef5382377f590f97a6dbaee88f369e6d7c5f6f)
|
||||
- 🌐 **Portuguese (Brazil) translation updates.** Translations for newly added UI items were added along with a consistency pass across existing entries. [#24503](https://github.com/open-webui/open-webui/pull/24503)
|
||||
|
||||
### Changed
|
||||
|
||||
- 🧹 **Removed unauthenticated retrieval status endpoint.** The unauthenticated `GET /api/v1/retrieval/` status endpoint has been removed as dead code — retrieval configuration is already available through authenticated admin endpoints. [#24497](https://github.com/open-webui/open-webui/pull/24497)
|
||||
- 📋 **PR template issue requirement.** Pull requests now require a linked Issue or Discussion reference, ensuring better traceability for all contributions. PRs without a linked issue or discussion may be closed without review.
|
||||
|
||||
## [0.9.4] - 2026-05-09
|
||||
|
||||
### Fixed
|
||||
|
||||
- 📜 **Chat scroll position on load.** Opening a chat conversation now reliably scrolls to the bottom of the message history, fixing a regression caused by `content-visibility: auto` where estimated element sizes prevented the initial scroll from reaching the true bottom.
|
||||
|
||||
## [0.9.3] - 2026-05-09
|
||||
|
||||
### Added
|
||||
|
||||
- 🔇 **Voice Mode mute control.** Voice Mode now includes a dedicated mute toggle with an "M" shortcut and auto-unmute after assistant playback, so you can prevent accidental interruptions from background noise without leaving the call overlay. [Commit](https://github.com/open-webui/open-webui/commit/072d2000f35a9f7b96342fa9bb28f925a92e7b4c), [#23832](https://github.com/open-webui/open-webui/issues/23832)
|
||||
- 🚀 **Faster prompt list loading.** Prompt and prompt-tag pages now load much faster for non-admin users, even with large prompt libraries, because accessible prompts are filtered efficiently in a single database query. [#24288](https://github.com/open-webui/open-webui/pull/24288), [#24258](https://github.com/open-webui/open-webui/discussions/24258)
|
||||
- ⚡ **Faster chat history loading.** Chat history maps now load from normalized message records when available, reducing overhead for large conversations while preserving fallback behavior for legacy chats. [Commit](https://github.com/open-webui/open-webui/commit/485d689cfd1ef8b9e7f77cd7b535b8b8747dff1f), [#23159](https://github.com/open-webui/open-webui/pull/23159)
|
||||
- 🗑️ **Delete from conversation menu.** You can now delete the current conversation directly from the chat menu with a confirmation step, so cleanup is faster without searching through the full chat list. [Commit](https://github.com/open-webui/open-webui/commit/ef6d4f2d6c4b79c7e12e864a4fcb6a57ee84e5d4), [#24329](https://github.com/open-webui/open-webui/issues/24329)
|
||||
- ⬆️ **Scroll to Top shortcut.** Long conversations now include a Scroll to Top action in the chat menu when you are away from the top, making it much faster to jump back to the beginning of a chat. [Commit](https://github.com/open-webui/open-webui/commit/cdfcbc4af6e9aec835b88dc1806a2a46711e6947), [#24133](https://github.com/open-webui/open-webui/issues/24133)
|
||||
- 📅 **Calendar creation flow.** Users can now create calendars from a dedicated modal and a quick-add action in the calendar sidebar, making calendar setup faster from the calendar workspace. [Commit](https://github.com/open-webui/open-webui/commit/34146ab60f5dc1a2f8bdda8e61ce02797233a25d), [Commit](https://github.com/open-webui/open-webui/commit/1baf73bdd56f4e5ded12a4bd3c168f4d2a70b840)
|
||||
- 🧭 **Unified model unload controls.** Administrators can now unload running models from the model selector across supported providers, with loaded-state indicators shown for Ollama and llama.cpp models. [Commit](https://github.com/open-webui/open-webui/commit/4fe2de78643c2213652190d2820f4e8d9f4f89cc)
|
||||
- ⚡ **Health check responsiveness.** Health and readiness probes now avoid blocking database calls and skip sync session commit handling on probe paths, improving responsiveness and reducing false unready transitions during database pressure. [#24380](https://github.com/open-webui/open-webui/pull/24380), [#24384](https://github.com/open-webui/open-webui/pull/24384)
|
||||
- 🎛️ **Playground controls panel.** The Playground now includes a dedicated Controls toggle so you can adjust parameters like temperature and related settings per chat run without changing model-level defaults. [Commit](https://github.com/open-webui/open-webui/commit/c6763521c00f042a28829e33fb6f1b7355054046), [#24103](https://github.com/open-webui/open-webui/issues/24103)
|
||||
- 🎙️ **STT file extension controls.** Administrators can now configure which audio file extensions are accepted for speech-to-text uploads, helping enforce safer and more predictable upload policies. [Commit](https://github.com/open-webui/open-webui/commit/4754ece4a2de5bba85a1d53af2dc8d24fdfb58be)
|
||||
- 📷 **Remembered call camera selection.** Voice call overlay now remembers your last selected camera and restores it automatically when available, so you do not need to reselect it every time you start voice mode. [Commit](https://github.com/open-webui/open-webui/commit/5c3edc2539ac4d92c4cc2d37079549995203238a), [#24416](https://github.com/open-webui/open-webui/issues/24416)
|
||||
- 👥 **User group prompt variable.** System and template prompts now support the "{{USER_GROUPS}}" variable, which expands to the user’s group memberships so prompts can adapt to role- or access-based context automatically. [Commit](https://github.com/open-webui/open-webui/commit/c1202a23277abb8e7080271a929dcc9d29b67e66), [#24462](https://github.com/open-webui/open-webui/issues/24462)
|
||||
- 🔐 **Public chat sharing permission control.** Administrators can now control whether users are allowed to create publicly shareable chats through a dedicated permission setting. [Commit](https://github.com/open-webui/open-webui/commit/ef6d4f2d6c4b79c7e12e864a4fcb6a57ee84e5d4)
|
||||
- 🔐 **Profile image forwarding control.** Administrators can now disable external profile image URL forwarding with the "ENABLE_PROFILE_IMAGE_URL_FORWARDING" setting to prevent browser metadata leaks to third-party servers. [#24420](https://github.com/open-webui/open-webui/pull/24420)
|
||||
- 🏷️ **Dynamic header template variables.** Administrators can now use chat, message, and user template variables in custom connection and tool server headers so each request can carry per-conversation context automatically. [Commit](https://github.com/open-webui/open-webui/commit/9907c0a25ae830d134af70022238715f834d20c6), [#24164](https://github.com/open-webui/open-webui/pull/24164)
|
||||
- 🛂 **MCP OAuth server URL setting.** Static OAuth tool server setups can now define a separate OAuth server URL, making discovery and client registration work when authentication endpoints are hosted separately from the tool server URL. [Commit](https://github.com/open-webui/open-webui/commit/9907c0a25ae830d134af70022238715f834d20c6), [#24164](https://github.com/open-webui/open-webui/pull/24164), [#24216](https://github.com/open-webui/open-webui/issues/24216)
|
||||
- ⚡ **Faster memory query performance.** Per-user memory lookups and deletions now run much faster at scale because the memory user filter is indexed for existing and new installations. [Commit](https://github.com/open-webui/open-webui/commit/38a382ef888685650135d61dcc8ec0e29eb65573), [#23836](https://github.com/open-webui/open-webui/pull/23836)
|
||||
- 🚀 **Smarter function dependency installs.** Function dependencies are now skipped when they were already preinstalled and unchanged, reducing first-load delays and repeated package installation churn after startup. [Commit](https://github.com/open-webui/open-webui/commit/ae43562b869b24699408e5ab107261a0a8bdb4bc), [#24166](https://github.com/open-webui/open-webui/pull/24166)
|
||||
- 🔎 **Brave LLM Context web search.** Administrators can now choose Brave LLM Context as a web search provider to retrieve richer grounded passages with a configurable context token budget. [Commit](https://github.com/open-webui/open-webui/commit/6700f7bb72d14a3f8dbb72dfa064cae3b3dc29ac), [#24120](https://github.com/open-webui/open-webui/issues/24120)
|
||||
- 🗂️ **Open Terminal date sorting.** Open Terminal now includes sort controls for name and date, with directory-first ordering and modified-time visibility to make file browsing faster. [Commit](https://github.com/open-webui/open-webui/commit/6bdc2ffa79d72daf78981209c9c5292c697cbfe5), [#24425](https://github.com/open-webui/open-webui/issues/24425)
|
||||
- 🎤 **Voice mode prompt toggle.** Administrators can now explicitly enable or disable the Voice Mode custom prompt behavior from Interface settings, giving finer control over how voice replies are guided. [Commit](https://github.com/open-webui/open-webui/commit/17893038869e3a763a8b34457f723b9666804e27)
|
||||
- 🧮 **LaTeX copy shortcut.** You can now click rendered LaTeX expressions to copy the raw formula to your clipboard, making it easier to reuse equations outside chat. [Commit](https://github.com/open-webui/open-webui/commit/064fdecb675c176a04b024c16ce179f4dda45236), [#24244](https://github.com/open-webui/open-webui/pull/24244)
|
||||
- ✨ **Smoother rich text editing.** The message composer now defers formatting toolbar refresh work to the next animation frame, reducing typing jank while formatting controls stay accurate. [Commit](https://github.com/open-webui/open-webui/commit/794b97025d4c56f91d49c9d1ec4775d2ea07b53a), [#24013](https://github.com/open-webui/open-webui/pull/24013)
|
||||
- 🖼️ **Arena model profile images.** Arena models can now reliably display configured profile images instead of falling back to the default icon. [Commit](https://github.com/open-webui/open-webui/commit/1dee67b64d0b34e70bac949682b216c0aaec8152), [#24412](https://github.com/open-webui/open-webui/issues/24412)
|
||||
- 🔄 **Replaceable tool embed updates.** Pipes and Tools can now overwrite previously emitted rich-UI embeds in-place by passing a `replace` flag on the `embeds` event, enabling live dashboards and progress panels that update without stacking duplicate entries.
|
||||
- ✏️ **Assistant response editing and continuation.** You can now edit and restructure assistant output items — including reasoning blocks, tool calls, and text content — from a dedicated editor view, and continue generating from the edited state so the model receives full prior context.
|
||||
- 🔄 **General improvements.** Various improvements were implemented across the application to enhance performance, stability, and security.
|
||||
- 🌐 **Translation updates.** Translations for Chinese, Catalan, Filipino, and Korean were enhanced and expanded.
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🧵 **Background code execution reliability.** Code execution no longer hangs indefinitely when you switch conversations or browser tabs during a run, and disconnected or inactive sessions now fail with a clear timeout error instead of endless processing. [Commit](https://github.com/open-webui/open-webui/commit/552bbcecfae5ae273ab98e2ce3e540d0771aa964), [#24089](https://github.com/open-webui/open-webui/issues/24089)
|
||||
- 🎙️ **Voice recording MIME fallback support.** Voice recording now tries a broader set of browser-supported audio formats and resets halted audio playback cleanly, improving microphone capture reliability in browsers where recording previously failed to start. [Commit](https://github.com/open-webui/open-webui/commit/8ffc3d746f20007e9eb4e3ae4f152f383bc371e1), [#24162](https://github.com/open-webui/open-webui/issues/24162)
|
||||
- 🧠 **Direct-connection task generation reliability.** Title, tags, follow-up, emoji, query, and related task-generation endpoints now work correctly when chats use direct-connection models instead of failing with model-not-found errors. [Commit](https://github.com/open-webui/open-webui/commit/1b4cd705d0b9a51a5e3a7851ec012fb3141eb0a9), [Commit](https://github.com/open-webui/open-webui/commit/005df577fec16733a64edbec8a1b46f42f4e9a43), [#24092](https://github.com/open-webui/open-webui/issues/24092)
|
||||
- 🔧 **Parameterized URL tool readiness.** New chats now wait for model defaults to finish applying before auto-submit, preventing early requests that can miss configured external tools. [Commit](https://github.com/open-webui/open-webui/commit/212bb68a66435dc1803a6d67cb4ea584d3455fb7), [#24176](https://github.com/open-webui/open-webui/issues/24176)
|
||||
- 🚦 **MCP cleanup response reliability.** Successful native MCP tool calls no longer get replaced by a 500 "No response returned" error during cleanup, so valid chat responses are now returned consistently. [#24105](https://github.com/open-webui/open-webui/pull/24105)
|
||||
- 🧵 **Active task state recovery.** Chat input no longer stays blocked by unrelated background tasks after a response is already complete, and interrupted assistant replies are now marked done more reliably. [Commit](https://github.com/open-webui/open-webui/commit/04bd0425ead28185bcd124e77892e31209a6e15b), [#23264](https://github.com/open-webui/open-webui/pull/23264)
|
||||
- 📌 **Per-user note pinning behavior.** Pinned notes are now tracked per user instead of with a shared note-level flag, so one person’s pin changes no longer affect everyone else. [Commit](https://github.com/open-webui/open-webui/commit/33e588cf09b294f0abe08b9566efa8545a7dbf92)
|
||||
- 🧱 **Custom header value coercion.** Custom header values are now converted to text before requests are sent, preventing request failures when non-text values are configured. [Commit](https://github.com/open-webui/open-webui/commit/9907c0a25ae830d134af70022238715f834d20c6), [#24164](https://github.com/open-webui/open-webui/pull/24164)
|
||||
- 🔗 **HTTP share link copy fallback.** Copy Link now works reliably on HTTP deployments by using a selection-based fallback when secure clipboard APIs are unavailable. [Commit](https://github.com/open-webui/open-webui/commit/f70b0da1563ffa0a8daecbe71cbc30fd8cf834c4), [#24135](https://github.com/open-webui/open-webui/issues/24135)
|
||||
- 🧵 **Regeneration loading lock recovery.** Chats no longer get stuck in a permanent loading state after failed regenerations because invalid message-tree references are repaired before rendering. [Commit](https://github.com/open-webui/open-webui/commit/ee3b82926b37843f2771c6a8d432781a557ea96a), [#24424](https://github.com/open-webui/open-webui/issues/24424)
|
||||
- 📸 **Complete chat image capture.** Downloaded chat snapshots now include all messages more reliably through visibility overrides and layout timing improvements during capture. [Commit](https://github.com/open-webui/open-webui/commit/34146ab60f5dc1a2f8bdda8e61ce02797233a25d), [Commit](https://github.com/open-webui/open-webui/commit/1baf73bdd56f4e5ded12a4bd3c168f4d2a70b840), [#24088](https://github.com/open-webui/open-webui/issues/24088)
|
||||
- 🗓️ **Calendar deletion lock handling.** Calendar deletion now avoids SQLite write-lock contention by revoking calendar access grants in a separate transaction after calendar and event removal. [Commit](https://github.com/open-webui/open-webui/commit/1d892ce2c513c4d933c902de5e5d76c317a06dd2)
|
||||
- 🧩 **Filter and internal tool coexistence.** Internal tools now remain available when filters add provider-native tools, so filter-added tools no longer replace the built-in tool set during request processing. [Commit](https://github.com/open-webui/open-webui/commit/02f9fe78907c2ecf6f1d93646cbfa2173409bbe8), [#24237](https://github.com/open-webui/open-webui/issues/24237)
|
||||
- 🛠️ **OpenAPI tool spec compatibility.** OpenAPI tool integrations now handle null or non-operation path entries more safely and parse path-level parameters consistently, preventing crashes and improving tool execution reliability across imperfect OpenAPI specs. [Commit](https://github.com/open-webui/open-webui/commit/2ba6b423aa0c9c800bd96cb638c6ade867cac0f6), [Commit](https://github.com/open-webui/open-webui/commit/5b80932e5951786bb348b91589e8d87753f18905), [#24376](https://github.com/open-webui/open-webui/pull/24376)
|
||||
- 🧰 **OpenAPI tool schema parsing.** OpenAPI tool imports now ignore non-method path item fields and correctly resolve nested composition schemas, preventing invalid tool parsing for compatible specs. [Commit](https://github.com/open-webui/open-webui/commit/85c7373f68ac3e39a9cd37e63b6926b13fb8b8cc), [#23254](https://github.com/open-webui/open-webui/pull/23254)
|
||||
- 🌍 **Web search proxy compatibility.** DuckDuckGo search now respects configured proxy environments more reliably, and trust-env behavior defaults to enabled so proxied web loading does not fail unexpectedly. [Commit](https://github.com/open-webui/open-webui/commit/bb0e6cb1085aa3c3da66a5f5ea1cecff7e9b5297), [#23810](https://github.com/open-webui/open-webui/pull/23810)
|
||||
- 🧾 **Final markdown render flush.** Streaming markdown now forces an immediate final parse when generation completes, preventing stale or partially rendered final output. [Commit](https://github.com/open-webui/open-webui/commit/29f6c72e879d67f23021938e24a21914cc9fb120), [#24088](https://github.com/open-webui/open-webui/issues/24088)
|
||||
- 🛡️ **Webhook avatar URL validation.** Channel webhook profile image URLs are now validated before saving, preventing invalid or unsafe avatar URLs from being accepted. [#24370](https://github.com/open-webui/open-webui/pull/24370)
|
||||
- 📝 **System prompt editor scroll stability.** Editing large system prompts no longer jumps the page back to the top, so you can continue editing long model prompts without losing your place. [Commit](https://github.com/open-webui/open-webui/commit/c978a788c8315e37357c93c1b605a2831fc77485), [#23999](https://github.com/open-webui/open-webui/issues/23999)
|
||||
- 🔎 **Knowledge content search matching.** Knowledge file search now matches both file titles and file content, so relevant files are easier to find even when the keyword is not in the filename. [Commit](https://github.com/open-webui/open-webui/commit/11e076817ae5db34621ce03136353248f7377d97), [#24297](https://github.com/open-webui/open-webui/pull/24297)
|
||||
- ⚡ **Faster prompt tag loading.** Prompt tag filters now load much faster for non-admin users by fetching only accessible tags directly, avoiding per-prompt permission checks and unnecessary prompt data loading. [#24287](https://github.com/open-webui/open-webui/pull/24287), [#24258](https://github.com/open-webui/open-webui/discussions/24258)
|
||||
- 🧾 **Citation overflow badge readability.** Citation overflow badges now keep multi-digit counts readable in a single compact bubble, preventing wrapped or cramped display when many sources are attached. [Commit](https://github.com/open-webui/open-webui/commit/23ff9943a9fc8c314100fa074157853fbece1a55), [#24391](https://github.com/open-webui/open-webui/pull/24391)
|
||||
- 🌐 **Yandex result parsing guard.** Yandex web search no longer fails when some XML fields are missing in individual results, so valid search responses continue to return usable sources instead of dropping to no results. [Commit](https://github.com/open-webui/open-webui/commit/9386fc83a3eff3e55cc157ac8c15c337e3d822c1), [#24243](https://github.com/open-webui/open-webui/issues/24243)
|
||||
- 🎧 **Safer voice transcription uploads.** Empty or failed voice conversions are now rejected with a clear error instead of continuing as malformed audio, reducing failed transcription attempts from corrupted or near-empty recordings. [Commit](https://github.com/open-webui/open-webui/commit/072d2000f35a9f7b96342fa9bb28f925a92e7b4c)
|
||||
- 🎚️ **Safer chunked STT processing.** Chunked transcription now limits worker concurrency when no external STT engine is configured, reducing failed transcription behavior caused by overly parallel local processing. [Commit](https://github.com/open-webui/open-webui/commit/55e7c7854bba5182803239c903a0ac2d14426a4c)
|
||||
- 📈 **Imported chat analytics coverage.** Imported ChatGPT conversations now carry proper model and timestamp metadata and reliably write imported messages into analytics-backed storage, so imported chats are reflected correctly in Admin Analytics totals and model usage views. [Commit](https://github.com/open-webui/open-webui/commit/4d766a3edfa116abcefe7168f1d1284683b860b2), [#24263](https://github.com/open-webui/open-webui/issues/24263)
|
||||
- 📎 **Knowledge collection persistence.** Knowledge collections selected with the chat input selector now remain attached after reloads and chat switches, so attached context no longer disappears between sessions. [Commit](https://github.com/open-webui/open-webui/commit/7c398a625a8d51f79d80217f6d329fc30c72b782), [#24142](https://github.com/open-webui/open-webui/issues/24142)
|
||||
- 🧹 **Embedding model name trimming.** Embedding model names entered in Documents settings now automatically trim surrounding whitespace, preventing silent embedding failures caused by accidental trailing spaces. [Commit](https://github.com/open-webui/open-webui/commit/6082e1adaebc8aa3e7f55265c8dc2dbe130c0446), [#24090](https://github.com/open-webui/open-webui/issues/24090)
|
||||
- 🔊 **PCM TTS playback compatibility.** Text-to-speech audio returned as PCM is now converted to MP3 before delivery, so speech playback works correctly with providers that return raw PCM audio. [Commit](https://github.com/open-webui/open-webui/commit/ff791b4814fc1453df2235ea78016d7015aa6806), [#24143](https://github.com/open-webui/open-webui/issues/24143)
|
||||
- 🪟 **Windows PostgreSQL startup compatibility.** Windows pip installs using PostgreSQL now start reliably with psycopg async by using a compatible event loop policy instead of the default Proactor loop. [Commit](https://github.com/open-webui/open-webui/commit/7eaecbad5a0913ed04ca3bc10c930bb051dd2bd9), [#24152](https://github.com/open-webui/open-webui/issues/24152)
|
||||
- ⏱️ **MCP OAuth timeout control.** OAuth token exchanges for MCP tool server connections now respect the configurable client timeout setting, reducing callback failures with slower providers. [Commit](https://github.com/open-webui/open-webui/commit/cde72dab71671645e119564ca9747ce25dd590ad), [#24138](https://github.com/open-webui/open-webui/issues/24138)
|
||||
- 📄 **PDF text search restoration.** PDF previews now include a proper text layer so browser text selection and find-in-page search work again instead of rendering only image-like pages. [Commit](https://github.com/open-webui/open-webui/commit/bc4d6eef33dcb92719b07483cdb1d63ebf250721), [#24149](https://github.com/open-webui/open-webui/issues/24149)
|
||||
- 🔑 **Android password autofill support.** Password inputs now expose the expected field name metadata, improving password manager autofill reliability on Android login pages. [Commit](https://github.com/open-webui/open-webui/commit/60ea4214aa42f1ad22142f1a43535007a2293d16), [#24137](https://github.com/open-webui/open-webui/issues/24137)
|
||||
- 🎤 **Non-blocking STT processing.** Speech-to-text transcription no longer blocks the server event loop during both live transcription and uploaded audio file processing, so other users can continue using chats and live connections under concurrent load. [#24338](https://github.com/open-webui/open-webui/pull/24338), [#24379](https://github.com/open-webui/open-webui/pull/24379), [#24169](https://github.com/open-webui/open-webui/issues/24169)
|
||||
- 🌐 **SearXNG language parameter handling.** Web searches now send clean multi-language values without trailing separators, so SearXNG requests no longer fail when multiple languages are selected. [Commit](https://github.com/open-webui/open-webui/commit/6dff85b9d205cfc4bc2845dac40909b8d859910c), [#24198](https://github.com/open-webui/open-webui/issues/24198)
|
||||
- 📂 **File modal open-link behavior.** Clicking a file name in the file details modal now opens the correct file content in a new tab for uploaded file items instead of failing to open. [#24125](https://github.com/open-webui/open-webui/pull/24125)
|
||||
- 📎 **Chat attachment display recovery.** Files attached by chat tools now appear reliably in assistant responses, including non-image file attachments that were previously hidden. [Commit](https://github.com/open-webui/open-webui/commit/7eeff2fdf945024585a01b72071a61971afc844d), [#24332](https://github.com/open-webui/open-webui/pull/24332)
|
||||
- 🧱 **Channel embed rendering guard.** Channel message embeds now appear only for model-generated messages and are suppressed in reply previews, preventing unintended embed expansion in regular user posts. [Commit](https://github.com/open-webui/open-webui/commit/e1dce9914745de9b4d2c67b1deddde3472ce4dfa)
|
||||
- 🛡️ **Safer image URL handling.** Untrusted external image URLs are now blocked in profile and rich-text image rendering paths, preventing unintended client-side requests to attacker-controlled domains. [#24420](https://github.com/open-webui/open-webui/pull/24420)
|
||||
- 🛡️ **Sanitized spreadsheet HTML previews.** Spreadsheet previews now sanitize generated HTML before rendering, reducing the risk of unsafe content being executed when opening office files in chat and file modals. [#24468](https://github.com/open-webui/open-webui/pull/24468)
|
||||
- 🧰 **Multi-worker tool update consistency.** Updated tool code now refreshes correctly across workers without requiring a full service restart, so chats no longer run stale tool versions after edits. [Commit](https://github.com/open-webui/open-webui/commit/3309f5d9f11f521c0ee97b64c59a83e3cf390bde), [#24400](https://github.com/open-webui/open-webui/issues/24400), [#24433](https://github.com/open-webui/open-webui/pull/24433)
|
||||
- 🧩 **Default model metadata env parsing.** The "DEFAULT_MODEL_METADATA" environment setting is now parsed and applied correctly, including when persistent config is disabled, so configured model capability defaults are no longer ignored at startup. [Commit](https://github.com/open-webui/open-webui/commit/0103d7e82cccbd5c4b1c8daabcb3e5160fa74a97), [#24319](https://github.com/open-webui/open-webui/issues/24319)
|
||||
- 🔄 **Config import and Redis consistency.** Imported settings now remain effective after import because configuration values are immediately synchronized to Redis, preventing stale cached values from overriding imported permissions and settings. [Commit](https://github.com/open-webui/open-webui/commit/55a572cd398c9b4e6118728f8f129941437aa225), [Commit](https://github.com/open-webui/open-webui/commit/1c1c8b18e5cc90ca3c6961a4c193a4363febbc83), [#24346](https://github.com/open-webui/open-webui/issues/24346)
|
||||
- 🔔 **LDAP signup webhook parity.** New accounts created through LDAP now trigger the same signup webhook notifications as password and OAuth signups, so downstream provisioning and audit automations receive consistent events. [Commit](https://github.com/open-webui/open-webui/commit/fd3368c0bff168417e3c49ffd73491c344702339), [#24377](https://github.com/open-webui/open-webui/issues/24377)
|
||||
- 🦆 **DDGS auto-backend compatibility.** Web search now handles DDGS automatic backend selection correctly and safely falls back on empty or rate-limited responses, preventing search failures in newer DDGS versions. [Commit](https://github.com/open-webui/open-webui/commit/9adc0c442a57eaa88a5f30c2b2cb393623154e20), [#24188](https://github.com/open-webui/open-webui/issues/24188)
|
||||
- 🤖 **Automation update tool reliability.** Updating existing automations in chat now works correctly instead of failing with a missing method error. [Commit](https://github.com/open-webui/open-webui/commit/f39f4a86aedc2769d8268670a020b1f3c16776dd), [#24405](https://github.com/open-webui/open-webui/issues/24405#issuecomment-4408011166)
|
||||
- 📅 **Calendar event permission checks.** Calendar event update and delete actions now handle ownership and access checks more reliably, returning clean access-denied results when appropriate. [Commit](https://github.com/open-webui/open-webui/commit/2977910ffd9d2369dfa504aa6ab12745b3dbd19a)
|
||||
- 🛡️ **Safer cached file delivery.** Cached files that are not recognized as image, audio, or video now download as attachments instead of rendering inline, reducing the risk of unsafe browser content handling. [Commit](https://github.com/open-webui/open-webui/commit/4754ece4a2de5bba85a1d53af2dc8d24fdfb58be)
|
||||
- 📊 **Streaming token analytics accuracy.** Admin Analytics now records and aggregates token usage correctly for streaming chats across Responses API and OpenAI-compatible providers, including fallback handling for provider usage formats that use prompt and completion token keys. [Commit](https://github.com/open-webui/open-webui/commit/989d5fd4e2ce285edf4475a1e13f0981a78d3821), [Commit](https://github.com/open-webui/open-webui/commit/a32d26e61d24d9f63650faed5cb8909ed90af661), [#24217](https://github.com/open-webui/open-webui/issues/24217), [#24294](https://github.com/open-webui/open-webui/issues/24294), [#24241](https://github.com/open-webui/open-webui/issues/24241)
|
||||
- 🔗 **Admin shared chat links.** Admin users can now open and clone shared chat links reliably without 401 errors because shared links are now resolved by share ID first, with safe fallback behavior for direct chat ID access. [Commit](https://github.com/open-webui/open-webui/commit/cde21b9f6dc11575a668484f42440824ec5a4fae), [#24311](https://github.com/open-webui/open-webui/issues/24311), [#24096](https://github.com/open-webui/open-webui/issues/24096)
|
||||
- 💾 **Chat settings persistence.** System prompts and other chat-level settings now persist correctly after creating a new chat and reloading, preventing prompt loss in affected conversations. [Commit](https://github.com/open-webui/open-webui/commit/86df8bf27e1b84abbe2eeedcc8650df59c7d23d6), [#24193](https://github.com/open-webui/open-webui/issues/24193), [#24270](https://github.com/open-webui/open-webui/issues/24270)
|
||||
- 💾 **Chat control autosave persistence.** Changes to chat controls like system prompt, parameters, and attached files are now autosaved on existing chats, so edits are no longer lost when you refresh or navigate away before sending a message. [Commit](https://github.com/open-webui/open-webui/commit/a938c8ae2e45a00d2f06151fdaeaee94e54a8095), [#23897](https://github.com/open-webui/open-webui/pull/23897)
|
||||
- ☁️ **OneDrive option visibility.** OneDrive personal and business upload options now appear only when their respective client IDs are configured, preventing unavailable options from showing in attachment menus. [Commit](https://github.com/open-webui/open-webui/commit/b72019db393a658ca0ceecdcc59b70f6cc5dcd40), [#24411](https://github.com/open-webui/open-webui/issues/24411)
|
||||
- 🧠 **Reasoning content leakage prevention.** Tool-call round-trip messages no longer wrap reasoning text in `<think>` tags inside the content field, preventing raw markup from leaking into chat output for models whose templates don't strip think tags (e.g. Gemma 4). [#23844](https://github.com/open-webui/open-webui/issues/23844)
|
||||
- 🖥️ **Terminal sidebar auto-open guard.** The terminal sidebar no longer auto-opens on chat load when OpenTerminal is disabled, because stale terminal IDs saved on models or in localStorage are now validated against available terminal servers before use.
|
||||
- 🔁 **Single-confirmation connection deletion.** Deleting OpenAI, Ollama, tool server, and terminal server connections now shows exactly one confirmation dialog instead of two, because redundant outer confirmation wrappers were removed from all connection components.
|
||||
- 🧵 **Reliable background task cleanup.** The chat task lifecycle now deregisters completed tasks before checking for remaining siblings, eliminating the off-by-one timing issue that could leave the stop button stuck or dismiss the sidebar activity spinner too early.
|
||||
|
||||
### Changed
|
||||
|
||||
- ⚠️ **Database Migrations**: This release includes database schema changes; we strongly recommend backing up your database and all associated data before upgrading in production environments. If you are running a multi-worker, multi-server, or load-balanced deployment, all instances must be updated simultaneously, rolling updates are not supported and will cause application failures due to schema incompatibility.
|
||||
- 🚪 **Signout request method.** The signout endpoint now requires POST instead of GET, so custom clients and integrations must update logout calls accordingly. [#24420](https://github.com/open-webui/open-webui/pull/24420)
|
||||
|
||||
## [0.9.2] - 2026-04-24
|
||||
|
||||
### Added
|
||||
|
||||
+2
-2
@@ -43,7 +43,7 @@ ENV APP_BUILD_HASH=${BUILD_HASH}
|
||||
RUN npm run build
|
||||
|
||||
######## WebUI backend ########
|
||||
FROM python:3.11-slim-bookworm AS base
|
||||
FROM python:3.11.14-slim-bookworm AS base
|
||||
|
||||
# Use args
|
||||
ARG USE_CUDA
|
||||
@@ -126,7 +126,7 @@ RUN chown -R $UID:$GID /app $HOME
|
||||
# Install common system dependencies
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
git build-essential pandoc gcc netcat-openbsd curl jq ca-certificates \
|
||||
git build-essential pandoc gcc netcat-openbsd curl jq \
|
||||
libmariadb-dev \
|
||||
python3-dev \
|
||||
ffmpeg libsm6 libxext6 zstd \
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||

|
||||

|
||||
[](https://discord.gg/5rJgQTnV4s)
|
||||
[](https://github.com/sponsors/open-webui)
|
||||
[](https://github.com/sponsors/tjbck)
|
||||
|
||||

|
||||
|
||||
@@ -27,84 +27,58 @@ For more information, be sure to check out our [Open WebUI Documentation](https:
|
||||
|
||||
## Key Features of Open WebUI ⭐
|
||||
|
||||
- 🚀 **Effortless Setup**: Install seamlessly via pip, uv, Docker, or Kubernetes (kubectl, kustomize, or helm), with `:ollama` and `:cuda` tagged images available for container deployments.
|
||||
- 🚀 **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.
|
||||
|
||||
- 🤝 **Broad Model & API Integration**: Connect any OpenAI-compatible API alongside local Ollama models. Point the API URL at **LMStudio, GroqCloud, Mistral, OpenRouter, vLLM, and more** to mix and match providers freely.
|
||||
- 🤝 **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**.
|
||||
|
||||
- 🔐 **Granular RBAC & User Groups**: Administrators define detailed roles, groups, and permissions, giving each user exactly the access they need. Secure by default, with tailored experiences per group.
|
||||
- 🛡️ **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.
|
||||
|
||||
- 🧩 **Plugin Support**: Extend Open WebUI with **Filters**, **Actions**, **Pipes**, **Tools**, and **Skills**. Connect external services through **MCP**, **MCPO**, and **OpenAPI tool servers**. Build custom integrations, rate limits, approval flows, data connections, and more.
|
||||
- 📱 **Responsive Design**: Enjoy a seamless experience across Desktop PC, Laptop, and Mobile devices.
|
||||
|
||||
- 🤖 **Models & Agents**: Wrap any base model with custom instructions, tools, and knowledge to build specialized agents. Supports dynamic variables, per-user/group access control, and community preset imports via [Open WebUI Community](https://openwebui.com/).
|
||||
- 📱 **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.
|
||||
|
||||
- 📝 **Notes**: A dedicated workspace for content outside conversations. Draft with a rich editor, use AI to rewrite selected text, and attach notes to any chat for full-context injection.
|
||||
- ✒️🔢 **Full Markdown and LaTeX Support**: Elevate your LLM experience with comprehensive Markdown and LaTeX capabilities for enriched interaction.
|
||||
|
||||
- 📢 **Channels**: Real-time shared spaces where your team and AI models collaborate in one timeline. Tag models to draft or critique, with threads, reactions, pins, and access control.
|
||||
- 🎤📹 **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.
|
||||
|
||||
- 🧠 **Persistent Memory**: The AI remembers facts about you across conversations, carrying context from one chat to the next.
|
||||
- 🛠️ **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.
|
||||
|
||||
- ✅ **Live Workflow & Message Flow**: Watch the AI build and work through checklists in real time. Queue messages while the AI is still responding; they send automatically when it's ready.
|
||||
- 🐍 **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.
|
||||
|
||||
- 📅 **Calendar & AI Scheduling**: Built-in personal and shared calendars with month/week/day views, recurring events, color coding, attendees, and reminders. Models manage your schedule conversationally through native function calling.
|
||||
- 💾 **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.
|
||||
|
||||
- ⏱️ **Automations**: Schedule prompts to run on recurring schedules, with runs surfaced on your calendar and each completed run linking back to the chat it produced.
|
||||
- 📚 **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, PaddleOCR-vl, External loaders). Load documents directly into chat or add files to your document library, effortlessly accessing them using the `#` command before a query.
|
||||
|
||||
- 📱 **Responsive Design & PWA**: Seamless experience across desktop, laptop, and mobile, with a Progressive Web App for native app-like feel and offline access on localhost.
|
||||
- 🔍 **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.
|
||||
|
||||
- ✒️🔢 **Full Markdown and LaTeX Support**: Comprehensive Markdown and LaTeX capabilities for enriched interaction.
|
||||
- 🌐 **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.
|
||||
|
||||
- 🎤📹 **Hands-Free Voice/Video Call**: Integrated voice and video calls with multiple Speech-to-Text providers (Local Whisper, OpenAI, Deepgram, Azure) and Text-to-Speech engines (Azure, ElevenLabs, OpenAI, Transformers, WebAPI).
|
||||
- 🎨 **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.
|
||||
|
||||
- 💾 **Persistent Artifact Storage**: Built-in key-value storage API for artifacts, enabling journals, trackers, leaderboards, and collaborative tools with personal and shared data scopes.
|
||||
- ⚙️ **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.
|
||||
|
||||
- 📚 **Local RAG Integration**: Retrieval Augmented Generation backed by 9 vector databases and multiple content-extraction engines (Tika, Docling, Document Intelligence, Mistral OCR, PaddleOCR-vl, external loaders). Supports hybrid search (BM25 + vector) with reranking and full-context mode. Load documents into chat or pull them from your library with the `#` command.
|
||||
- 🔐 **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.
|
||||
|
||||
- 🔍 **Web Search for RAG**: Search the web through dozens of providers including `SearXNG`, `Google PSE`, `Brave Search`, `Kagi`, `Mojeek`, `Tavily`, `Perplexity`, `Firecrawl`, `serpstack`, `serper`, `Serply`, `DuckDuckGo`, `SearchApi`, `SerpApi`, `Bing`, `Jina`, `Exa`, `Sougou`, `Azure AI Search`, and `Ollama Cloud`, injecting results directly into the conversation.
|
||||
- 🗄️ **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.
|
||||
|
||||
- 🌐 **Web Browsing Capability**: Pull websites into chat with the `#` command followed by a URL, or let the model fetch them on its own when needed.
|
||||
- 🔍 **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.
|
||||
|
||||
- 🎨 **Image Generation & Editing**: Create and edit images with multiple engines including OpenAI DALL·E, Gemini, ComfyUI (local), and AUTOMATIC1111 (local), supporting both generation and prompt-based editing.
|
||||
- 🔐 **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.
|
||||
|
||||
- ⚙️ **Multi-Model Conversations**: Engage several models at once, harnessing their individual strengths in parallel for the best possible responses.
|
||||
- ☁️ **Cloud-Native Integration**: Native support for Google Drive and OneDrive/SharePoint file picking, enabling seamless document import from enterprise cloud storage.
|
||||
|
||||
- 📊 **Usage Analytics & Model Evaluation**: Admin dashboards track message volume, token consumption, and cost across users and models. Evaluate models with a built-in arena, A/B testing, and ELO-based leaderboards.
|
||||
- 📊 **Production Observability**: Built-in OpenTelemetry support for traces, metrics, and logs, enabling comprehensive monitoring with your existing observability stack.
|
||||
|
||||
- 🗄️ **Flexible Database & Storage**: Choose SQLite (with optional encryption) or PostgreSQL, and store files locally or on S3, Google Cloud Storage, or Azure Blob Storage.
|
||||
- ⚖️ **Horizontal Scalability**: Redis-backed session management and WebSocket support for multi-worker and multi-node deployments behind load balancers.
|
||||
|
||||
- 🧬 **Advanced Vector Database Support**: Pick from 9 vector databases: ChromaDB, PGVector, Qdrant, Milvus, Elasticsearch, OpenSearch, Pinecone, S3Vector, and Oracle 23ai.
|
||||
- 🌐🌍 **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!
|
||||
|
||||
- 🪪 **Enterprise Authentication & Provisioning**: Full LDAP/Active Directory integration, SSO via trusted headers and OAuth providers, and SCIM 2.0 automated provisioning for identity providers like Okta, Azure AD, and Google Workspace.
|
||||
- 🧩 **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.
|
||||
|
||||
- ☁️ **Cloud-Native File Integration**: Native Google Drive and OneDrive/SharePoint file picking for seamless document import from enterprise cloud storage.
|
||||
|
||||
- 🔭 **Production Observability**: Built-in OpenTelemetry support for traces, metrics, and logs, plugging into your existing monitoring stack.
|
||||
|
||||
- ⚖️ **Horizontal Scalability**: Redis-backed session management and WebSocket support for multi-worker, multi-node deployments behind load balancers.
|
||||
|
||||
- 🌐🌍 **Multilingual Support**: Use Open WebUI in your preferred language with i18n support. We're actively seeking contributors to expand language coverage!
|
||||
|
||||
- 🌟 **Continuous Updates**: We're committed to improving Open WebUI with regular updates, fixes, and new features.
|
||||
|
||||
- 🛡️ **Transparent Security Process**: Security reports are triaged, fixed, and published as open advisories through a documented responsible-disclosure process. See our [Security Policy](https://github.com/open-webui/open-webui/security).
|
||||
- 🌟 **Continuous Updates**: We are committed to improving Open WebUI with regular updates, fixes, and new features.
|
||||
|
||||
Want to learn more about Open WebUI's features? Check out our [Open WebUI documentation](https://docs.openwebui.com/features) for a comprehensive overview!
|
||||
|
||||
## The Open WebUI Ecosystem 🌐
|
||||
|
||||
Open WebUI is the core, surrounded by companion apps and infrastructure that extend what your AI can do, where it can reach, and how you run it:
|
||||
|
||||
- ⚡ **Open Terminal** ([open-webui/open-terminal](https://github.com/open-webui/open-terminal)): A self-hosted computing environment that plugs into Open WebUI, giving the AI a place to write code, run it, read output, fix errors, and iterate inside the chat.
|
||||
|
||||
- 🔒 **Terminals** · Enterprise ([open-webui/terminals](https://github.com/open-webui/terminals)): Per-user isolated containers with separate credentials, resource limits, and network rules. Automatic lifecycle management on Docker or Kubernetes.
|
||||
|
||||
- 💻 **cptr** ([open-webui/computer](https://github.com/open-webui/computer)): A standalone, mobile-first computer and coding agent that runs on the machine you own. Files, terminal, and git in a browser tab, reachable from your phone. Connect it into Open WebUI as a model, or reach it from Telegram, WhatsApp, and more.
|
||||
|
||||
- 🔄 **oikb** ([open-webui/oikb](https://github.com/open-webui/oikb)): Feed your Knowledge Bases from 45+ sources (GitHub, Confluence, ServiceNow, Salesforce, Jira, Slack, SharePoint, Notion, and more), keeping the tools your team already uses continuously in sync.
|
||||
|
||||
- 🖥️ **Native Desktop App** ([open-webui/desktop](https://github.com/open-webui/desktop)): Run Open WebUI as a native app on macOS, Windows, and Linux. System-wide Spotlight chat bar with screenshot capture, push-to-talk voice, and optional fully-local inference via a built-in llama.cpp engine.
|
||||
|
||||
Want to learn more? Check out our [Open WebUI documentation](https://docs.openwebui.com) for more details!
|
||||
|
||||
---
|
||||
|
||||
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!
|
||||
@@ -248,10 +222,6 @@ This project contains code under multiple licenses. The current codebase include
|
||||
If you have any questions, suggestions, or need assistance, please open an issue or join our
|
||||
[Open WebUI Discord community](https://discord.gg/5rJgQTnV4s) to connect with us! 🤝
|
||||
|
||||
## Security 🛡️
|
||||
|
||||
If you believe you've found a security vulnerability, or something that shouldn't be disclosed publicly, please [reach out confidentially through our responsible disclosure program on GitHub](https://github.com/open-webui/open-webui/security). We accept reports only through GitHub, not through any other platform. Thank you for helping us keep Open WebUI secure!
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://star-history.com/#open-webui/open-webui&Date">
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import base64
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
|
||||
@@ -11,7 +10,6 @@ import uvicorn
|
||||
app = typer.Typer()
|
||||
|
||||
KEY_FILE = Path.cwd() / '.webui_secret_key'
|
||||
DEFAULT_SECRET_KEY_LENGTH = 24
|
||||
|
||||
|
||||
def version_callback(value: bool) -> None:
|
||||
@@ -38,11 +36,8 @@ def serve(
|
||||
if os.getenv('WEBUI_SECRET_KEY') is None:
|
||||
typer.echo('Loading WEBUI_SECRET_KEY from file, not provided as an environment variable.')
|
||||
if not KEY_FILE.exists():
|
||||
key_length = int(os.getenv('WEBUI_SECRET_KEY_LENGTH', DEFAULT_SECRET_KEY_LENGTH))
|
||||
if key_length < 1:
|
||||
raise ValueError('WEBUI_SECRET_KEY_LENGTH must be a positive integer')
|
||||
typer.echo(f'Generating a new secret key and saving it to {KEY_FILE}')
|
||||
KEY_FILE.write_bytes(base64.b64encode(random.randbytes(key_length)))
|
||||
KEY_FILE.write_bytes(base64.b64encode(random.randbytes(12)))
|
||||
typer.echo(f'Loading WEBUI_SECRET_KEY from {KEY_FILE}')
|
||||
os.environ['WEBUI_SECRET_KEY'] = KEY_FILE.read_text()
|
||||
|
||||
@@ -73,18 +68,12 @@ def serve(
|
||||
import open_webui.main # noqa: F401
|
||||
from open_webui.env import UVICORN_WORKERS # Import the workers setting
|
||||
|
||||
# On Windows, uvicorn's default loop factory hardcodes ProactorEventLoop,
|
||||
# which is incompatible with psycopg v3 async. Setting loop='none' lets
|
||||
# asyncio.run() respect the WindowsSelectorEventLoopPolicy set in db.py.
|
||||
loop = 'none' if sys.platform == 'win32' else 'auto'
|
||||
|
||||
uvicorn.run(
|
||||
'open_webui.main:app',
|
||||
host=host,
|
||||
port=port,
|
||||
forwarded_allow_ips='*',
|
||||
workers=UVICORN_WORKERS,
|
||||
loop=loop,
|
||||
)
|
||||
|
||||
|
||||
|
||||
+3207
-2166
@@ -1,50 +1,43 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import socket
|
||||
import base64
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import redis
|
||||
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union
|
||||
from typing import Generic, Union, Optional, TypeVar
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import redis
|
||||
import requests
|
||||
from authlib.integrations.starlette_client import OAuth
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import JSON, Column, DateTime, Integer, func
|
||||
from authlib.integrations.starlette_client import OAuth
|
||||
|
||||
|
||||
from open_webui.env import (
|
||||
DATA_DIR,
|
||||
DATABASE_URL,
|
||||
ENABLE_DB_MIGRATIONS,
|
||||
ENV,
|
||||
FRONTEND_BUILD_DIR,
|
||||
OFFLINE_MODE,
|
||||
OPEN_WEBUI_DIR,
|
||||
REDIS_URL,
|
||||
REDIS_KEY_PREFIX,
|
||||
REDIS_SENTINEL_HOSTS,
|
||||
REDIS_SENTINEL_PORT,
|
||||
REDIS_URL,
|
||||
FRONTEND_BUILD_DIR,
|
||||
OFFLINE_MODE,
|
||||
OPEN_WEBUI_DIR,
|
||||
WEBUI_AUTH,
|
||||
WEBUI_FAVICON_URL,
|
||||
WEBUI_NAME,
|
||||
log,
|
||||
)
|
||||
from open_webui.models.config import Config
|
||||
|
||||
|
||||
async def seed_registered_defaults():
|
||||
await Config.rename_prefix('rag.web', 'web')
|
||||
await Config.repair_flattened_dict_configs()
|
||||
await Config.seed_defaults(DEFAULT_CONFIG)
|
||||
|
||||
|
||||
async def async_reset_config():
|
||||
await Config.clear()
|
||||
from open_webui.internal.db import Base, get_db, get_async_db
|
||||
from open_webui.utils.redis import get_redis_connection
|
||||
|
||||
|
||||
class EndpointFilter(logging.Filter):
|
||||
@@ -52,21 +45,24 @@ class EndpointFilter(logging.Filter):
|
||||
return record.getMessage().find('/health') == -1
|
||||
|
||||
|
||||
# Filter out /endpoint
|
||||
logging.getLogger('uvicorn.access').addFilter(EndpointFilter())
|
||||
|
||||
####################################
|
||||
# Initialization
|
||||
# Config helpers
|
||||
####################################
|
||||
|
||||
|
||||
# Function to run the alembic migrations
|
||||
def run_migrations():
|
||||
log.info('Running migrations')
|
||||
try:
|
||||
from alembic import command
|
||||
from alembic.config import Config as AlembicConfig
|
||||
from alembic.config import Config
|
||||
|
||||
alembic_cfg = AlembicConfig(OPEN_WEBUI_DIR / 'alembic.ini')
|
||||
alembic_cfg = Config(OPEN_WEBUI_DIR / 'alembic.ini')
|
||||
|
||||
# Set the script location dynamically
|
||||
migrations_path = OPEN_WEBUI_DIR / 'migrations'
|
||||
alembic_cfg.set_main_option('script_location', str(migrations_path))
|
||||
|
||||
@@ -79,14 +75,828 @@ if ENABLE_DB_MIGRATIONS:
|
||||
run_migrations()
|
||||
|
||||
|
||||
async def import_legacy_config_json():
|
||||
"""Migrate legacy config.json → database on first run."""
|
||||
if not os.path.exists(f'{DATA_DIR}/config.json'):
|
||||
return
|
||||
with open(f'{DATA_DIR}/config.json', 'r') as _f:
|
||||
await Config.upsert(json.load(_f))
|
||||
class Config(Base):
|
||||
__tablename__ = 'config'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
data = Column(JSON, nullable=False)
|
||||
version = Column(Integer, nullable=False, default=0)
|
||||
created_at = Column(DateTime, nullable=False, server_default=func.now())
|
||||
updated_at = Column(DateTime, nullable=True, onupdate=func.now())
|
||||
|
||||
|
||||
def load_json_config():
|
||||
with open(f'{DATA_DIR}/config.json', 'r') as file:
|
||||
return json.load(file)
|
||||
|
||||
|
||||
def save_to_db(data):
|
||||
"""Sync save — used ONLY at startup/import time."""
|
||||
with get_db() as db:
|
||||
existing_config = db.query(Config).first()
|
||||
if not existing_config:
|
||||
new_config = Config(data=data, version=0)
|
||||
db.add(new_config)
|
||||
else:
|
||||
existing_config.data = data
|
||||
existing_config.updated_at = datetime.now()
|
||||
db.add(existing_config)
|
||||
db.commit()
|
||||
|
||||
|
||||
async def async_save_to_db(data):
|
||||
"""Async save — used for ALL runtime config persistence."""
|
||||
from sqlalchemy import select
|
||||
|
||||
async with get_async_db() as db:
|
||||
result = await db.execute(select(Config).limit(1))
|
||||
existing_config = result.scalars().first()
|
||||
if not existing_config:
|
||||
new_config = Config(data=data, version=0)
|
||||
db.add(new_config)
|
||||
else:
|
||||
existing_config.data = data
|
||||
existing_config.updated_at = datetime.now()
|
||||
db.add(existing_config)
|
||||
await db.commit()
|
||||
|
||||
|
||||
def reset_config():
|
||||
"""Sync reset — used ONLY at startup."""
|
||||
with get_db() as db:
|
||||
db.query(Config).delete()
|
||||
db.commit()
|
||||
|
||||
|
||||
async def async_reset_config():
|
||||
"""Async reset — used at runtime."""
|
||||
from sqlalchemy import delete as sa_delete
|
||||
|
||||
async with get_async_db() as db:
|
||||
await db.execute(sa_delete(Config))
|
||||
await db.commit()
|
||||
|
||||
|
||||
# When initializing, check if config.json exists and migrate it to the database
|
||||
if os.path.exists(f'{DATA_DIR}/config.json'):
|
||||
data = load_json_config()
|
||||
save_to_db(data)
|
||||
os.rename(f'{DATA_DIR}/config.json', f'{DATA_DIR}/old_config.json')
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
'version': 0,
|
||||
'ui': {},
|
||||
}
|
||||
|
||||
|
||||
def get_config():
|
||||
with get_db() as db:
|
||||
config_entry = db.query(Config).order_by(Config.id.desc()).first()
|
||||
return config_entry.data if config_entry else DEFAULT_CONFIG
|
||||
|
||||
|
||||
CONFIG_DATA = get_config()
|
||||
|
||||
|
||||
def get_config_value(config_path: str):
|
||||
path_parts = config_path.split('.')
|
||||
cur_config = CONFIG_DATA
|
||||
for key in path_parts:
|
||||
if key in cur_config:
|
||||
cur_config = cur_config[key]
|
||||
else:
|
||||
return None
|
||||
return cur_config
|
||||
|
||||
|
||||
PERSISTENT_CONFIG_REGISTRY = []
|
||||
|
||||
|
||||
def save_config(config):
|
||||
"""Sync save — used ONLY at startup/import time."""
|
||||
global CONFIG_DATA
|
||||
global PERSISTENT_CONFIG_REGISTRY
|
||||
try:
|
||||
save_to_db(config)
|
||||
CONFIG_DATA = config
|
||||
|
||||
# Trigger updates on all registered PersistentConfig entries
|
||||
for config_item in PERSISTENT_CONFIG_REGISTRY:
|
||||
config_item.update()
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
async def async_save_config(config):
|
||||
"""Async save — used for ALL runtime config persistence."""
|
||||
global CONFIG_DATA
|
||||
global PERSISTENT_CONFIG_REGISTRY
|
||||
try:
|
||||
await async_save_to_db(config)
|
||||
CONFIG_DATA = config
|
||||
|
||||
# Trigger updates on all registered PersistentConfig entries
|
||||
for config_item in PERSISTENT_CONFIG_REGISTRY:
|
||||
config_item.update()
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
ENABLE_PERSISTENT_CONFIG = os.environ.get('ENABLE_PERSISTENT_CONFIG', 'True').lower() == 'true'
|
||||
|
||||
|
||||
class PersistentConfig(Generic[T]):
|
||||
def __init__(self, env_name: str, config_path: str, env_value: T):
|
||||
self.env_name = env_name
|
||||
self.config_path = config_path
|
||||
self.env_value = env_value
|
||||
self.config_value = get_config_value(config_path)
|
||||
|
||||
if self.config_value is not None and ENABLE_PERSISTENT_CONFIG:
|
||||
if self.config_path.startswith('oauth.') and not ENABLE_OAUTH_PERSISTENT_CONFIG:
|
||||
log.info(f"Skipping loading of '{env_name}' as OAuth persistent config is disabled")
|
||||
self.value = env_value
|
||||
else:
|
||||
log.info(f"'{env_name}' loaded from the latest database entry")
|
||||
self.value = self.config_value
|
||||
else:
|
||||
self.value = env_value
|
||||
|
||||
PERSISTENT_CONFIG_REGISTRY.append(self)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.value)
|
||||
|
||||
@property
|
||||
def __dict__(self):
|
||||
raise TypeError('PersistentConfig object cannot be converted to dict, use config_get or .value instead.')
|
||||
|
||||
def __getattribute__(self, item):
|
||||
if item == '__dict__':
|
||||
raise TypeError('PersistentConfig object cannot be converted to dict, use config_get or .value instead.')
|
||||
return super().__getattribute__(item)
|
||||
|
||||
def update(self):
|
||||
new_value = get_config_value(self.config_path)
|
||||
if new_value is not None:
|
||||
self.value = new_value
|
||||
log.info(f'Updated {self.env_name} to new value {self.value}')
|
||||
|
||||
def save(self):
|
||||
"""Sync save — used ONLY at startup/import time."""
|
||||
log.info(f"Saving '{self.env_name}' to the database")
|
||||
path_parts = self.config_path.split('.')
|
||||
sub_config = CONFIG_DATA
|
||||
for key in path_parts[:-1]:
|
||||
if key not in sub_config:
|
||||
sub_config[key] = {}
|
||||
sub_config = sub_config[key]
|
||||
sub_config[path_parts[-1]] = self.value
|
||||
save_to_db(CONFIG_DATA)
|
||||
self.config_value = self.value
|
||||
|
||||
async def async_save(self):
|
||||
"""Async save — used for ALL runtime config persistence."""
|
||||
log.info(f"Saving '{self.env_name}' to the database")
|
||||
path_parts = self.config_path.split('.')
|
||||
sub_config = CONFIG_DATA
|
||||
for key in path_parts[:-1]:
|
||||
if key not in sub_config:
|
||||
sub_config[key] = {}
|
||||
sub_config = sub_config[key]
|
||||
sub_config[path_parts[-1]] = self.value
|
||||
await async_save_to_db(CONFIG_DATA)
|
||||
self.config_value = self.value
|
||||
|
||||
|
||||
class AppConfig:
|
||||
_redis: Union[redis.Redis, redis.cluster.RedisCluster] = None
|
||||
_redis_key_prefix: str
|
||||
|
||||
_state: dict[str, PersistentConfig]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
redis_url: Optional[str] = None,
|
||||
redis_sentinels: Optional[list] = [],
|
||||
redis_cluster: Optional[bool] = False,
|
||||
redis_key_prefix: str = 'open-webui',
|
||||
):
|
||||
if redis_url:
|
||||
super().__setattr__('_redis_key_prefix', redis_key_prefix)
|
||||
super().__setattr__(
|
||||
'_redis',
|
||||
get_redis_connection(
|
||||
redis_url,
|
||||
redis_sentinels,
|
||||
redis_cluster,
|
||||
decode_responses=True,
|
||||
),
|
||||
)
|
||||
|
||||
super().__setattr__('_state', {})
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
if isinstance(value, PersistentConfig):
|
||||
self._state[key] = value
|
||||
else:
|
||||
self._state[key].value = value
|
||||
|
||||
# At runtime (inside the event loop) persist via the async engine
|
||||
# to avoid blocking the loop and contending with the async DB pool.
|
||||
# At startup/import time, fall back to sync.
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
loop.create_task(self._async_persist(key))
|
||||
except RuntimeError:
|
||||
self._state[key].save()
|
||||
|
||||
if self._redis and ENABLE_PERSISTENT_CONFIG:
|
||||
redis_key = f'{self._redis_key_prefix}:config:{key}'
|
||||
self._redis.set(redis_key, json.dumps(self._state[key].value))
|
||||
|
||||
async def _async_persist(self, key):
|
||||
"""Persist a single config key via the async engine."""
|
||||
try:
|
||||
await self._state[key].async_save()
|
||||
except Exception as e:
|
||||
log.error(f'Failed to async-persist config key {key}: {e}')
|
||||
|
||||
def __getattr__(self, key):
|
||||
if key not in self._state:
|
||||
raise AttributeError(f"Config key '{key}' not found")
|
||||
|
||||
# If Redis is available and persistent config is enabled, check for an updated value
|
||||
if self._redis and ENABLE_PERSISTENT_CONFIG:
|
||||
redis_key = f'{self._redis_key_prefix}:config:{key}'
|
||||
redis_value = self._redis.get(redis_key)
|
||||
|
||||
if redis_value is not None:
|
||||
try:
|
||||
decoded_value = json.loads(redis_value)
|
||||
|
||||
# Update the in-memory value if different
|
||||
if self._state[key].value != decoded_value:
|
||||
self._state[key].value = decoded_value
|
||||
log.info(f'Updated {key} from Redis: {decoded_value}')
|
||||
|
||||
except json.JSONDecodeError:
|
||||
log.error(f'Invalid JSON format in Redis for {key}: {redis_value}')
|
||||
|
||||
return self._state[key].value
|
||||
|
||||
|
||||
####################################
|
||||
# WEBUI_AUTH (Required for security)
|
||||
####################################
|
||||
|
||||
ENABLE_API_KEYS = PersistentConfig(
|
||||
'ENABLE_API_KEYS',
|
||||
'auth.enable_api_keys',
|
||||
os.environ.get('ENABLE_API_KEYS', 'False').lower() == 'true',
|
||||
)
|
||||
|
||||
ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS = PersistentConfig(
|
||||
'ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS',
|
||||
'auth.api_key.endpoint_restrictions',
|
||||
os.environ.get(
|
||||
'ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS',
|
||||
os.environ.get('ENABLE_API_KEY_ENDPOINT_RESTRICTIONS', 'False'),
|
||||
).lower()
|
||||
== 'true',
|
||||
)
|
||||
|
||||
API_KEYS_ALLOWED_ENDPOINTS = PersistentConfig(
|
||||
'API_KEYS_ALLOWED_ENDPOINTS',
|
||||
'auth.api_key.allowed_endpoints',
|
||||
os.environ.get('API_KEYS_ALLOWED_ENDPOINTS', os.environ.get('API_KEY_ALLOWED_ENDPOINTS', '')),
|
||||
)
|
||||
|
||||
JWT_EXPIRES_IN = PersistentConfig('JWT_EXPIRES_IN', 'auth.jwt_expiry', os.environ.get('JWT_EXPIRES_IN', '4w'))
|
||||
|
||||
if JWT_EXPIRES_IN.value == '-1':
|
||||
log.warning(
|
||||
"⚠️ SECURITY WARNING: JWT_EXPIRES_IN is set to '-1'\n"
|
||||
' See: https://docs.openwebui.com/reference/env-configuration\n'
|
||||
)
|
||||
|
||||
####################################
|
||||
# OAuth config
|
||||
####################################
|
||||
|
||||
ENABLE_OAUTH_PERSISTENT_CONFIG = os.environ.get('ENABLE_OAUTH_PERSISTENT_CONFIG', 'False').lower() == 'true'
|
||||
|
||||
ENABLE_OAUTH_SIGNUP = PersistentConfig(
|
||||
'ENABLE_OAUTH_SIGNUP',
|
||||
'oauth.enable_signup',
|
||||
os.environ.get('ENABLE_OAUTH_SIGNUP', 'False').lower() == 'true',
|
||||
)
|
||||
|
||||
OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE = PersistentConfig(
|
||||
'OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE',
|
||||
'oauth.refresh_token_include_scope',
|
||||
os.environ.get('OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE', 'False').lower() == 'true',
|
||||
)
|
||||
|
||||
|
||||
OAUTH_MERGE_ACCOUNTS_BY_EMAIL = PersistentConfig(
|
||||
'OAUTH_MERGE_ACCOUNTS_BY_EMAIL',
|
||||
'oauth.merge_accounts_by_email',
|
||||
os.environ.get('OAUTH_MERGE_ACCOUNTS_BY_EMAIL', 'False').lower() == 'true',
|
||||
)
|
||||
|
||||
OAUTH_PROVIDERS = {}
|
||||
|
||||
GOOGLE_CLIENT_ID = PersistentConfig(
|
||||
'GOOGLE_CLIENT_ID',
|
||||
'oauth.google.client_id',
|
||||
os.environ.get('GOOGLE_CLIENT_ID', ''),
|
||||
)
|
||||
|
||||
GOOGLE_CLIENT_SECRET = PersistentConfig(
|
||||
'GOOGLE_CLIENT_SECRET',
|
||||
'oauth.google.client_secret',
|
||||
os.environ.get('GOOGLE_CLIENT_SECRET', ''),
|
||||
)
|
||||
|
||||
|
||||
GOOGLE_OAUTH_SCOPE = PersistentConfig(
|
||||
'GOOGLE_OAUTH_SCOPE',
|
||||
'oauth.google.scope',
|
||||
os.environ.get('GOOGLE_OAUTH_SCOPE', 'openid email profile'),
|
||||
)
|
||||
|
||||
GOOGLE_REDIRECT_URI = PersistentConfig(
|
||||
'GOOGLE_REDIRECT_URI',
|
||||
'oauth.google.redirect_uri',
|
||||
os.environ.get('GOOGLE_REDIRECT_URI', ''),
|
||||
)
|
||||
|
||||
GOOGLE_OAUTH_AUTHORIZE_PARAMS = {}
|
||||
_google_oauth_authorize_params = os.environ.get('GOOGLE_OAUTH_AUTHORIZE_PARAMS', '')
|
||||
if _google_oauth_authorize_params:
|
||||
try:
|
||||
_parsed = json.loads(_google_oauth_authorize_params)
|
||||
if isinstance(_parsed, dict):
|
||||
GOOGLE_OAUTH_AUTHORIZE_PARAMS = _parsed
|
||||
else:
|
||||
log.warning('GOOGLE_OAUTH_AUTHORIZE_PARAMS must be a JSON object, ignoring')
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
log.warning('GOOGLE_OAUTH_AUTHORIZE_PARAMS is not valid JSON, ignoring')
|
||||
|
||||
MICROSOFT_CLIENT_ID = PersistentConfig(
|
||||
'MICROSOFT_CLIENT_ID',
|
||||
'oauth.microsoft.client_id',
|
||||
os.environ.get('MICROSOFT_CLIENT_ID', ''),
|
||||
)
|
||||
|
||||
MICROSOFT_CLIENT_SECRET = PersistentConfig(
|
||||
'MICROSOFT_CLIENT_SECRET',
|
||||
'oauth.microsoft.client_secret',
|
||||
os.environ.get('MICROSOFT_CLIENT_SECRET', ''),
|
||||
)
|
||||
|
||||
MICROSOFT_CLIENT_TENANT_ID = PersistentConfig(
|
||||
'MICROSOFT_CLIENT_TENANT_ID',
|
||||
'oauth.microsoft.tenant_id',
|
||||
os.environ.get('MICROSOFT_CLIENT_TENANT_ID', ''),
|
||||
)
|
||||
|
||||
MICROSOFT_CLIENT_LOGIN_BASE_URL = PersistentConfig(
|
||||
'MICROSOFT_CLIENT_LOGIN_BASE_URL',
|
||||
'oauth.microsoft.login_base_url',
|
||||
os.environ.get('MICROSOFT_CLIENT_LOGIN_BASE_URL', 'https://login.microsoftonline.com'),
|
||||
)
|
||||
|
||||
MICROSOFT_CLIENT_PICTURE_URL = PersistentConfig(
|
||||
'MICROSOFT_CLIENT_PICTURE_URL',
|
||||
'oauth.microsoft.picture_url',
|
||||
os.environ.get(
|
||||
'MICROSOFT_CLIENT_PICTURE_URL',
|
||||
'https://graph.microsoft.com/v1.0/me/photo/$value',
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
MICROSOFT_OAUTH_SCOPE = PersistentConfig(
|
||||
'MICROSOFT_OAUTH_SCOPE',
|
||||
'oauth.microsoft.scope',
|
||||
os.environ.get('MICROSOFT_OAUTH_SCOPE', 'openid email profile'),
|
||||
)
|
||||
|
||||
MICROSOFT_REDIRECT_URI = PersistentConfig(
|
||||
'MICROSOFT_REDIRECT_URI',
|
||||
'oauth.microsoft.redirect_uri',
|
||||
os.environ.get('MICROSOFT_REDIRECT_URI', ''),
|
||||
)
|
||||
|
||||
GITHUB_CLIENT_ID = PersistentConfig(
|
||||
'GITHUB_CLIENT_ID',
|
||||
'oauth.github.client_id',
|
||||
os.environ.get('GITHUB_CLIENT_ID', ''),
|
||||
)
|
||||
|
||||
GITHUB_CLIENT_SECRET = PersistentConfig(
|
||||
'GITHUB_CLIENT_SECRET',
|
||||
'oauth.github.client_secret',
|
||||
os.environ.get('GITHUB_CLIENT_SECRET', ''),
|
||||
)
|
||||
|
||||
GITHUB_CLIENT_SCOPE = PersistentConfig(
|
||||
'GITHUB_CLIENT_SCOPE',
|
||||
'oauth.github.scope',
|
||||
os.environ.get('GITHUB_CLIENT_SCOPE', 'user:email'),
|
||||
)
|
||||
|
||||
GITHUB_CLIENT_REDIRECT_URI = PersistentConfig(
|
||||
'GITHUB_CLIENT_REDIRECT_URI',
|
||||
'oauth.github.redirect_uri',
|
||||
os.environ.get('GITHUB_CLIENT_REDIRECT_URI', ''),
|
||||
)
|
||||
|
||||
OAUTH_CLIENT_ID = PersistentConfig(
|
||||
'OAUTH_CLIENT_ID',
|
||||
'oauth.oidc.client_id',
|
||||
os.environ.get('OAUTH_CLIENT_ID', ''),
|
||||
)
|
||||
|
||||
OAUTH_CLIENT_SECRET = PersistentConfig(
|
||||
'OAUTH_CLIENT_SECRET',
|
||||
'oauth.oidc.client_secret',
|
||||
os.environ.get('OAUTH_CLIENT_SECRET', ''),
|
||||
)
|
||||
|
||||
OPENID_PROVIDER_URL = PersistentConfig(
|
||||
'OPENID_PROVIDER_URL',
|
||||
'oauth.oidc.provider_url',
|
||||
os.environ.get('OPENID_PROVIDER_URL', ''),
|
||||
)
|
||||
|
||||
OPENID_END_SESSION_ENDPOINT = PersistentConfig(
|
||||
'OPENID_END_SESSION_ENDPOINT',
|
||||
'oauth.oidc.end_session_endpoint',
|
||||
os.environ.get('OPENID_END_SESSION_ENDPOINT', ''),
|
||||
)
|
||||
|
||||
OPENID_REDIRECT_URI = PersistentConfig(
|
||||
'OPENID_REDIRECT_URI',
|
||||
'oauth.oidc.redirect_uri',
|
||||
os.environ.get('OPENID_REDIRECT_URI', ''),
|
||||
)
|
||||
|
||||
OAUTH_SCOPES = PersistentConfig(
|
||||
'OAUTH_SCOPES',
|
||||
'oauth.oidc.scopes',
|
||||
os.environ.get('OAUTH_SCOPES', 'openid email profile'),
|
||||
)
|
||||
|
||||
OAUTH_TIMEOUT = PersistentConfig(
|
||||
'OAUTH_TIMEOUT',
|
||||
'oauth.oidc.oauth_timeout',
|
||||
os.environ.get('OAUTH_TIMEOUT', ''),
|
||||
)
|
||||
|
||||
OAUTH_TOKEN_ENDPOINT_AUTH_METHOD = PersistentConfig(
|
||||
'OAUTH_TOKEN_ENDPOINT_AUTH_METHOD',
|
||||
'oauth.oidc.token_endpoint_auth_method',
|
||||
os.environ.get('OAUTH_TOKEN_ENDPOINT_AUTH_METHOD', None),
|
||||
)
|
||||
|
||||
OAUTH_CODE_CHALLENGE_METHOD = PersistentConfig(
|
||||
'OAUTH_CODE_CHALLENGE_METHOD',
|
||||
'oauth.oidc.code_challenge_method',
|
||||
os.environ.get('OAUTH_CODE_CHALLENGE_METHOD', None),
|
||||
)
|
||||
|
||||
OAUTH_PROVIDER_NAME = PersistentConfig(
|
||||
'OAUTH_PROVIDER_NAME',
|
||||
'oauth.oidc.provider_name',
|
||||
os.environ.get('OAUTH_PROVIDER_NAME', 'SSO'),
|
||||
)
|
||||
|
||||
OAUTH_SUB_CLAIM = PersistentConfig(
|
||||
'OAUTH_SUB_CLAIM',
|
||||
'oauth.oidc.sub_claim',
|
||||
os.environ.get('OAUTH_SUB_CLAIM', None),
|
||||
)
|
||||
|
||||
OAUTH_USERNAME_CLAIM = PersistentConfig(
|
||||
'OAUTH_USERNAME_CLAIM',
|
||||
'oauth.oidc.username_claim',
|
||||
os.environ.get('OAUTH_USERNAME_CLAIM', 'name'),
|
||||
)
|
||||
|
||||
|
||||
OAUTH_PICTURE_CLAIM = PersistentConfig(
|
||||
'OAUTH_PICTURE_CLAIM',
|
||||
'oauth.oidc.avatar_claim',
|
||||
os.environ.get('OAUTH_PICTURE_CLAIM', 'picture'),
|
||||
)
|
||||
|
||||
OAUTH_EMAIL_CLAIM = PersistentConfig(
|
||||
'OAUTH_EMAIL_CLAIM',
|
||||
'oauth.oidc.email_claim',
|
||||
os.environ.get('OAUTH_EMAIL_CLAIM', 'email'),
|
||||
)
|
||||
|
||||
OAUTH_GROUPS_CLAIM = PersistentConfig(
|
||||
'OAUTH_GROUPS_CLAIM',
|
||||
'oauth.oidc.group_claim',
|
||||
os.environ.get('OAUTH_GROUPS_CLAIM', os.environ.get('OAUTH_GROUP_CLAIM', 'groups')),
|
||||
)
|
||||
|
||||
FEISHU_CLIENT_ID = PersistentConfig(
|
||||
'FEISHU_CLIENT_ID',
|
||||
'oauth.feishu.client_id',
|
||||
os.environ.get('FEISHU_CLIENT_ID', ''),
|
||||
)
|
||||
|
||||
FEISHU_CLIENT_SECRET = PersistentConfig(
|
||||
'FEISHU_CLIENT_SECRET',
|
||||
'oauth.feishu.client_secret',
|
||||
os.environ.get('FEISHU_CLIENT_SECRET', ''),
|
||||
)
|
||||
|
||||
FEISHU_OAUTH_SCOPE = PersistentConfig(
|
||||
'FEISHU_OAUTH_SCOPE',
|
||||
'oauth.feishu.scope',
|
||||
os.environ.get('FEISHU_OAUTH_SCOPE', 'contact:user.base:readonly'),
|
||||
)
|
||||
|
||||
FEISHU_REDIRECT_URI = PersistentConfig(
|
||||
'FEISHU_REDIRECT_URI',
|
||||
'oauth.feishu.redirect_uri',
|
||||
os.environ.get('FEISHU_REDIRECT_URI', ''),
|
||||
)
|
||||
|
||||
ENABLE_OAUTH_ROLE_MANAGEMENT = PersistentConfig(
|
||||
'ENABLE_OAUTH_ROLE_MANAGEMENT',
|
||||
'oauth.enable_role_mapping',
|
||||
os.environ.get('ENABLE_OAUTH_ROLE_MANAGEMENT', 'False').lower() == 'true',
|
||||
)
|
||||
|
||||
ENABLE_OAUTH_GROUP_MANAGEMENT = PersistentConfig(
|
||||
'ENABLE_OAUTH_GROUP_MANAGEMENT',
|
||||
'oauth.enable_group_mapping',
|
||||
os.environ.get('ENABLE_OAUTH_GROUP_MANAGEMENT', 'False').lower() == 'true',
|
||||
)
|
||||
|
||||
ENABLE_OAUTH_GROUP_CREATION = PersistentConfig(
|
||||
'ENABLE_OAUTH_GROUP_CREATION',
|
||||
'oauth.enable_group_creation',
|
||||
os.environ.get('ENABLE_OAUTH_GROUP_CREATION', 'False').lower() == 'true',
|
||||
)
|
||||
|
||||
|
||||
oauth_group_default_share = os.environ.get('OAUTH_GROUP_DEFAULT_SHARE', 'true').strip().lower()
|
||||
OAUTH_GROUP_DEFAULT_SHARE = PersistentConfig(
|
||||
'OAUTH_GROUP_DEFAULT_SHARE',
|
||||
'oauth.group_default_share',
|
||||
('members' if oauth_group_default_share == 'members' else oauth_group_default_share == 'true'),
|
||||
)
|
||||
|
||||
|
||||
OAUTH_BLOCKED_GROUPS = PersistentConfig(
|
||||
'OAUTH_BLOCKED_GROUPS',
|
||||
'oauth.blocked_groups',
|
||||
os.environ.get('OAUTH_BLOCKED_GROUPS', '[]'),
|
||||
)
|
||||
|
||||
OAUTH_GROUPS_SEPARATOR = os.environ.get('OAUTH_GROUPS_SEPARATOR', ';')
|
||||
|
||||
OAUTH_ROLES_CLAIM = PersistentConfig(
|
||||
'OAUTH_ROLES_CLAIM',
|
||||
'oauth.roles_claim',
|
||||
os.environ.get('OAUTH_ROLES_CLAIM', 'roles'),
|
||||
)
|
||||
|
||||
OAUTH_ROLES_SEPARATOR = os.environ.get('OAUTH_ROLES_SEPARATOR', ',')
|
||||
|
||||
OAUTH_ALLOWED_ROLES = PersistentConfig(
|
||||
'OAUTH_ALLOWED_ROLES',
|
||||
'oauth.allowed_roles',
|
||||
[
|
||||
role.strip()
|
||||
for role in os.environ.get('OAUTH_ALLOWED_ROLES', f'user{OAUTH_ROLES_SEPARATOR}admin').split(
|
||||
OAUTH_ROLES_SEPARATOR
|
||||
)
|
||||
if role
|
||||
],
|
||||
)
|
||||
|
||||
OAUTH_ADMIN_ROLES = PersistentConfig(
|
||||
'OAUTH_ADMIN_ROLES',
|
||||
'oauth.admin_roles',
|
||||
[role.strip() for role in os.environ.get('OAUTH_ADMIN_ROLES', 'admin').split(OAUTH_ROLES_SEPARATOR) if role],
|
||||
)
|
||||
|
||||
OAUTH_ALLOWED_DOMAINS = PersistentConfig(
|
||||
'OAUTH_ALLOWED_DOMAINS',
|
||||
'oauth.allowed_domains',
|
||||
[domain.strip() for domain in os.environ.get('OAUTH_ALLOWED_DOMAINS', '*').split(',')],
|
||||
)
|
||||
|
||||
OAUTH_UPDATE_PICTURE_ON_LOGIN = PersistentConfig(
|
||||
'OAUTH_UPDATE_PICTURE_ON_LOGIN',
|
||||
'oauth.update_picture_on_login',
|
||||
os.environ.get('OAUTH_UPDATE_PICTURE_ON_LOGIN', 'False').lower() == 'true',
|
||||
)
|
||||
|
||||
OAUTH_UPDATE_NAME_ON_LOGIN = PersistentConfig(
|
||||
'OAUTH_UPDATE_NAME_ON_LOGIN',
|
||||
'oauth.update_name_on_login',
|
||||
os.environ.get('OAUTH_UPDATE_NAME_ON_LOGIN', 'False').lower() == 'true',
|
||||
)
|
||||
|
||||
OAUTH_UPDATE_EMAIL_ON_LOGIN = PersistentConfig(
|
||||
'OAUTH_UPDATE_EMAIL_ON_LOGIN',
|
||||
'oauth.update_email_on_login',
|
||||
os.environ.get('OAUTH_UPDATE_EMAIL_ON_LOGIN', 'False').lower() == 'true',
|
||||
)
|
||||
|
||||
OAUTH_ACCESS_TOKEN_REQUEST_INCLUDE_CLIENT_ID = (
|
||||
os.environ.get('OAUTH_ACCESS_TOKEN_REQUEST_INCLUDE_CLIENT_ID', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
OAUTH_AUDIENCE = PersistentConfig(
|
||||
'OAUTH_AUDIENCE',
|
||||
'oauth.audience',
|
||||
os.environ.get('OAUTH_AUDIENCE', ''),
|
||||
)
|
||||
|
||||
OAUTH_AUTHORIZE_PARAMS = {}
|
||||
_oauth_authorize_params = os.environ.get('OAUTH_AUTHORIZE_PARAMS', '')
|
||||
if _oauth_authorize_params:
|
||||
try:
|
||||
_parsed = json.loads(_oauth_authorize_params)
|
||||
if isinstance(_parsed, dict):
|
||||
OAUTH_AUTHORIZE_PARAMS = _parsed
|
||||
else:
|
||||
log.warning('OAUTH_AUTHORIZE_PARAMS must be a JSON object, ignoring')
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
log.warning('OAUTH_AUTHORIZE_PARAMS is not valid JSON, ignoring')
|
||||
|
||||
|
||||
def load_oauth_providers():
|
||||
OAUTH_PROVIDERS.clear()
|
||||
if GOOGLE_CLIENT_ID.value and GOOGLE_CLIENT_SECRET.value:
|
||||
|
||||
def google_oauth_register(oauth: OAuth):
|
||||
client = oauth.register(
|
||||
name='google',
|
||||
client_id=GOOGLE_CLIENT_ID.value,
|
||||
client_secret=GOOGLE_CLIENT_SECRET.value,
|
||||
server_metadata_url='https://accounts.google.com/.well-known/openid-configuration',
|
||||
client_kwargs={
|
||||
'scope': GOOGLE_OAUTH_SCOPE.value,
|
||||
**({'timeout': int(OAUTH_TIMEOUT.value)} if OAUTH_TIMEOUT.value else {}),
|
||||
},
|
||||
redirect_uri=GOOGLE_REDIRECT_URI.value,
|
||||
**({'authorize_params': GOOGLE_OAUTH_AUTHORIZE_PARAMS} if GOOGLE_OAUTH_AUTHORIZE_PARAMS else {}),
|
||||
)
|
||||
return client
|
||||
|
||||
OAUTH_PROVIDERS['google'] = {
|
||||
'register': google_oauth_register,
|
||||
}
|
||||
|
||||
if MICROSOFT_CLIENT_ID.value and MICROSOFT_CLIENT_SECRET.value and MICROSOFT_CLIENT_TENANT_ID.value:
|
||||
|
||||
def microsoft_oauth_register(oauth: OAuth):
|
||||
client = oauth.register(
|
||||
name='microsoft',
|
||||
client_id=MICROSOFT_CLIENT_ID.value,
|
||||
client_secret=MICROSOFT_CLIENT_SECRET.value,
|
||||
server_metadata_url=f'{MICROSOFT_CLIENT_LOGIN_BASE_URL.value}/{MICROSOFT_CLIENT_TENANT_ID.value}/v2.0/.well-known/openid-configuration?appid={MICROSOFT_CLIENT_ID.value}',
|
||||
client_kwargs={
|
||||
'scope': MICROSOFT_OAUTH_SCOPE.value,
|
||||
**({'timeout': int(OAUTH_TIMEOUT.value)} if OAUTH_TIMEOUT.value else {}),
|
||||
},
|
||||
redirect_uri=MICROSOFT_REDIRECT_URI.value,
|
||||
)
|
||||
return client
|
||||
|
||||
OAUTH_PROVIDERS['microsoft'] = {
|
||||
'picture_url': MICROSOFT_CLIENT_PICTURE_URL.value,
|
||||
'register': microsoft_oauth_register,
|
||||
}
|
||||
|
||||
if GITHUB_CLIENT_ID.value and GITHUB_CLIENT_SECRET.value:
|
||||
|
||||
def github_oauth_register(oauth: OAuth):
|
||||
client = oauth.register(
|
||||
name='github',
|
||||
client_id=GITHUB_CLIENT_ID.value,
|
||||
client_secret=GITHUB_CLIENT_SECRET.value,
|
||||
access_token_url='https://github.com/login/oauth/access_token',
|
||||
authorize_url='https://github.com/login/oauth/authorize',
|
||||
api_base_url='https://api.github.com',
|
||||
userinfo_endpoint='https://api.github.com/user',
|
||||
client_kwargs={
|
||||
'scope': GITHUB_CLIENT_SCOPE.value,
|
||||
**({'timeout': int(OAUTH_TIMEOUT.value)} if OAUTH_TIMEOUT.value else {}),
|
||||
},
|
||||
redirect_uri=GITHUB_CLIENT_REDIRECT_URI.value,
|
||||
)
|
||||
return client
|
||||
|
||||
OAUTH_PROVIDERS['github'] = {
|
||||
'register': github_oauth_register,
|
||||
'sub_claim': 'id',
|
||||
}
|
||||
|
||||
if (
|
||||
OAUTH_CLIENT_ID.value
|
||||
and (OAUTH_CLIENT_SECRET.value or OAUTH_CODE_CHALLENGE_METHOD.value)
|
||||
and OPENID_PROVIDER_URL.value
|
||||
):
|
||||
|
||||
def oidc_oauth_register(oauth: OAuth):
|
||||
client_kwargs = {
|
||||
'scope': OAUTH_SCOPES.value,
|
||||
**(
|
||||
{'token_endpoint_auth_method': OAUTH_TOKEN_ENDPOINT_AUTH_METHOD.value}
|
||||
if OAUTH_TOKEN_ENDPOINT_AUTH_METHOD.value
|
||||
else {}
|
||||
),
|
||||
**({'timeout': int(OAUTH_TIMEOUT.value)} if OAUTH_TIMEOUT.value else {}),
|
||||
}
|
||||
|
||||
if OAUTH_CODE_CHALLENGE_METHOD.value and OAUTH_CODE_CHALLENGE_METHOD.value == 'S256':
|
||||
client_kwargs['code_challenge_method'] = 'S256'
|
||||
elif OAUTH_CODE_CHALLENGE_METHOD.value:
|
||||
raise Exception(
|
||||
'Code challenge methods other than "%s" not supported. Given: "%s"'
|
||||
% ('S256', OAUTH_CODE_CHALLENGE_METHOD.value)
|
||||
)
|
||||
|
||||
client = oauth.register(
|
||||
name='oidc',
|
||||
client_id=OAUTH_CLIENT_ID.value,
|
||||
client_secret=OAUTH_CLIENT_SECRET.value,
|
||||
server_metadata_url=OPENID_PROVIDER_URL.value,
|
||||
client_kwargs=client_kwargs,
|
||||
redirect_uri=OPENID_REDIRECT_URI.value,
|
||||
)
|
||||
return client
|
||||
|
||||
OAUTH_PROVIDERS['oidc'] = {
|
||||
'name': OAUTH_PROVIDER_NAME.value,
|
||||
'register': oidc_oauth_register,
|
||||
}
|
||||
|
||||
if FEISHU_CLIENT_ID.value and FEISHU_CLIENT_SECRET.value:
|
||||
|
||||
def feishu_oauth_register(oauth: OAuth):
|
||||
client = oauth.register(
|
||||
name='feishu',
|
||||
client_id=FEISHU_CLIENT_ID.value,
|
||||
client_secret=FEISHU_CLIENT_SECRET.value,
|
||||
access_token_url='https://open.feishu.cn/open-apis/authen/v2/oauth/token',
|
||||
authorize_url='https://accounts.feishu.cn/open-apis/authen/v1/authorize',
|
||||
api_base_url='https://open.feishu.cn/open-apis',
|
||||
userinfo_endpoint='https://open.feishu.cn/open-apis/authen/v1/user_info',
|
||||
client_kwargs={
|
||||
'scope': FEISHU_OAUTH_SCOPE.value,
|
||||
**({'timeout': int(OAUTH_TIMEOUT.value)} if OAUTH_TIMEOUT.value else {}),
|
||||
},
|
||||
redirect_uri=FEISHU_REDIRECT_URI.value,
|
||||
)
|
||||
return client
|
||||
|
||||
OAUTH_PROVIDERS['feishu'] = {
|
||||
'register': feishu_oauth_register,
|
||||
'sub_claim': 'user_id',
|
||||
}
|
||||
|
||||
configured_providers = []
|
||||
if GOOGLE_CLIENT_ID.value:
|
||||
configured_providers.append('Google')
|
||||
if MICROSOFT_CLIENT_ID.value:
|
||||
configured_providers.append('Microsoft')
|
||||
if GITHUB_CLIENT_ID.value:
|
||||
configured_providers.append('GitHub')
|
||||
if FEISHU_CLIENT_ID.value:
|
||||
configured_providers.append('Feishu')
|
||||
|
||||
if configured_providers and not OPENID_PROVIDER_URL.value and not OPENID_END_SESSION_ENDPOINT.value:
|
||||
provider_list = ', '.join(configured_providers)
|
||||
log.warning(
|
||||
f'⚠️ OAuth providers configured ({provider_list}) but OPENID_PROVIDER_URL not set - logout will not work!'
|
||||
)
|
||||
log.warning(
|
||||
f"Set OPENID_PROVIDER_URL to your OAuth provider's OpenID Connect discovery endpoint,"
|
||||
f' or set OPENID_END_SESSION_ENDPOINT to a custom logout URL to fix logout functionality.'
|
||||
)
|
||||
|
||||
|
||||
load_oauth_providers()
|
||||
|
||||
####################################
|
||||
# Static DIR
|
||||
@@ -139,49 +949,11 @@ if frontend_loader.exists():
|
||||
logging.error(f'An error occurred: {e}')
|
||||
|
||||
|
||||
# --- Storage Provider ---
|
||||
|
||||
STORAGE_PROVIDER = os.getenv('STORAGE_PROVIDER', 'local') # defaults to local, s3
|
||||
STORAGE_LOCAL_CACHE = os.getenv('STORAGE_LOCAL_CACHE', 'true').lower() == 'true'
|
||||
|
||||
S3_ACCESS_KEY_ID = os.getenv('S3_ACCESS_KEY_ID', None)
|
||||
S3_SECRET_ACCESS_KEY = os.getenv('S3_SECRET_ACCESS_KEY', None)
|
||||
S3_REGION_NAME = os.getenv('S3_REGION_NAME', None)
|
||||
S3_BUCKET_NAME = os.getenv('S3_BUCKET_NAME', None)
|
||||
S3_KEY_PREFIX = os.getenv('S3_KEY_PREFIX', None)
|
||||
S3_ENDPOINT_URL = os.getenv('S3_ENDPOINT_URL', None)
|
||||
S3_USE_ACCELERATE_ENDPOINT = os.getenv('S3_USE_ACCELERATE_ENDPOINT', 'false').lower() == 'true'
|
||||
S3_ADDRESSING_STYLE = os.getenv('S3_ADDRESSING_STYLE', None)
|
||||
S3_ENABLE_TAGGING = os.getenv('S3_ENABLE_TAGGING', 'false').lower() == 'true'
|
||||
|
||||
GCS_BUCKET_NAME = os.getenv('GCS_BUCKET_NAME', None)
|
||||
GOOGLE_APPLICATION_CREDENTIALS_JSON = os.getenv('GOOGLE_APPLICATION_CREDENTIALS_JSON', None)
|
||||
|
||||
AZURE_STORAGE_ENDPOINT = os.getenv('AZURE_STORAGE_ENDPOINT', None)
|
||||
AZURE_STORAGE_CONTAINER_NAME = os.getenv('AZURE_STORAGE_CONTAINER_NAME', None)
|
||||
AZURE_STORAGE_KEY = os.getenv('AZURE_STORAGE_KEY', None)
|
||||
|
||||
####################################
|
||||
# File Upload DIR
|
||||
####################################
|
||||
|
||||
UPLOAD_DIR = DATA_DIR / 'uploads'
|
||||
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
####################################
|
||||
# Cache DIR
|
||||
####################################
|
||||
|
||||
CACHE_DIR = DATA_DIR / 'cache'
|
||||
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
####################################
|
||||
# CUSTOM_NAME (Legacy)
|
||||
####################################
|
||||
|
||||
CUSTOM_NAME = os.getenv('CUSTOM_NAME', '')
|
||||
CUSTOM_NAME = os.environ.get('CUSTOM_NAME', '')
|
||||
|
||||
if CUSTOM_NAME:
|
||||
try:
|
||||
@@ -214,28 +986,76 @@ if CUSTOM_NAME:
|
||||
pass
|
||||
|
||||
|
||||
####################################
|
||||
# STORAGE PROVIDER
|
||||
####################################
|
||||
|
||||
STORAGE_PROVIDER = os.environ.get('STORAGE_PROVIDER', 'local') # defaults to local, s3
|
||||
STORAGE_LOCAL_CACHE = os.environ.get('STORAGE_LOCAL_CACHE', 'true').lower() == 'true'
|
||||
|
||||
S3_ACCESS_KEY_ID = os.environ.get('S3_ACCESS_KEY_ID', None)
|
||||
S3_SECRET_ACCESS_KEY = os.environ.get('S3_SECRET_ACCESS_KEY', None)
|
||||
S3_REGION_NAME = os.environ.get('S3_REGION_NAME', None)
|
||||
S3_BUCKET_NAME = os.environ.get('S3_BUCKET_NAME', None)
|
||||
S3_KEY_PREFIX = os.environ.get('S3_KEY_PREFIX', None)
|
||||
S3_ENDPOINT_URL = os.environ.get('S3_ENDPOINT_URL', None)
|
||||
S3_USE_ACCELERATE_ENDPOINT = os.environ.get('S3_USE_ACCELERATE_ENDPOINT', 'false').lower() == 'true'
|
||||
S3_ADDRESSING_STYLE = os.environ.get('S3_ADDRESSING_STYLE', None)
|
||||
S3_ENABLE_TAGGING = os.getenv('S3_ENABLE_TAGGING', 'false').lower() == 'true'
|
||||
|
||||
GCS_BUCKET_NAME = os.environ.get('GCS_BUCKET_NAME', None)
|
||||
GOOGLE_APPLICATION_CREDENTIALS_JSON = os.environ.get('GOOGLE_APPLICATION_CREDENTIALS_JSON', None)
|
||||
|
||||
AZURE_STORAGE_ENDPOINT = os.environ.get('AZURE_STORAGE_ENDPOINT', None)
|
||||
AZURE_STORAGE_CONTAINER_NAME = os.environ.get('AZURE_STORAGE_CONTAINER_NAME', None)
|
||||
AZURE_STORAGE_KEY = os.environ.get('AZURE_STORAGE_KEY', None)
|
||||
|
||||
####################################
|
||||
# File Upload DIR
|
||||
####################################
|
||||
|
||||
UPLOAD_DIR = DATA_DIR / 'uploads'
|
||||
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
####################################
|
||||
# Cache DIR
|
||||
####################################
|
||||
|
||||
CACHE_DIR = DATA_DIR / 'cache'
|
||||
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
####################################
|
||||
# DIRECT CONNECTIONS
|
||||
####################################
|
||||
|
||||
ENABLE_DIRECT_CONNECTIONS = os.getenv('ENABLE_DIRECT_CONNECTIONS', 'False').lower() == 'true'
|
||||
ENABLE_DIRECT_CONNECTIONS = PersistentConfig(
|
||||
'ENABLE_DIRECT_CONNECTIONS',
|
||||
'direct.enable',
|
||||
os.environ.get('ENABLE_DIRECT_CONNECTIONS', 'False').lower() == 'true',
|
||||
)
|
||||
|
||||
####################################
|
||||
# OLLAMA_BASE_URL
|
||||
####################################
|
||||
|
||||
ENABLE_OLLAMA_API = os.getenv('ENABLE_OLLAMA_API', 'True').lower() == 'true'
|
||||
ENABLE_OLLAMA_API = PersistentConfig(
|
||||
'ENABLE_OLLAMA_API',
|
||||
'ollama.enable',
|
||||
os.environ.get('ENABLE_OLLAMA_API', 'True').lower() == 'true',
|
||||
)
|
||||
|
||||
OLLAMA_API_BASE_URL = os.getenv('OLLAMA_API_BASE_URL', 'http://localhost:11434/api')
|
||||
OLLAMA_API_BASE_URL = os.environ.get('OLLAMA_API_BASE_URL', 'http://localhost:11434/api')
|
||||
|
||||
OLLAMA_BASE_URL = os.getenv('OLLAMA_BASE_URL', '')
|
||||
OLLAMA_BASE_URL = os.environ.get('OLLAMA_BASE_URL', '')
|
||||
if OLLAMA_BASE_URL:
|
||||
# Remove trailing slash
|
||||
OLLAMA_BASE_URL = OLLAMA_BASE_URL[:-1] if OLLAMA_BASE_URL.endswith('/') else OLLAMA_BASE_URL
|
||||
|
||||
|
||||
K8S_FLAG = os.getenv('K8S_FLAG', '')
|
||||
USE_OLLAMA_DOCKER = os.getenv('USE_OLLAMA_DOCKER', 'false')
|
||||
K8S_FLAG = os.environ.get('K8S_FLAG', '')
|
||||
USE_OLLAMA_DOCKER = os.environ.get('USE_OLLAMA_DOCKER', 'false')
|
||||
|
||||
if OLLAMA_BASE_URL == '' and OLLAMA_API_BASE_URL != '':
|
||||
OLLAMA_BASE_URL = OLLAMA_API_BASE_URL[:-4] if OLLAMA_API_BASE_URL.endswith('/api') else OLLAMA_API_BASE_URL
|
||||
@@ -279,41 +1099,39 @@ def _resolve_ollama_base_url(url: str) -> str:
|
||||
|
||||
# Auto-resolve Ollama port when no explicit URL was provided by the user.
|
||||
# The Dockerfile default is "/ollama" which the block above rewrites to :11434.
|
||||
if os.getenv('OLLAMA_BASE_URL', '') in ('', '/ollama') and not os.getenv('OLLAMA_BASE_URLS', ''):
|
||||
if os.environ.get('OLLAMA_BASE_URL', '') in ('', '/ollama') and not os.environ.get('OLLAMA_BASE_URLS', ''):
|
||||
OLLAMA_BASE_URL = _resolve_ollama_base_url(OLLAMA_BASE_URL)
|
||||
|
||||
|
||||
OLLAMA_BASE_URLS = os.getenv('OLLAMA_BASE_URLS', '')
|
||||
OLLAMA_BASE_URLS = os.environ.get('OLLAMA_BASE_URLS', '')
|
||||
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS if OLLAMA_BASE_URLS != '' else OLLAMA_BASE_URL
|
||||
|
||||
OLLAMA_BASE_URLS = [url.strip() for url in OLLAMA_BASE_URLS.split(';')]
|
||||
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS
|
||||
OLLAMA_BASE_URLS = PersistentConfig('OLLAMA_BASE_URLS', 'ollama.base_urls', OLLAMA_BASE_URLS)
|
||||
|
||||
OLLAMA_API_CONFIGS = {}
|
||||
_ollama_api_configs = os.getenv('OLLAMA_API_CONFIGS', '')
|
||||
if _ollama_api_configs:
|
||||
try:
|
||||
parsed = json.loads(_ollama_api_configs)
|
||||
if isinstance(parsed, dict):
|
||||
OLLAMA_API_CONFIGS = parsed
|
||||
else:
|
||||
log.warning('OLLAMA_API_CONFIGS must be a JSON object, ignoring')
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
log.warning('OLLAMA_API_CONFIGS is not valid JSON, ignoring')
|
||||
OLLAMA_API_CONFIGS = PersistentConfig(
|
||||
'OLLAMA_API_CONFIGS',
|
||||
'ollama.api_configs',
|
||||
{},
|
||||
)
|
||||
|
||||
####################################
|
||||
# OPENAI_API
|
||||
####################################
|
||||
|
||||
|
||||
ENABLE_OPENAI_API = os.getenv('ENABLE_OPENAI_API', 'True').lower() == 'true'
|
||||
ENABLE_OPENAI_API = PersistentConfig(
|
||||
'ENABLE_OPENAI_API',
|
||||
'openai.enable',
|
||||
os.environ.get('ENABLE_OPENAI_API', 'True').lower() == 'true',
|
||||
)
|
||||
|
||||
|
||||
OPENAI_API_KEY = os.getenv('OPENAI_API_KEY', '')
|
||||
OPENAI_API_BASE_URL = os.getenv('OPENAI_API_BASE_URL', '')
|
||||
OPENAI_API_KEY = os.environ.get('OPENAI_API_KEY', '')
|
||||
OPENAI_API_BASE_URL = os.environ.get('OPENAI_API_BASE_URL', '')
|
||||
|
||||
GEMINI_API_KEY = os.getenv('GEMINI_API_KEY', '')
|
||||
GEMINI_API_BASE_URL = os.getenv('GEMINI_API_BASE_URL', '')
|
||||
GEMINI_API_KEY = os.environ.get('GEMINI_API_KEY', '')
|
||||
GEMINI_API_BASE_URL = os.environ.get('GEMINI_API_BASE_URL', '')
|
||||
|
||||
|
||||
if OPENAI_API_BASE_URL == '':
|
||||
@@ -322,36 +1140,30 @@ else:
|
||||
if OPENAI_API_BASE_URL.endswith('/'):
|
||||
OPENAI_API_BASE_URL = OPENAI_API_BASE_URL[:-1]
|
||||
|
||||
OPENAI_API_KEYS = os.getenv('OPENAI_API_KEYS', '')
|
||||
OPENAI_API_KEYS = os.environ.get('OPENAI_API_KEYS', '')
|
||||
OPENAI_API_KEYS = OPENAI_API_KEYS if OPENAI_API_KEYS != '' else OPENAI_API_KEY
|
||||
|
||||
OPENAI_API_KEYS = [url.strip() for url in OPENAI_API_KEYS.split(';')]
|
||||
OPENAI_API_KEYS = OPENAI_API_KEYS
|
||||
OPENAI_API_KEYS = PersistentConfig('OPENAI_API_KEYS', 'openai.api_keys', OPENAI_API_KEYS)
|
||||
|
||||
OPENAI_API_BASE_URLS = os.getenv('OPENAI_API_BASE_URLS', '')
|
||||
OPENAI_API_BASE_URLS = os.environ.get('OPENAI_API_BASE_URLS', '')
|
||||
OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS if OPENAI_API_BASE_URLS != '' else OPENAI_API_BASE_URL
|
||||
|
||||
OPENAI_API_BASE_URLS = [
|
||||
url.strip() if url != '' else 'https://api.openai.com/v1' for url in OPENAI_API_BASE_URLS.split(';')
|
||||
]
|
||||
OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS
|
||||
OPENAI_API_BASE_URLS = PersistentConfig('OPENAI_API_BASE_URLS', 'openai.api_base_urls', OPENAI_API_BASE_URLS)
|
||||
|
||||
OPENAI_API_CONFIGS = {}
|
||||
_openai_api_configs = os.getenv('OPENAI_API_CONFIGS', '')
|
||||
if _openai_api_configs:
|
||||
try:
|
||||
parsed = json.loads(_openai_api_configs)
|
||||
if isinstance(parsed, dict):
|
||||
OPENAI_API_CONFIGS = parsed
|
||||
else:
|
||||
log.warning('OPENAI_API_CONFIGS must be a JSON object, ignoring')
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
log.warning('OPENAI_API_CONFIGS is not valid JSON, ignoring')
|
||||
OPENAI_API_CONFIGS = PersistentConfig(
|
||||
'OPENAI_API_CONFIGS',
|
||||
'openai.api_configs',
|
||||
{},
|
||||
)
|
||||
|
||||
# Get the actual OpenAI API key based on the base URL
|
||||
OPENAI_API_KEY = ''
|
||||
try:
|
||||
OPENAI_API_KEY = OPENAI_API_KEYS[OPENAI_API_BASE_URLS.index('https://api.openai.com/v1')]
|
||||
OPENAI_API_KEY = OPENAI_API_KEYS.value[OPENAI_API_BASE_URLS.value.index('https://api.openai.com/v1')]
|
||||
except Exception:
|
||||
pass
|
||||
OPENAI_API_BASE_URL = 'https://api.openai.com/v1'
|
||||
@@ -361,7 +1173,11 @@ OPENAI_API_BASE_URL = 'https://api.openai.com/v1'
|
||||
# MODELS
|
||||
####################################
|
||||
|
||||
ENABLE_BASE_MODELS_CACHE = os.getenv('ENABLE_BASE_MODELS_CACHE', 'False').lower() == 'true'
|
||||
ENABLE_BASE_MODELS_CACHE = PersistentConfig(
|
||||
'ENABLE_BASE_MODELS_CACHE',
|
||||
'models.base_models_cache',
|
||||
os.environ.get('ENABLE_BASE_MODELS_CACHE', 'False').lower() == 'true',
|
||||
)
|
||||
|
||||
|
||||
####################################
|
||||
@@ -369,88 +1185,1057 @@ ENABLE_BASE_MODELS_CACHE = os.getenv('ENABLE_BASE_MODELS_CACHE', 'False').lower(
|
||||
####################################
|
||||
|
||||
try:
|
||||
tool_server_connections = json.loads(os.getenv('TOOL_SERVER_CONNECTIONS', '[]'))
|
||||
tool_server_connections = json.loads(os.environ.get('TOOL_SERVER_CONNECTIONS', '[]'))
|
||||
except Exception as e:
|
||||
log.exception(f'Error loading TOOL_SERVER_CONNECTIONS: {e}')
|
||||
tool_server_connections = []
|
||||
|
||||
|
||||
TOOL_SERVER_CONNECTIONS = tool_server_connections
|
||||
|
||||
OAUTH_CLIENT_TIMEOUT = os.getenv('OAUTH_CLIENT_TIMEOUT', '')
|
||||
TOOL_SERVER_CONNECTIONS = PersistentConfig(
|
||||
'TOOL_SERVER_CONNECTIONS',
|
||||
'tool_server.connections',
|
||||
tool_server_connections,
|
||||
)
|
||||
|
||||
####################################
|
||||
# TERMINAL_SERVER
|
||||
####################################
|
||||
|
||||
terminal_server_connections = json.loads(os.getenv('TERMINAL_SERVER_CONNECTIONS', '[]'))
|
||||
terminal_server_connections = json.loads(os.environ.get('TERMINAL_SERVER_CONNECTIONS', '[]'))
|
||||
|
||||
TERMINAL_SERVER_CONNECTIONS = terminal_server_connections
|
||||
TERMINAL_SERVER_CONNECTIONS = PersistentConfig(
|
||||
'TERMINAL_SERVER_CONNECTIONS',
|
||||
'terminal_server.connections',
|
||||
terminal_server_connections,
|
||||
)
|
||||
|
||||
####################################
|
||||
# WEBUI
|
||||
####################################
|
||||
|
||||
|
||||
WEBUI_URL = PersistentConfig('WEBUI_URL', 'webui.url', os.environ.get('WEBUI_URL', ''))
|
||||
|
||||
|
||||
ENABLE_SIGNUP = PersistentConfig(
|
||||
'ENABLE_SIGNUP',
|
||||
'ui.enable_signup',
|
||||
(False if not WEBUI_AUTH else os.environ.get('ENABLE_SIGNUP', 'True').lower() == 'true'),
|
||||
)
|
||||
|
||||
ENABLE_LOGIN_FORM = PersistentConfig(
|
||||
'ENABLE_LOGIN_FORM',
|
||||
'ui.enable_login_form',
|
||||
os.environ.get('ENABLE_LOGIN_FORM', 'True').lower() == 'true',
|
||||
)
|
||||
|
||||
ENABLE_PASSWORD_CHANGE_FORM = PersistentConfig(
|
||||
'ENABLE_PASSWORD_CHANGE_FORM',
|
||||
'ui.enable_password_change_form',
|
||||
os.environ.get('ENABLE_PASSWORD_CHANGE_FORM', 'True').lower() == 'true',
|
||||
)
|
||||
|
||||
ENABLE_PASSWORD_AUTH = os.environ.get('ENABLE_PASSWORD_AUTH', 'True').lower() == 'true'
|
||||
|
||||
DEFAULT_LOCALE = PersistentConfig(
|
||||
'DEFAULT_LOCALE',
|
||||
'ui.default_locale',
|
||||
os.environ.get('DEFAULT_LOCALE', ''),
|
||||
)
|
||||
|
||||
DEFAULT_MODELS = PersistentConfig('DEFAULT_MODELS', 'ui.default_models', os.environ.get('DEFAULT_MODELS', None))
|
||||
|
||||
DEFAULT_PINNED_MODELS = PersistentConfig(
|
||||
'DEFAULT_PINNED_MODELS',
|
||||
'ui.default_pinned_models',
|
||||
os.environ.get('DEFAULT_PINNED_MODELS', None),
|
||||
)
|
||||
|
||||
try:
|
||||
TERMINAL_PROXY_HEADERS = json.loads(os.getenv('TERMINAL_PROXY_HEADERS', '{}'))
|
||||
except Exception:
|
||||
TERMINAL_PROXY_HEADERS = {}
|
||||
default_prompt_suggestions = json.loads(os.environ.get('DEFAULT_PROMPT_SUGGESTIONS', '[]'))
|
||||
except Exception as e:
|
||||
log.exception(f'Error loading DEFAULT_PROMPT_SUGGESTIONS: {e}')
|
||||
default_prompt_suggestions = []
|
||||
if default_prompt_suggestions == []:
|
||||
default_prompt_suggestions = [
|
||||
{
|
||||
'title': ['Help me study', 'vocabulary for a college entrance exam'],
|
||||
'content': "Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option.",
|
||||
},
|
||||
{
|
||||
'title': ['Give me ideas', "for what to do with my kids' art"],
|
||||
'content': "What are 5 creative things I could do with my kids' art? I don't want to throw them away, but it's also so much clutter.",
|
||||
},
|
||||
{
|
||||
'title': ['Tell me a fun fact', 'about the Roman Empire'],
|
||||
'content': 'Tell me a random fun fact about the Roman Empire',
|
||||
},
|
||||
{
|
||||
'title': ['Show me a code snippet', "of a website's sticky header"],
|
||||
'content': "Show me a code snippet of a website's sticky header in CSS and JavaScript.",
|
||||
},
|
||||
{
|
||||
'title': [
|
||||
'Explain options trading',
|
||||
"if I'm familiar with buying and selling stocks",
|
||||
],
|
||||
'content': "Explain options trading in simple terms if I'm familiar with buying and selling stocks.",
|
||||
},
|
||||
{
|
||||
'title': ['Overcome procrastination', 'give me tips'],
|
||||
'content': 'Could you start by asking me about instances when I procrastinate the most and then give me some suggestions to overcome it?',
|
||||
},
|
||||
]
|
||||
|
||||
DEFAULT_PROMPT_SUGGESTIONS = PersistentConfig(
|
||||
'DEFAULT_PROMPT_SUGGESTIONS',
|
||||
'ui.prompt_suggestions',
|
||||
default_prompt_suggestions,
|
||||
)
|
||||
|
||||
MODEL_ORDER_LIST = PersistentConfig(
|
||||
'MODEL_ORDER_LIST',
|
||||
'ui.model_order_list',
|
||||
[],
|
||||
)
|
||||
|
||||
DEFAULT_MODEL_METADATA = PersistentConfig(
|
||||
'DEFAULT_MODEL_METADATA',
|
||||
'models.default_metadata',
|
||||
{},
|
||||
)
|
||||
|
||||
try:
|
||||
default_model_params = json.loads(os.environ.get('DEFAULT_MODEL_PARAMS', '{}'))
|
||||
except Exception as e:
|
||||
log.exception(f'Error loading DEFAULT_MODEL_PARAMS: {e}')
|
||||
default_model_params = {}
|
||||
|
||||
DEFAULT_MODEL_PARAMS = PersistentConfig(
|
||||
'DEFAULT_MODEL_PARAMS',
|
||||
'models.default_params',
|
||||
default_model_params,
|
||||
)
|
||||
|
||||
DEFAULT_USER_ROLE = PersistentConfig(
|
||||
'DEFAULT_USER_ROLE',
|
||||
'ui.default_user_role',
|
||||
os.getenv('DEFAULT_USER_ROLE', 'pending'),
|
||||
)
|
||||
|
||||
DEFAULT_GROUP_ID = PersistentConfig(
|
||||
'DEFAULT_GROUP_ID',
|
||||
'ui.default_group_id',
|
||||
os.environ.get('DEFAULT_GROUP_ID', ''),
|
||||
)
|
||||
|
||||
PENDING_USER_OVERLAY_TITLE = PersistentConfig(
|
||||
'PENDING_USER_OVERLAY_TITLE',
|
||||
'ui.pending_user_overlay_title',
|
||||
os.environ.get('PENDING_USER_OVERLAY_TITLE', ''),
|
||||
)
|
||||
|
||||
PENDING_USER_OVERLAY_CONTENT = PersistentConfig(
|
||||
'PENDING_USER_OVERLAY_CONTENT',
|
||||
'ui.pending_user_overlay_content',
|
||||
os.environ.get('PENDING_USER_OVERLAY_CONTENT', ''),
|
||||
)
|
||||
|
||||
|
||||
RESPONSE_WATERMARK = PersistentConfig(
|
||||
'RESPONSE_WATERMARK',
|
||||
'ui.watermark',
|
||||
os.environ.get('RESPONSE_WATERMARK', ''),
|
||||
)
|
||||
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS = (
|
||||
os.environ.get('USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ACCESS = (
|
||||
os.environ.get('USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ACCESS', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_PROMPTS_ACCESS = (
|
||||
os.environ.get('USER_PERMISSIONS_WORKSPACE_PROMPTS_ACCESS', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS = (
|
||||
os.environ.get('USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_SKILLS_ACCESS = (
|
||||
os.environ.get('USER_PERMISSIONS_WORKSPACE_SKILLS_ACCESS', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_MODELS_IMPORT = (
|
||||
os.environ.get('USER_PERMISSIONS_WORKSPACE_MODELS_IMPORT', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_MODELS_EXPORT = (
|
||||
os.environ.get('USER_PERMISSIONS_WORKSPACE_MODELS_EXPORT', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_PROMPTS_IMPORT = (
|
||||
os.environ.get('USER_PERMISSIONS_WORKSPACE_PROMPTS_IMPORT', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_PROMPTS_EXPORT = (
|
||||
os.environ.get('USER_PERMISSIONS_WORKSPACE_PROMPTS_EXPORT', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_TOOLS_IMPORT = (
|
||||
os.environ.get('USER_PERMISSIONS_WORKSPACE_TOOLS_IMPORT', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_TOOLS_EXPORT = (
|
||||
os.environ.get('USER_PERMISSIONS_WORKSPACE_TOOLS_EXPORT', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_SHARING = (
|
||||
os.environ.get('USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_SHARING', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_PUBLIC_SHARING = (
|
||||
os.environ.get('USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_SHARING = (
|
||||
os.environ.get('USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_SHARING', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_PUBLIC_SHARING = (
|
||||
os.environ.get('USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_SHARING = (
|
||||
os.environ.get('USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_SHARING', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_PUBLIC_SHARING = (
|
||||
os.environ.get('USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_SHARING = (
|
||||
os.environ.get('USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_SHARING', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_PUBLIC_SHARING = (
|
||||
os.environ.get('USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_SHARING = (
|
||||
os.environ.get('USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_SHARING', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_PUBLIC_SHARING = (
|
||||
os.environ.get('USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
|
||||
USER_PERMISSIONS_NOTES_ALLOW_SHARING = os.environ.get('USER_PERMISSIONS_NOTES_ALLOW_SHARING', 'False').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING = (
|
||||
os.environ.get('USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS = (
|
||||
os.environ.get('USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS', 'True').lower() == 'true'
|
||||
)
|
||||
|
||||
|
||||
USER_PERMISSIONS_CHAT_CONTROLS = os.environ.get('USER_PERMISSIONS_CHAT_CONTROLS', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_CHAT_VALVES = os.environ.get('USER_PERMISSIONS_CHAT_VALVES', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_CHAT_SYSTEM_PROMPT = os.environ.get('USER_PERMISSIONS_CHAT_SYSTEM_PROMPT', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_CHAT_PARAMS = os.environ.get('USER_PERMISSIONS_CHAT_PARAMS', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_CHAT_FILE_UPLOAD = os.environ.get('USER_PERMISSIONS_CHAT_FILE_UPLOAD', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_CHAT_WEB_UPLOAD = os.environ.get('USER_PERMISSIONS_CHAT_WEB_UPLOAD', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_CHAT_DELETE = os.environ.get('USER_PERMISSIONS_CHAT_DELETE', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_CHAT_DELETE_MESSAGE = os.environ.get('USER_PERMISSIONS_CHAT_DELETE_MESSAGE', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_CHAT_CONTINUE_RESPONSE = (
|
||||
os.environ.get('USER_PERMISSIONS_CHAT_CONTINUE_RESPONSE', 'True').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_CHAT_REGENERATE_RESPONSE = (
|
||||
os.environ.get('USER_PERMISSIONS_CHAT_REGENERATE_RESPONSE', 'True').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_CHAT_RATE_RESPONSE = os.environ.get('USER_PERMISSIONS_CHAT_RATE_RESPONSE', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_CHAT_EDIT = os.environ.get('USER_PERMISSIONS_CHAT_EDIT', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_CHAT_SHARE = os.environ.get('USER_PERMISSIONS_CHAT_SHARE', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_CHAT_EXPORT = os.environ.get('USER_PERMISSIONS_CHAT_EXPORT', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_CHAT_STT = os.environ.get('USER_PERMISSIONS_CHAT_STT', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_CHAT_TTS = os.environ.get('USER_PERMISSIONS_CHAT_TTS', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_CHAT_CALL = os.environ.get('USER_PERMISSIONS_CHAT_CALL', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_CHAT_MULTIPLE_MODELS = (
|
||||
os.environ.get('USER_PERMISSIONS_CHAT_MULTIPLE_MODELS', 'True').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_CHAT_TEMPORARY = os.environ.get('USER_PERMISSIONS_CHAT_TEMPORARY', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_CHAT_TEMPORARY_ENFORCED = (
|
||||
os.environ.get('USER_PERMISSIONS_CHAT_TEMPORARY_ENFORCED', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
|
||||
USER_PERMISSIONS_FEATURES_DIRECT_TOOL_SERVERS = (
|
||||
os.environ.get('USER_PERMISSIONS_FEATURES_DIRECT_TOOL_SERVERS', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_FEATURES_WEB_SEARCH = os.environ.get('USER_PERMISSIONS_FEATURES_WEB_SEARCH', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_FEATURES_IMAGE_GENERATION = (
|
||||
os.environ.get('USER_PERMISSIONS_FEATURES_IMAGE_GENERATION', 'True').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_FEATURES_CODE_INTERPRETER = (
|
||||
os.environ.get('USER_PERMISSIONS_FEATURES_CODE_INTERPRETER', 'True').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_FEATURES_FOLDERS = os.environ.get('USER_PERMISSIONS_FEATURES_FOLDERS', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_FEATURES_NOTES = os.environ.get('USER_PERMISSIONS_FEATURES_NOTES', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_FEATURES_CHANNELS = os.environ.get('USER_PERMISSIONS_FEATURES_CHANNELS', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_FEATURES_API_KEYS = os.environ.get('USER_PERMISSIONS_FEATURES_API_KEYS', 'False').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_FEATURES_MEMORIES = os.environ.get('USER_PERMISSIONS_FEATURES_MEMORIES', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_FEATURES_AUTOMATIONS = (
|
||||
os.environ.get('USER_PERMISSIONS_FEATURES_AUTOMATIONS', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_FEATURES_CALENDAR = os.environ.get('USER_PERMISSIONS_FEATURES_CALENDAR', 'True').lower() == 'true'
|
||||
|
||||
|
||||
USER_PERMISSIONS_SETTINGS_INTERFACE = os.environ.get('USER_PERMISSIONS_SETTINGS_INTERFACE', 'True').lower() == 'true'
|
||||
|
||||
|
||||
DEFAULT_USER_PERMISSIONS = {
|
||||
'workspace': {
|
||||
'models': USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS,
|
||||
'knowledge': USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ACCESS,
|
||||
'prompts': USER_PERMISSIONS_WORKSPACE_PROMPTS_ACCESS,
|
||||
'tools': USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS,
|
||||
'skills': USER_PERMISSIONS_WORKSPACE_SKILLS_ACCESS,
|
||||
'models_import': USER_PERMISSIONS_WORKSPACE_MODELS_IMPORT,
|
||||
'models_export': USER_PERMISSIONS_WORKSPACE_MODELS_EXPORT,
|
||||
'prompts_import': USER_PERMISSIONS_WORKSPACE_PROMPTS_IMPORT,
|
||||
'prompts_export': USER_PERMISSIONS_WORKSPACE_PROMPTS_EXPORT,
|
||||
'tools_import': USER_PERMISSIONS_WORKSPACE_TOOLS_IMPORT,
|
||||
'tools_export': USER_PERMISSIONS_WORKSPACE_TOOLS_EXPORT,
|
||||
},
|
||||
'sharing': {
|
||||
'models': USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_SHARING,
|
||||
'public_models': USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_PUBLIC_SHARING,
|
||||
'knowledge': USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_SHARING,
|
||||
'public_knowledge': USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_PUBLIC_SHARING,
|
||||
'prompts': USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_SHARING,
|
||||
'public_prompts': USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_PUBLIC_SHARING,
|
||||
'tools': USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_SHARING,
|
||||
'public_tools': USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_PUBLIC_SHARING,
|
||||
'skills': USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_SHARING,
|
||||
'public_skills': USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_PUBLIC_SHARING,
|
||||
'notes': USER_PERMISSIONS_NOTES_ALLOW_SHARING,
|
||||
'public_notes': USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING,
|
||||
},
|
||||
'access_grants': {
|
||||
'allow_users': USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS,
|
||||
},
|
||||
'chat': {
|
||||
'controls': USER_PERMISSIONS_CHAT_CONTROLS,
|
||||
'valves': USER_PERMISSIONS_CHAT_VALVES,
|
||||
'system_prompt': USER_PERMISSIONS_CHAT_SYSTEM_PROMPT,
|
||||
'params': USER_PERMISSIONS_CHAT_PARAMS,
|
||||
'file_upload': USER_PERMISSIONS_CHAT_FILE_UPLOAD,
|
||||
'web_upload': USER_PERMISSIONS_CHAT_WEB_UPLOAD,
|
||||
'delete': USER_PERMISSIONS_CHAT_DELETE,
|
||||
'delete_message': USER_PERMISSIONS_CHAT_DELETE_MESSAGE,
|
||||
'continue_response': USER_PERMISSIONS_CHAT_CONTINUE_RESPONSE,
|
||||
'regenerate_response': USER_PERMISSIONS_CHAT_REGENERATE_RESPONSE,
|
||||
'rate_response': USER_PERMISSIONS_CHAT_RATE_RESPONSE,
|
||||
'edit': USER_PERMISSIONS_CHAT_EDIT,
|
||||
'share': USER_PERMISSIONS_CHAT_SHARE,
|
||||
'export': USER_PERMISSIONS_CHAT_EXPORT,
|
||||
'stt': USER_PERMISSIONS_CHAT_STT,
|
||||
'tts': USER_PERMISSIONS_CHAT_TTS,
|
||||
'call': USER_PERMISSIONS_CHAT_CALL,
|
||||
'multiple_models': USER_PERMISSIONS_CHAT_MULTIPLE_MODELS,
|
||||
'temporary': USER_PERMISSIONS_CHAT_TEMPORARY,
|
||||
'temporary_enforced': USER_PERMISSIONS_CHAT_TEMPORARY_ENFORCED,
|
||||
},
|
||||
'features': {
|
||||
# General features
|
||||
'api_keys': USER_PERMISSIONS_FEATURES_API_KEYS,
|
||||
'notes': USER_PERMISSIONS_FEATURES_NOTES,
|
||||
'folders': USER_PERMISSIONS_FEATURES_FOLDERS,
|
||||
'channels': USER_PERMISSIONS_FEATURES_CHANNELS,
|
||||
'direct_tool_servers': USER_PERMISSIONS_FEATURES_DIRECT_TOOL_SERVERS,
|
||||
# Chat features
|
||||
'web_search': USER_PERMISSIONS_FEATURES_WEB_SEARCH,
|
||||
'image_generation': USER_PERMISSIONS_FEATURES_IMAGE_GENERATION,
|
||||
'code_interpreter': USER_PERMISSIONS_FEATURES_CODE_INTERPRETER,
|
||||
'memories': USER_PERMISSIONS_FEATURES_MEMORIES,
|
||||
'automations': USER_PERMISSIONS_FEATURES_AUTOMATIONS,
|
||||
'calendar': USER_PERMISSIONS_FEATURES_CALENDAR,
|
||||
},
|
||||
'settings': {
|
||||
'interface': USER_PERMISSIONS_SETTINGS_INTERFACE,
|
||||
},
|
||||
}
|
||||
|
||||
USER_PERMISSIONS = PersistentConfig(
|
||||
'USER_PERMISSIONS',
|
||||
'user.permissions',
|
||||
DEFAULT_USER_PERMISSIONS,
|
||||
)
|
||||
|
||||
ENABLE_FOLDERS = PersistentConfig(
|
||||
'ENABLE_FOLDERS',
|
||||
'folders.enable',
|
||||
os.environ.get('ENABLE_FOLDERS', 'True').lower() == 'true',
|
||||
)
|
||||
|
||||
FOLDER_MAX_FILE_COUNT = PersistentConfig(
|
||||
'FOLDER_MAX_FILE_COUNT',
|
||||
'folders.max_file_count',
|
||||
os.environ.get('FOLDER_MAX_FILE_COUNT', ''),
|
||||
)
|
||||
|
||||
ENABLE_CHANNELS = PersistentConfig(
|
||||
'ENABLE_CHANNELS',
|
||||
'channels.enable',
|
||||
os.environ.get('ENABLE_CHANNELS', 'False').lower() == 'true',
|
||||
)
|
||||
|
||||
ENABLE_CALENDAR = PersistentConfig(
|
||||
'ENABLE_CALENDAR',
|
||||
'calendar.enable',
|
||||
os.environ.get('ENABLE_CALENDAR', 'True').lower() == 'true',
|
||||
)
|
||||
|
||||
ENABLE_AUTOMATIONS = PersistentConfig(
|
||||
'ENABLE_AUTOMATIONS',
|
||||
'automations.enable',
|
||||
os.environ.get('ENABLE_AUTOMATIONS', 'True').lower() == 'true',
|
||||
)
|
||||
|
||||
AUTOMATION_MAX_COUNT = PersistentConfig(
|
||||
'AUTOMATION_MAX_COUNT',
|
||||
'automations.max_count',
|
||||
os.environ.get('AUTOMATION_MAX_COUNT', ''),
|
||||
)
|
||||
|
||||
AUTOMATION_MIN_INTERVAL = PersistentConfig(
|
||||
'AUTOMATION_MIN_INTERVAL',
|
||||
'automations.min_interval',
|
||||
os.environ.get('AUTOMATION_MIN_INTERVAL', ''),
|
||||
)
|
||||
|
||||
ENABLE_NOTES = PersistentConfig(
|
||||
'ENABLE_NOTES',
|
||||
'notes.enable',
|
||||
os.environ.get('ENABLE_NOTES', 'True').lower() == 'true',
|
||||
)
|
||||
|
||||
ENABLE_USER_STATUS = PersistentConfig(
|
||||
'ENABLE_USER_STATUS',
|
||||
'users.enable_status',
|
||||
os.environ.get('ENABLE_USER_STATUS', 'True').lower() == 'true',
|
||||
)
|
||||
|
||||
ENABLE_EVALUATION_ARENA_MODELS = PersistentConfig(
|
||||
'ENABLE_EVALUATION_ARENA_MODELS',
|
||||
'evaluation.arena.enable',
|
||||
os.environ.get('ENABLE_EVALUATION_ARENA_MODELS', 'True').lower() == 'true',
|
||||
)
|
||||
EVALUATION_ARENA_MODELS = PersistentConfig(
|
||||
'EVALUATION_ARENA_MODELS',
|
||||
'evaluation.arena.models',
|
||||
[],
|
||||
)
|
||||
|
||||
DEFAULT_ARENA_MODEL = {
|
||||
'id': 'arena-model',
|
||||
'name': 'Arena Model',
|
||||
'meta': {
|
||||
'profile_image_url': '/favicon.png',
|
||||
'description': 'Submit your questions to anonymous AI chatbots and vote on the best response.',
|
||||
'model_ids': None,
|
||||
},
|
||||
}
|
||||
|
||||
WEBHOOK_URL = PersistentConfig('WEBHOOK_URL', 'webhook_url', os.environ.get('WEBHOOK_URL', ''))
|
||||
|
||||
ENABLE_ADMIN_EXPORT = os.environ.get('ENABLE_ADMIN_EXPORT', 'True').lower() == 'true'
|
||||
|
||||
ENABLE_ADMIN_WORKSPACE_CONTENT_ACCESS = (
|
||||
os.environ.get('ENABLE_ADMIN_WORKSPACE_CONTENT_ACCESS', 'True').lower() == 'true'
|
||||
)
|
||||
|
||||
BYPASS_ADMIN_ACCESS_CONTROL = (
|
||||
os.environ.get(
|
||||
'BYPASS_ADMIN_ACCESS_CONTROL',
|
||||
os.environ.get('ENABLE_ADMIN_WORKSPACE_CONTENT_ACCESS', 'True'),
|
||||
).lower()
|
||||
== 'true'
|
||||
)
|
||||
|
||||
ENABLE_ADMIN_CHAT_ACCESS = os.environ.get('ENABLE_ADMIN_CHAT_ACCESS', 'True').lower() == 'true'
|
||||
|
||||
ENABLE_ADMIN_ANALYTICS = os.environ.get('ENABLE_ADMIN_ANALYTICS', 'True').lower() == 'true'
|
||||
|
||||
ENABLE_COMMUNITY_SHARING = PersistentConfig(
|
||||
'ENABLE_COMMUNITY_SHARING',
|
||||
'ui.enable_community_sharing',
|
||||
os.environ.get('ENABLE_COMMUNITY_SHARING', 'True').lower() == 'true',
|
||||
)
|
||||
|
||||
ENABLE_MESSAGE_RATING = PersistentConfig(
|
||||
'ENABLE_MESSAGE_RATING',
|
||||
'ui.enable_message_rating',
|
||||
os.environ.get('ENABLE_MESSAGE_RATING', 'True').lower() == 'true',
|
||||
)
|
||||
|
||||
ENABLE_USER_WEBHOOKS = PersistentConfig(
|
||||
'ENABLE_USER_WEBHOOKS',
|
||||
'ui.enable_user_webhooks',
|
||||
os.environ.get('ENABLE_USER_WEBHOOKS', 'False').lower() == 'true',
|
||||
)
|
||||
|
||||
# FastAPI / AnyIO settings
|
||||
THREAD_POOL_SIZE = os.getenv('THREAD_POOL_SIZE', None)
|
||||
|
||||
if THREAD_POOL_SIZE is not None and isinstance(THREAD_POOL_SIZE, str):
|
||||
try:
|
||||
THREAD_POOL_SIZE = int(THREAD_POOL_SIZE)
|
||||
except ValueError:
|
||||
log.warning(f'THREAD_POOL_SIZE is not a valid integer: {THREAD_POOL_SIZE}. Defaulting to None.')
|
||||
THREAD_POOL_SIZE = None
|
||||
|
||||
|
||||
def validate_cors_origin(origin):
|
||||
parsed_url = urlparse(origin)
|
||||
|
||||
# Check if the scheme is either http or https, or a custom scheme
|
||||
schemes = ['http', 'https'] + CORS_ALLOW_CUSTOM_SCHEME
|
||||
if parsed_url.scheme not in schemes:
|
||||
raise ValueError(
|
||||
f"Invalid scheme in CORS_ALLOW_ORIGIN: '{origin}'. Only 'http' and 'https' and CORS_ALLOW_CUSTOM_SCHEME are allowed."
|
||||
)
|
||||
|
||||
# Ensure that the netloc (domain + port) is present, indicating it's a valid URL
|
||||
if not parsed_url.netloc:
|
||||
raise ValueError(f"Invalid URL structure in CORS_ALLOW_ORIGIN: '{origin}'.")
|
||||
|
||||
|
||||
# For production, you should only need one host as
|
||||
# fastapi serves the svelte-kit built frontend and backend from the same host and port.
|
||||
# To test CORS_ALLOW_ORIGIN locally, you can set something like
|
||||
# CORS_ALLOW_ORIGIN=http://localhost:5173;http://localhost:8080
|
||||
# in your .env file depending on your frontend port, 5173 in this case.
|
||||
CORS_ALLOW_ORIGIN = os.environ.get('CORS_ALLOW_ORIGIN', '*').split(';')
|
||||
|
||||
# Allows custom URL schemes (e.g., app://) to be used as origins for CORS.
|
||||
# Useful for local development or desktop clients with schemes like app:// or other custom protocols.
|
||||
# Provide a semicolon-separated list of allowed schemes in the environment variable CORS_ALLOW_CUSTOM_SCHEMES.
|
||||
CORS_ALLOW_CUSTOM_SCHEME = os.environ.get('CORS_ALLOW_CUSTOM_SCHEME', '').split(';')
|
||||
|
||||
if CORS_ALLOW_ORIGIN == ['*']:
|
||||
log.warning("\n\nWARNING: CORS_ALLOW_ORIGIN IS SET TO '*' - NOT RECOMMENDED FOR PRODUCTION DEPLOYMENTS.\n")
|
||||
else:
|
||||
# You have to pick between a single wildcard or a list of origins.
|
||||
# Doing both will result in CORS errors in the browser.
|
||||
for origin in CORS_ALLOW_ORIGIN:
|
||||
validate_cors_origin(origin)
|
||||
|
||||
|
||||
class BannerModel(BaseModel):
|
||||
id: str
|
||||
type: str
|
||||
title: Optional[str] = None
|
||||
content: str
|
||||
dismissible: bool
|
||||
timestamp: int
|
||||
|
||||
|
||||
try:
|
||||
banners = json.loads(os.environ.get('WEBUI_BANNERS', '[]'))
|
||||
banners = [BannerModel(**banner) for banner in banners]
|
||||
except Exception as e:
|
||||
log.exception(f'Error loading WEBUI_BANNERS: {e}')
|
||||
banners = []
|
||||
|
||||
WEBUI_BANNERS = PersistentConfig('WEBUI_BANNERS', 'ui.banners', banners)
|
||||
|
||||
|
||||
SHOW_ADMIN_DETAILS = PersistentConfig(
|
||||
'SHOW_ADMIN_DETAILS',
|
||||
'auth.admin.show',
|
||||
os.environ.get('SHOW_ADMIN_DETAILS', 'true').lower() == 'true',
|
||||
)
|
||||
|
||||
ADMIN_EMAIL = PersistentConfig(
|
||||
'ADMIN_EMAIL',
|
||||
'auth.admin.email',
|
||||
os.environ.get('ADMIN_EMAIL', None),
|
||||
)
|
||||
|
||||
|
||||
####################################
|
||||
# TASKS
|
||||
####################################
|
||||
|
||||
|
||||
TASK_MODEL = PersistentConfig(
|
||||
'TASK_MODEL',
|
||||
'task.model.default',
|
||||
os.environ.get('TASK_MODEL', ''),
|
||||
)
|
||||
|
||||
TASK_MODEL_EXTERNAL = PersistentConfig(
|
||||
'TASK_MODEL_EXTERNAL',
|
||||
'task.model.external',
|
||||
os.environ.get('TASK_MODEL_EXTERNAL', ''),
|
||||
)
|
||||
|
||||
TITLE_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
|
||||
'TITLE_GENERATION_PROMPT_TEMPLATE',
|
||||
'task.title.prompt_template',
|
||||
os.environ.get('TITLE_GENERATION_PROMPT_TEMPLATE', ''),
|
||||
)
|
||||
|
||||
DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE = """### Task:
|
||||
Generate a concise, 3-5 word title with an emoji summarizing the chat history.
|
||||
### Guidelines:
|
||||
- The title should clearly represent the main theme or subject of the conversation.
|
||||
- Use emojis that enhance understanding of the topic, but avoid quotation marks or special formatting.
|
||||
- Write the title in the chat's primary language; default to English if multilingual.
|
||||
- Prioritize accuracy over excessive creativity; keep it clear and simple.
|
||||
- Your entire response must consist solely of the JSON object, without any introductory or concluding text.
|
||||
- The output must be a single, raw JSON object, without any markdown code fences or other encapsulating text.
|
||||
- Ensure no conversational text, affirmations, or explanations precede or follow the raw JSON output, as this will cause direct parsing failure.
|
||||
### Output:
|
||||
JSON format: { "title": "your concise title here" }
|
||||
### Examples:
|
||||
- { "title": "📉 Stock Market Trends" },
|
||||
- { "title": "🍪 Perfect Chocolate Chip Recipe" },
|
||||
- { "title": "Evolution of Music Streaming" },
|
||||
- { "title": "Remote Work Productivity Tips" },
|
||||
- { "title": "Artificial Intelligence in Healthcare" },
|
||||
- { "title": "🎮 Video Game Development Insights" }
|
||||
### Chat History:
|
||||
<chat_history>
|
||||
{{MESSAGES:END:2}}
|
||||
</chat_history>"""
|
||||
|
||||
TAGS_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
|
||||
'TAGS_GENERATION_PROMPT_TEMPLATE',
|
||||
'task.tags.prompt_template',
|
||||
os.environ.get('TAGS_GENERATION_PROMPT_TEMPLATE', ''),
|
||||
)
|
||||
|
||||
DEFAULT_TAGS_GENERATION_PROMPT_TEMPLATE = """### Task:
|
||||
Generate 1-3 broad tags categorizing the main themes of the chat history, along with 1-3 more specific subtopic tags.
|
||||
|
||||
### Guidelines:
|
||||
- Start with high-level domains (e.g. Science, Technology, Philosophy, Arts, Politics, Business, Health, Sports, Entertainment, Education)
|
||||
- Consider including relevant subfields/subdomains if they are strongly represented throughout the conversation
|
||||
- If content is too short (less than 3 messages) or too diverse, use only ["General"]
|
||||
- Use the chat's primary language; default to English if multilingual
|
||||
- Prioritize accuracy over specificity
|
||||
|
||||
### Output:
|
||||
JSON format: { "tags": ["tag1", "tag2", "tag3"] }
|
||||
|
||||
### Chat History:
|
||||
<chat_history>
|
||||
{{MESSAGES:END:6}}
|
||||
</chat_history>"""
|
||||
|
||||
IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
|
||||
'IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE',
|
||||
'task.image.prompt_template',
|
||||
os.environ.get('IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE', ''),
|
||||
)
|
||||
|
||||
DEFAULT_IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE = """### Task:
|
||||
Generate a detailed prompt for am image generation task based on the given language and context. Describe the image as if you were explaining it to someone who cannot see it. Include relevant details, colors, shapes, and any other important elements.
|
||||
|
||||
### Guidelines:
|
||||
- Be descriptive and detailed, focusing on the most important aspects of the image.
|
||||
- Avoid making assumptions or adding information not present in the image.
|
||||
- Use the chat's primary language; default to English if multilingual.
|
||||
- If the image is too complex, focus on the most prominent elements.
|
||||
|
||||
### Output:
|
||||
Strictly return in JSON format:
|
||||
{
|
||||
"prompt": "Your detailed description here."
|
||||
}
|
||||
|
||||
### Chat History:
|
||||
<chat_history>
|
||||
{{MESSAGES:END:6}}
|
||||
</chat_history>"""
|
||||
|
||||
|
||||
FOLLOW_UP_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
|
||||
'FOLLOW_UP_GENERATION_PROMPT_TEMPLATE',
|
||||
'task.follow_up.prompt_template',
|
||||
os.environ.get('FOLLOW_UP_GENERATION_PROMPT_TEMPLATE', ''),
|
||||
)
|
||||
|
||||
DEFAULT_FOLLOW_UP_GENERATION_PROMPT_TEMPLATE = """### Task:
|
||||
Suggest 3-5 relevant follow-up questions or prompts that the user might naturally ask next in this conversation as a **user**, based on the chat history, to help continue or deepen the discussion.
|
||||
### Guidelines:
|
||||
- Write all follow-up questions from the user’s point of view, directed to the assistant.
|
||||
- Make questions concise, clear, and directly related to the discussed topic(s).
|
||||
- Only suggest follow-ups that make sense given the chat content and do not repeat what was already covered.
|
||||
- If the conversation is very short or not specific, suggest more general (but relevant) follow-ups the user might ask.
|
||||
- Use the conversation's primary language; default to English if multilingual.
|
||||
- Response must be a JSON object with a "follow_ups" key containing an array of strings, no extra text or formatting.
|
||||
### Output:
|
||||
JSON format: { "follow_ups": ["Question 1?", "Question 2?", "Question 3?"] }
|
||||
### Chat History:
|
||||
<chat_history>
|
||||
{{MESSAGES:END:6}}
|
||||
</chat_history>"""
|
||||
|
||||
ENABLE_FOLLOW_UP_GENERATION = PersistentConfig(
|
||||
'ENABLE_FOLLOW_UP_GENERATION',
|
||||
'task.follow_up.enable',
|
||||
os.environ.get('ENABLE_FOLLOW_UP_GENERATION', 'True').lower() == 'true',
|
||||
)
|
||||
|
||||
ENABLE_TAGS_GENERATION = PersistentConfig(
|
||||
'ENABLE_TAGS_GENERATION',
|
||||
'task.tags.enable',
|
||||
os.environ.get('ENABLE_TAGS_GENERATION', 'True').lower() == 'true',
|
||||
)
|
||||
|
||||
ENABLE_TITLE_GENERATION = PersistentConfig(
|
||||
'ENABLE_TITLE_GENERATION',
|
||||
'task.title.enable',
|
||||
os.environ.get('ENABLE_TITLE_GENERATION', 'True').lower() == 'true',
|
||||
)
|
||||
|
||||
|
||||
ENABLE_SEARCH_QUERY_GENERATION = PersistentConfig(
|
||||
'ENABLE_SEARCH_QUERY_GENERATION',
|
||||
'task.query.search.enable',
|
||||
os.environ.get('ENABLE_SEARCH_QUERY_GENERATION', 'True').lower() == 'true',
|
||||
)
|
||||
|
||||
ENABLE_RETRIEVAL_QUERY_GENERATION = PersistentConfig(
|
||||
'ENABLE_RETRIEVAL_QUERY_GENERATION',
|
||||
'task.query.retrieval.enable',
|
||||
os.environ.get('ENABLE_RETRIEVAL_QUERY_GENERATION', 'True').lower() == 'true',
|
||||
)
|
||||
|
||||
|
||||
QUERY_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
|
||||
'QUERY_GENERATION_PROMPT_TEMPLATE',
|
||||
'task.query.prompt_template',
|
||||
os.environ.get('QUERY_GENERATION_PROMPT_TEMPLATE', ''),
|
||||
)
|
||||
|
||||
DEFAULT_QUERY_GENERATION_PROMPT_TEMPLATE = """### Task:
|
||||
Analyze the chat history to determine the necessity of generating search queries, in the given language. By default, **prioritize generating 1-3 broad and relevant search queries** unless it is absolutely certain that no additional information is required. The aim is to retrieve comprehensive, updated, and valuable information even with minimal uncertainty. If no search is unequivocally needed, return an empty list.
|
||||
|
||||
### Guidelines:
|
||||
- Respond **EXCLUSIVELY** with a JSON object. Any form of extra commentary, explanation, or additional text is strictly prohibited.
|
||||
- When generating search queries, respond in the format: { "queries": ["query1", "query2"] }, ensuring each query is distinct, concise, and relevant to the topic.
|
||||
- If and only if it is entirely certain that no useful results can be retrieved by a search, return: { "queries": [] }.
|
||||
- Err on the side of suggesting search queries if there is **any chance** they might provide useful or updated information.
|
||||
- Be concise and focused on composing high-quality search queries, avoiding unnecessary elaboration, commentary, or assumptions.
|
||||
- Today's date is: {{CURRENT_DATE}}.
|
||||
- Always prioritize providing actionable and broad queries that maximize informational coverage.
|
||||
|
||||
### Output:
|
||||
Strictly return in JSON format:
|
||||
{
|
||||
"queries": ["query1", "query2"]
|
||||
}
|
||||
|
||||
### Chat History:
|
||||
<chat_history>
|
||||
{{MESSAGES:END:6}}
|
||||
</chat_history>
|
||||
"""
|
||||
|
||||
ENABLE_AUTOCOMPLETE_GENERATION = PersistentConfig(
|
||||
'ENABLE_AUTOCOMPLETE_GENERATION',
|
||||
'task.autocomplete.enable',
|
||||
os.environ.get('ENABLE_AUTOCOMPLETE_GENERATION', 'False').lower() == 'true',
|
||||
)
|
||||
|
||||
AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH = PersistentConfig(
|
||||
'AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH',
|
||||
'task.autocomplete.input_max_length',
|
||||
int(os.environ.get('AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH', '-1')),
|
||||
)
|
||||
|
||||
AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
|
||||
'AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE',
|
||||
'task.autocomplete.prompt_template',
|
||||
os.environ.get('AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE', ''),
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE = """### Task:
|
||||
You are an autocompletion system. Continue the text in `<text>` based on the **completion type** in `<type>` and the given language.
|
||||
|
||||
### **Instructions**:
|
||||
1. Analyze `<text>` for context and meaning.
|
||||
2. Use `<type>` to guide your output:
|
||||
- **General**: Provide a natural, concise continuation.
|
||||
- **Search Query**: Complete as if generating a realistic search query.
|
||||
3. Start as if you are directly continuing `<text>`. Do **not** repeat, paraphrase, or respond as a model. Simply complete the text.
|
||||
4. Ensure the continuation:
|
||||
- Flows naturally from `<text>`.
|
||||
- Avoids repetition, overexplaining, or unrelated ideas.
|
||||
5. If unsure, return: `{ "text": "" }`.
|
||||
|
||||
### **Output Rules**:
|
||||
- Respond only in JSON format: `{ "text": "<your_completion>" }`.
|
||||
|
||||
### **Examples**:
|
||||
#### Example 1:
|
||||
Input:
|
||||
<type>General</type>
|
||||
<text>The sun was setting over the horizon, painting the sky</text>
|
||||
Output:
|
||||
{ "text": "with vibrant shades of orange and pink." }
|
||||
|
||||
#### Example 2:
|
||||
Input:
|
||||
<type>Search Query</type>
|
||||
<text>Top-rated restaurants in</text>
|
||||
Output:
|
||||
{ "text": "New York City for Italian cuisine." }
|
||||
|
||||
---
|
||||
### Context:
|
||||
<chat_history>
|
||||
{{MESSAGES:END:6}}
|
||||
</chat_history>
|
||||
<type>{{TYPE}}</type>
|
||||
<text>{{PROMPT}}</text>
|
||||
#### Output:
|
||||
"""
|
||||
|
||||
|
||||
VOICE_MODE_PROMPT_TEMPLATE = PersistentConfig(
|
||||
'VOICE_MODE_PROMPT_TEMPLATE',
|
||||
'task.voice.prompt_template',
|
||||
os.environ.get('VOICE_MODE_PROMPT_TEMPLATE', ''),
|
||||
)
|
||||
|
||||
DEFAULT_VOICE_MODE_PROMPT_TEMPLATE = """You are a friendly, concise voice assistant.
|
||||
|
||||
Everything you say will be spoken aloud.
|
||||
Keep responses short, clear, and natural.
|
||||
|
||||
STYLE:
|
||||
- Use simple words and short sentences.
|
||||
- Sound warm and conversational.
|
||||
- Avoid long explanations, lists, or complex phrasing.
|
||||
|
||||
BEHAVIOR:
|
||||
- Give the quickest helpful answer first.
|
||||
- Offer extra detail only if needed.
|
||||
- Ask for clarification only when necessary.
|
||||
|
||||
VOICE OPTIMIZATION:
|
||||
- Break information into small, easy-to-hear chunks.
|
||||
- Avoid dense wording or anything that sounds like reading text.
|
||||
|
||||
ERROR HANDLING:
|
||||
- If unsure, say so briefly and offer options.
|
||||
- If something is unsafe or impossible, decline kindly and suggest a safe alternative.
|
||||
|
||||
Stay consistent, helpful, and easy to listen to."""
|
||||
|
||||
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = PersistentConfig(
|
||||
'TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE',
|
||||
'task.tools.prompt_template',
|
||||
os.environ.get('TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE', ''),
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = """Available Tools: {{TOOLS}}
|
||||
|
||||
Your task is to choose and return the correct tool(s) from the list of available tools based on the query. Follow these guidelines:
|
||||
|
||||
- Return only the JSON object, without any additional text or explanation.
|
||||
|
||||
- If no tools match the query, return an empty array:
|
||||
{
|
||||
"tool_calls": []
|
||||
}
|
||||
|
||||
- If one or more tools match the query, construct a JSON response containing a "tool_calls" array with objects that include:
|
||||
- "name": The tool's name.
|
||||
- "parameters": A dictionary of required parameters and their corresponding values.
|
||||
|
||||
The format for the JSON response is strictly:
|
||||
{
|
||||
"tool_calls": [
|
||||
{"name": "toolName1", "parameters": {"key1": "value1"}},
|
||||
{"name": "toolName2", "parameters": {"key2": "value2"}}
|
||||
]
|
||||
}"""
|
||||
|
||||
|
||||
DEFAULT_EMOJI_GENERATION_PROMPT_TEMPLATE = """Your task is to reflect the speaker's likely facial expression through a fitting emoji. Interpret emotions from the message and reflect their facial expression using fitting, diverse emojis (e.g., 😊, 😢, 😡, 😱).
|
||||
|
||||
Message: ```{{prompt}}```"""
|
||||
|
||||
DEFAULT_MOA_GENERATION_PROMPT_TEMPLATE = """You have been provided with a set of responses from various models to the latest user query: "{{prompt}}"
|
||||
|
||||
Your task is to synthesize these responses into a single, high-quality response. It is crucial to critically evaluate the information provided in these responses, recognizing that some of it may be biased or incorrect. Your response should not simply replicate the given answers but should offer a refined, accurate, and comprehensive reply to the instruction. Ensure your response is well-structured, coherent, and adheres to the highest standards of accuracy and reliability.
|
||||
|
||||
Responses from models: {{responses}}"""
|
||||
|
||||
|
||||
####################################
|
||||
# Code Interpreter
|
||||
####################################
|
||||
|
||||
ENABLE_CODE_EXECUTION = os.getenv('ENABLE_CODE_EXECUTION', 'True').lower() == 'true'
|
||||
ENABLE_CODE_EXECUTION = PersistentConfig(
|
||||
'ENABLE_CODE_EXECUTION',
|
||||
'code_execution.enable',
|
||||
os.environ.get('ENABLE_CODE_EXECUTION', 'True').lower() == 'true',
|
||||
)
|
||||
|
||||
CODE_EXECUTION_ENGINE = os.getenv('CODE_EXECUTION_ENGINE', 'pyodide')
|
||||
CODE_EXECUTION_ENGINE = PersistentConfig(
|
||||
'CODE_EXECUTION_ENGINE',
|
||||
'code_execution.engine',
|
||||
os.environ.get('CODE_EXECUTION_ENGINE', 'pyodide'),
|
||||
)
|
||||
|
||||
CODE_EXECUTION_JUPYTER_URL = os.getenv('CODE_EXECUTION_JUPYTER_URL', '')
|
||||
CODE_EXECUTION_JUPYTER_URL = PersistentConfig(
|
||||
'CODE_EXECUTION_JUPYTER_URL',
|
||||
'code_execution.jupyter.url',
|
||||
os.environ.get('CODE_EXECUTION_JUPYTER_URL', ''),
|
||||
)
|
||||
|
||||
CODE_EXECUTION_JUPYTER_AUTH = os.getenv('CODE_EXECUTION_JUPYTER_AUTH', '')
|
||||
CODE_EXECUTION_JUPYTER_AUTH = PersistentConfig(
|
||||
'CODE_EXECUTION_JUPYTER_AUTH',
|
||||
'code_execution.jupyter.auth',
|
||||
os.environ.get('CODE_EXECUTION_JUPYTER_AUTH', ''),
|
||||
)
|
||||
|
||||
CODE_EXECUTION_JUPYTER_AUTH_TOKEN = os.getenv('CODE_EXECUTION_JUPYTER_AUTH_TOKEN', '')
|
||||
CODE_EXECUTION_JUPYTER_AUTH_TOKEN = PersistentConfig(
|
||||
'CODE_EXECUTION_JUPYTER_AUTH_TOKEN',
|
||||
'code_execution.jupyter.auth_token',
|
||||
os.environ.get('CODE_EXECUTION_JUPYTER_AUTH_TOKEN', ''),
|
||||
)
|
||||
|
||||
|
||||
CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = os.getenv('CODE_EXECUTION_JUPYTER_AUTH_PASSWORD', '')
|
||||
CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = PersistentConfig(
|
||||
'CODE_EXECUTION_JUPYTER_AUTH_PASSWORD',
|
||||
'code_execution.jupyter.auth_password',
|
||||
os.environ.get('CODE_EXECUTION_JUPYTER_AUTH_PASSWORD', ''),
|
||||
)
|
||||
|
||||
CODE_EXECUTION_JUPYTER_TIMEOUT = int(os.getenv('CODE_EXECUTION_JUPYTER_TIMEOUT', '60'))
|
||||
CODE_EXECUTION_JUPYTER_TIMEOUT = PersistentConfig(
|
||||
'CODE_EXECUTION_JUPYTER_TIMEOUT',
|
||||
'code_execution.jupyter.timeout',
|
||||
int(os.environ.get('CODE_EXECUTION_JUPYTER_TIMEOUT', '60')),
|
||||
)
|
||||
|
||||
ENABLE_CODE_INTERPRETER = os.getenv('ENABLE_CODE_INTERPRETER', 'True').lower() == 'true'
|
||||
ENABLE_CODE_INTERPRETER = PersistentConfig(
|
||||
'ENABLE_CODE_INTERPRETER',
|
||||
'code_interpreter.enable',
|
||||
os.environ.get('ENABLE_CODE_INTERPRETER', 'True').lower() == 'true',
|
||||
)
|
||||
|
||||
ENABLE_MEMORIES = os.getenv('ENABLE_MEMORIES', 'True').lower() == 'true'
|
||||
ENABLE_MEMORY_SYSTEM_CONTEXT = os.getenv('ENABLE_MEMORY_SYSTEM_CONTEXT', 'True').lower() == 'true'
|
||||
ENABLE_MEMORY_BACKGROUND_REVIEW = os.getenv('ENABLE_MEMORY_BACKGROUND_REVIEW', 'False').lower() == 'true'
|
||||
MEMORIES_REVIEW_INTERVAL_TURNS = int(os.getenv('MEMORIES_REVIEW_INTERVAL_TURNS', '10'))
|
||||
MEMORIES_USER_CHAR_LIMIT = int(os.getenv('MEMORIES_USER_CHAR_LIMIT', '2000'))
|
||||
MEMORIES_CONTEXT_CHAR_LIMIT = int(os.getenv('MEMORIES_CONTEXT_CHAR_LIMIT', '2000'))
|
||||
ENABLE_MEMORIES = PersistentConfig(
|
||||
'ENABLE_MEMORIES',
|
||||
'memories.enable',
|
||||
os.environ.get('ENABLE_MEMORIES', 'True').lower() == 'true',
|
||||
)
|
||||
|
||||
CODE_INTERPRETER_ENGINE = os.getenv('CODE_INTERPRETER_ENGINE', 'pyodide')
|
||||
CODE_INTERPRETER_ENGINE = PersistentConfig(
|
||||
'CODE_INTERPRETER_ENGINE',
|
||||
'code_interpreter.engine',
|
||||
os.environ.get('CODE_INTERPRETER_ENGINE', 'pyodide'),
|
||||
)
|
||||
|
||||
CODE_INTERPRETER_PROMPT_TEMPLATE = os.getenv('CODE_INTERPRETER_PROMPT_TEMPLATE', '')
|
||||
CODE_INTERPRETER_PROMPT_TEMPLATE = PersistentConfig(
|
||||
'CODE_INTERPRETER_PROMPT_TEMPLATE',
|
||||
'code_interpreter.prompt_template',
|
||||
os.environ.get('CODE_INTERPRETER_PROMPT_TEMPLATE', ''),
|
||||
)
|
||||
|
||||
CODE_INTERPRETER_JUPYTER_URL = os.getenv('CODE_INTERPRETER_JUPYTER_URL', os.getenv('CODE_EXECUTION_JUPYTER_URL', ''))
|
||||
CODE_INTERPRETER_JUPYTER_URL = PersistentConfig(
|
||||
'CODE_INTERPRETER_JUPYTER_URL',
|
||||
'code_interpreter.jupyter.url',
|
||||
os.environ.get('CODE_INTERPRETER_JUPYTER_URL', os.environ.get('CODE_EXECUTION_JUPYTER_URL', '')),
|
||||
)
|
||||
|
||||
CODE_INTERPRETER_JUPYTER_AUTH = os.getenv(
|
||||
CODE_INTERPRETER_JUPYTER_AUTH = PersistentConfig(
|
||||
'CODE_INTERPRETER_JUPYTER_AUTH',
|
||||
os.getenv('CODE_EXECUTION_JUPYTER_AUTH', ''),
|
||||
'code_interpreter.jupyter.auth',
|
||||
os.environ.get(
|
||||
'CODE_INTERPRETER_JUPYTER_AUTH',
|
||||
os.environ.get('CODE_EXECUTION_JUPYTER_AUTH', ''),
|
||||
),
|
||||
)
|
||||
|
||||
CODE_INTERPRETER_JUPYTER_AUTH_TOKEN = os.getenv(
|
||||
CODE_INTERPRETER_JUPYTER_AUTH_TOKEN = PersistentConfig(
|
||||
'CODE_INTERPRETER_JUPYTER_AUTH_TOKEN',
|
||||
os.getenv('CODE_EXECUTION_JUPYTER_AUTH_TOKEN', ''),
|
||||
'code_interpreter.jupyter.auth_token',
|
||||
os.environ.get(
|
||||
'CODE_INTERPRETER_JUPYTER_AUTH_TOKEN',
|
||||
os.environ.get('CODE_EXECUTION_JUPYTER_AUTH_TOKEN', ''),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = os.getenv(
|
||||
CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = PersistentConfig(
|
||||
'CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD',
|
||||
os.getenv('CODE_EXECUTION_JUPYTER_AUTH_PASSWORD', ''),
|
||||
'code_interpreter.jupyter.auth_password',
|
||||
os.environ.get(
|
||||
'CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD',
|
||||
os.environ.get('CODE_EXECUTION_JUPYTER_AUTH_PASSWORD', ''),
|
||||
),
|
||||
)
|
||||
|
||||
CODE_INTERPRETER_JUPYTER_TIMEOUT = int(
|
||||
os.getenv(
|
||||
'CODE_INTERPRETER_JUPYTER_TIMEOUT',
|
||||
os.getenv('CODE_EXECUTION_JUPYTER_TIMEOUT', '60'),
|
||||
)
|
||||
CODE_INTERPRETER_JUPYTER_TIMEOUT = PersistentConfig(
|
||||
'CODE_INTERPRETER_JUPYTER_TIMEOUT',
|
||||
'code_interpreter.jupyter.timeout',
|
||||
int(
|
||||
os.environ.get(
|
||||
'CODE_INTERPRETER_JUPYTER_TIMEOUT',
|
||||
os.environ.get('CODE_EXECUTION_JUPYTER_TIMEOUT', '60'),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
CODE_INTERPRETER_BLOCKED_MODULES = [
|
||||
library.strip() for library in os.getenv('CODE_INTERPRETER_BLOCKED_MODULES', '').split(',') if library.strip()
|
||||
library.strip() for library in os.environ.get('CODE_INTERPRETER_BLOCKED_MODULES', '').split(',') if library.strip()
|
||||
]
|
||||
|
||||
DEFAULT_CODE_INTERPRETER_PROMPT = """
|
||||
@@ -490,7 +2275,7 @@ CODE_INTERPRETER_PYODIDE_PROMPT = """
|
||||
# Vector Database
|
||||
####################################
|
||||
|
||||
VECTOR_DB = os.getenv('VECTOR_DB', 'chroma')
|
||||
VECTOR_DB = os.environ.get('VECTOR_DB', 'chroma')
|
||||
|
||||
# Chroma
|
||||
CHROMA_DATA_PATH = f'{DATA_DIR}/vector_db'
|
||||
@@ -498,39 +2283,39 @@ CHROMA_DATA_PATH = f'{DATA_DIR}/vector_db'
|
||||
if VECTOR_DB == 'chroma':
|
||||
import chromadb
|
||||
|
||||
CHROMA_TENANT = os.getenv('CHROMA_TENANT', chromadb.DEFAULT_TENANT)
|
||||
CHROMA_DATABASE = os.getenv('CHROMA_DATABASE', chromadb.DEFAULT_DATABASE)
|
||||
CHROMA_HTTP_HOST = os.getenv('CHROMA_HTTP_HOST', '')
|
||||
CHROMA_HTTP_PORT = int(os.getenv('CHROMA_HTTP_PORT', '8000'))
|
||||
CHROMA_CLIENT_AUTH_PROVIDER = os.getenv('CHROMA_CLIENT_AUTH_PROVIDER', '')
|
||||
CHROMA_CLIENT_AUTH_CREDENTIALS = os.getenv('CHROMA_CLIENT_AUTH_CREDENTIALS', '')
|
||||
CHROMA_TENANT = os.environ.get('CHROMA_TENANT', chromadb.DEFAULT_TENANT)
|
||||
CHROMA_DATABASE = os.environ.get('CHROMA_DATABASE', chromadb.DEFAULT_DATABASE)
|
||||
CHROMA_HTTP_HOST = os.environ.get('CHROMA_HTTP_HOST', '')
|
||||
CHROMA_HTTP_PORT = int(os.environ.get('CHROMA_HTTP_PORT', '8000'))
|
||||
CHROMA_CLIENT_AUTH_PROVIDER = os.environ.get('CHROMA_CLIENT_AUTH_PROVIDER', '')
|
||||
CHROMA_CLIENT_AUTH_CREDENTIALS = os.environ.get('CHROMA_CLIENT_AUTH_CREDENTIALS', '')
|
||||
# Comma-separated list of header=value pairs
|
||||
CHROMA_HTTP_HEADERS = os.getenv('CHROMA_HTTP_HEADERS', '')
|
||||
CHROMA_HTTP_HEADERS = os.environ.get('CHROMA_HTTP_HEADERS', '')
|
||||
if CHROMA_HTTP_HEADERS:
|
||||
CHROMA_HTTP_HEADERS = dict([pair.split('=') for pair in CHROMA_HTTP_HEADERS.split(',')])
|
||||
else:
|
||||
CHROMA_HTTP_HEADERS = None
|
||||
CHROMA_HTTP_SSL = os.getenv('CHROMA_HTTP_SSL', 'false').lower() == 'true'
|
||||
CHROMA_HTTP_SSL = os.environ.get('CHROMA_HTTP_SSL', 'false').lower() == 'true'
|
||||
# this uses the model defined in the Dockerfile ENV variable. If you dont use docker or docker based deployments such as k8s, the default embedding model will be used (sentence-transformers/all-MiniLM-L6-v2)
|
||||
|
||||
|
||||
# MariaDB Vector (mariadb-vector)
|
||||
MARIADB_VECTOR_DB_URL = os.getenv('MARIADB_VECTOR_DB_URL', '').strip()
|
||||
MARIADB_VECTOR_DB_URL = os.environ.get('MARIADB_VECTOR_DB_URL', '').strip()
|
||||
|
||||
MARIADB_VECTOR_INITIALIZE_MAX_VECTOR_LENGTH = int(
|
||||
os.getenv('MARIADB_VECTOR_INITIALIZE_MAX_VECTOR_LENGTH', '1536').strip() or '1536'
|
||||
os.environ.get('MARIADB_VECTOR_INITIALIZE_MAX_VECTOR_LENGTH', '1536').strip() or '1536'
|
||||
)
|
||||
|
||||
# Distance strategy:
|
||||
# - cosine => vec_distance_cosine(...)
|
||||
# - euclidean => vec_distance_euclidean(...)
|
||||
MARIADB_VECTOR_DISTANCE_STRATEGY = os.getenv('MARIADB_VECTOR_DISTANCE_STRATEGY', 'cosine').strip().lower()
|
||||
MARIADB_VECTOR_DISTANCE_STRATEGY = os.environ.get('MARIADB_VECTOR_DISTANCE_STRATEGY', 'cosine').strip().lower()
|
||||
|
||||
# HNSW M parameter (MariaDB VECTOR INDEX ... M=<int>)
|
||||
MARIADB_VECTOR_INDEX_M = int(os.getenv('MARIADB_VECTOR_INDEX_M', '8').strip() or '8')
|
||||
MARIADB_VECTOR_INDEX_M = int(os.environ.get('MARIADB_VECTOR_INDEX_M', '8').strip() or '8')
|
||||
|
||||
# Pooling (MariaDB-Vector)
|
||||
MARIADB_VECTOR_POOL_SIZE = os.getenv('MARIADB_VECTOR_POOL_SIZE', None)
|
||||
MARIADB_VECTOR_POOL_SIZE = os.environ.get('MARIADB_VECTOR_POOL_SIZE', None)
|
||||
|
||||
if MARIADB_VECTOR_POOL_SIZE != None:
|
||||
try:
|
||||
@@ -538,7 +2323,7 @@ if MARIADB_VECTOR_POOL_SIZE != None:
|
||||
except Exception:
|
||||
MARIADB_VECTOR_POOL_SIZE = None
|
||||
|
||||
MARIADB_VECTOR_POOL_MAX_OVERFLOW = os.getenv('MARIADB_VECTOR_POOL_MAX_OVERFLOW', 0)
|
||||
MARIADB_VECTOR_POOL_MAX_OVERFLOW = os.environ.get('MARIADB_VECTOR_POOL_MAX_OVERFLOW', 0)
|
||||
|
||||
if MARIADB_VECTOR_POOL_MAX_OVERFLOW == '':
|
||||
MARIADB_VECTOR_POOL_MAX_OVERFLOW = 0
|
||||
@@ -548,7 +2333,7 @@ else:
|
||||
except Exception:
|
||||
MARIADB_VECTOR_POOL_MAX_OVERFLOW = 0
|
||||
|
||||
MARIADB_VECTOR_POOL_TIMEOUT = os.getenv('MARIADB_VECTOR_POOL_TIMEOUT', 30)
|
||||
MARIADB_VECTOR_POOL_TIMEOUT = os.environ.get('MARIADB_VECTOR_POOL_TIMEOUT', 30)
|
||||
|
||||
if MARIADB_VECTOR_POOL_TIMEOUT == '':
|
||||
MARIADB_VECTOR_POOL_TIMEOUT = 30
|
||||
@@ -558,7 +2343,7 @@ else:
|
||||
except Exception:
|
||||
MARIADB_VECTOR_POOL_TIMEOUT = 30
|
||||
|
||||
MARIADB_VECTOR_POOL_RECYCLE = os.getenv('MARIADB_VECTOR_POOL_RECYCLE', 3600)
|
||||
MARIADB_VECTOR_POOL_RECYCLE = os.environ.get('MARIADB_VECTOR_POOL_RECYCLE', 3600)
|
||||
|
||||
if MARIADB_VECTOR_POOL_RECYCLE == '':
|
||||
MARIADB_VECTOR_POOL_RECYCLE = 3600
|
||||
@@ -584,63 +2369,63 @@ if VECTOR_DB == 'mariadb-vector':
|
||||
|
||||
|
||||
# Milvus
|
||||
MILVUS_URI = os.getenv('MILVUS_URI', f'{DATA_DIR}/vector_db/milvus.db')
|
||||
MILVUS_DB = os.getenv('MILVUS_DB', 'default')
|
||||
MILVUS_TOKEN = os.getenv('MILVUS_TOKEN', None)
|
||||
MILVUS_INDEX_TYPE = os.getenv('MILVUS_INDEX_TYPE', 'HNSW')
|
||||
MILVUS_METRIC_TYPE = os.getenv('MILVUS_METRIC_TYPE', 'COSINE')
|
||||
MILVUS_HNSW_M = int(os.getenv('MILVUS_HNSW_M', '16'))
|
||||
MILVUS_HNSW_EFCONSTRUCTION = int(os.getenv('MILVUS_HNSW_EFCONSTRUCTION', '100'))
|
||||
MILVUS_IVF_FLAT_NLIST = int(os.getenv('MILVUS_IVF_FLAT_NLIST', '128'))
|
||||
MILVUS_DISKANN_MAX_DEGREE = int(os.getenv('MILVUS_DISKANN_MAX_DEGREE', '56'))
|
||||
MILVUS_DISKANN_SEARCH_LIST_SIZE = int(os.getenv('MILVUS_DISKANN_SEARCH_LIST_SIZE', '100'))
|
||||
ENABLE_MILVUS_MULTITENANCY_MODE = os.getenv('ENABLE_MILVUS_MULTITENANCY_MODE', 'false').lower() == 'true'
|
||||
MILVUS_URI = os.environ.get('MILVUS_URI', f'{DATA_DIR}/vector_db/milvus.db')
|
||||
MILVUS_DB = os.environ.get('MILVUS_DB', 'default')
|
||||
MILVUS_TOKEN = os.environ.get('MILVUS_TOKEN', None)
|
||||
MILVUS_INDEX_TYPE = os.environ.get('MILVUS_INDEX_TYPE', 'HNSW')
|
||||
MILVUS_METRIC_TYPE = os.environ.get('MILVUS_METRIC_TYPE', 'COSINE')
|
||||
MILVUS_HNSW_M = int(os.environ.get('MILVUS_HNSW_M', '16'))
|
||||
MILVUS_HNSW_EFCONSTRUCTION = int(os.environ.get('MILVUS_HNSW_EFCONSTRUCTION', '100'))
|
||||
MILVUS_IVF_FLAT_NLIST = int(os.environ.get('MILVUS_IVF_FLAT_NLIST', '128'))
|
||||
MILVUS_DISKANN_MAX_DEGREE = int(os.environ.get('MILVUS_DISKANN_MAX_DEGREE', '56'))
|
||||
MILVUS_DISKANN_SEARCH_LIST_SIZE = int(os.environ.get('MILVUS_DISKANN_SEARCH_LIST_SIZE', '100'))
|
||||
ENABLE_MILVUS_MULTITENANCY_MODE = os.environ.get('ENABLE_MILVUS_MULTITENANCY_MODE', 'false').lower() == 'true'
|
||||
# Hyphens not allowed, need to use underscores in collection names
|
||||
MILVUS_COLLECTION_PREFIX = os.getenv('MILVUS_COLLECTION_PREFIX', 'open_webui')
|
||||
MILVUS_COLLECTION_PREFIX = os.environ.get('MILVUS_COLLECTION_PREFIX', 'open_webui')
|
||||
|
||||
# Qdrant
|
||||
QDRANT_URI = os.getenv('QDRANT_URI', None)
|
||||
QDRANT_API_KEY = os.getenv('QDRANT_API_KEY', None)
|
||||
QDRANT_ON_DISK = os.getenv('QDRANT_ON_DISK', 'false').lower() == 'true'
|
||||
QDRANT_PREFER_GRPC = os.getenv('QDRANT_PREFER_GRPC', 'false').lower() == 'true'
|
||||
QDRANT_GRPC_PORT = int(os.getenv('QDRANT_GRPC_PORT', '6334'))
|
||||
QDRANT_TIMEOUT = int(os.getenv('QDRANT_TIMEOUT', '5'))
|
||||
QDRANT_HNSW_M = int(os.getenv('QDRANT_HNSW_M', '16'))
|
||||
ENABLE_QDRANT_MULTITENANCY_MODE = os.getenv('ENABLE_QDRANT_MULTITENANCY_MODE', 'true').lower() == 'true'
|
||||
QDRANT_COLLECTION_PREFIX = os.getenv('QDRANT_COLLECTION_PREFIX', 'open-webui')
|
||||
QDRANT_URI = os.environ.get('QDRANT_URI', None)
|
||||
QDRANT_API_KEY = os.environ.get('QDRANT_API_KEY', None)
|
||||
QDRANT_ON_DISK = os.environ.get('QDRANT_ON_DISK', 'false').lower() == 'true'
|
||||
QDRANT_PREFER_GRPC = os.environ.get('QDRANT_PREFER_GRPC', 'false').lower() == 'true'
|
||||
QDRANT_GRPC_PORT = int(os.environ.get('QDRANT_GRPC_PORT', '6334'))
|
||||
QDRANT_TIMEOUT = int(os.environ.get('QDRANT_TIMEOUT', '5'))
|
||||
QDRANT_HNSW_M = int(os.environ.get('QDRANT_HNSW_M', '16'))
|
||||
ENABLE_QDRANT_MULTITENANCY_MODE = os.environ.get('ENABLE_QDRANT_MULTITENANCY_MODE', 'true').lower() == 'true'
|
||||
QDRANT_COLLECTION_PREFIX = os.environ.get('QDRANT_COLLECTION_PREFIX', 'open-webui')
|
||||
|
||||
WEAVIATE_HTTP_HOST = os.getenv('WEAVIATE_HTTP_HOST', '')
|
||||
WEAVIATE_GRPC_HOST = os.getenv('WEAVIATE_GRPC_HOST', '')
|
||||
WEAVIATE_HTTP_PORT = int(os.getenv('WEAVIATE_HTTP_PORT', '8080'))
|
||||
WEAVIATE_GRPC_PORT = int(os.getenv('WEAVIATE_GRPC_PORT', '50051'))
|
||||
WEAVIATE_API_KEY = os.getenv('WEAVIATE_API_KEY')
|
||||
WEAVIATE_HTTP_SECURE = os.getenv('WEAVIATE_HTTP_SECURE', 'false').lower() == 'true'
|
||||
WEAVIATE_GRPC_SECURE = os.getenv('WEAVIATE_GRPC_SECURE', 'false').lower() == 'true'
|
||||
WEAVIATE_SKIP_INIT_CHECKS = os.getenv('WEAVIATE_SKIP_INIT_CHECKS', 'false').lower() == 'true'
|
||||
WEAVIATE_HTTP_HOST = os.environ.get('WEAVIATE_HTTP_HOST', '')
|
||||
WEAVIATE_GRPC_HOST = os.environ.get('WEAVIATE_GRPC_HOST', '')
|
||||
WEAVIATE_HTTP_PORT = int(os.environ.get('WEAVIATE_HTTP_PORT', '8080'))
|
||||
WEAVIATE_GRPC_PORT = int(os.environ.get('WEAVIATE_GRPC_PORT', '50051'))
|
||||
WEAVIATE_API_KEY = os.environ.get('WEAVIATE_API_KEY')
|
||||
WEAVIATE_HTTP_SECURE = os.environ.get('WEAVIATE_HTTP_SECURE', 'false').lower() == 'true'
|
||||
WEAVIATE_GRPC_SECURE = os.environ.get('WEAVIATE_GRPC_SECURE', 'false').lower() == 'true'
|
||||
WEAVIATE_SKIP_INIT_CHECKS = os.environ.get('WEAVIATE_SKIP_INIT_CHECKS', 'false').lower() == 'true'
|
||||
|
||||
# OpenSearch
|
||||
OPENSEARCH_URI = os.getenv('OPENSEARCH_URI', 'https://localhost:9200')
|
||||
OPENSEARCH_SSL = os.getenv('OPENSEARCH_SSL', 'true').lower() == 'true'
|
||||
OPENSEARCH_CERT_VERIFY = os.getenv('OPENSEARCH_CERT_VERIFY', 'false').lower() == 'true'
|
||||
OPENSEARCH_USERNAME = os.getenv('OPENSEARCH_USERNAME', None)
|
||||
OPENSEARCH_PASSWORD = os.getenv('OPENSEARCH_PASSWORD', None)
|
||||
OPENSEARCH_URI = os.environ.get('OPENSEARCH_URI', 'https://localhost:9200')
|
||||
OPENSEARCH_SSL = os.environ.get('OPENSEARCH_SSL', 'true').lower() == 'true'
|
||||
OPENSEARCH_CERT_VERIFY = os.environ.get('OPENSEARCH_CERT_VERIFY', 'false').lower() == 'true'
|
||||
OPENSEARCH_USERNAME = os.environ.get('OPENSEARCH_USERNAME', None)
|
||||
OPENSEARCH_PASSWORD = os.environ.get('OPENSEARCH_PASSWORD', None)
|
||||
|
||||
# ElasticSearch
|
||||
ELASTICSEARCH_URL = os.getenv('ELASTICSEARCH_URL', 'https://localhost:9200')
|
||||
ELASTICSEARCH_CA_CERTS = os.getenv('ELASTICSEARCH_CA_CERTS', None)
|
||||
ELASTICSEARCH_API_KEY = os.getenv('ELASTICSEARCH_API_KEY', None)
|
||||
ELASTICSEARCH_USERNAME = os.getenv('ELASTICSEARCH_USERNAME', None)
|
||||
ELASTICSEARCH_PASSWORD = os.getenv('ELASTICSEARCH_PASSWORD', None)
|
||||
ELASTICSEARCH_CLOUD_ID = os.getenv('ELASTICSEARCH_CLOUD_ID', None)
|
||||
SSL_ASSERT_FINGERPRINT = os.getenv('SSL_ASSERT_FINGERPRINT', None)
|
||||
ELASTICSEARCH_INDEX_PREFIX = os.getenv('ELASTICSEARCH_INDEX_PREFIX', 'open_webui_collections')
|
||||
ELASTICSEARCH_URL = os.environ.get('ELASTICSEARCH_URL', 'https://localhost:9200')
|
||||
ELASTICSEARCH_CA_CERTS = os.environ.get('ELASTICSEARCH_CA_CERTS', None)
|
||||
ELASTICSEARCH_API_KEY = os.environ.get('ELASTICSEARCH_API_KEY', None)
|
||||
ELASTICSEARCH_USERNAME = os.environ.get('ELASTICSEARCH_USERNAME', None)
|
||||
ELASTICSEARCH_PASSWORD = os.environ.get('ELASTICSEARCH_PASSWORD', None)
|
||||
ELASTICSEARCH_CLOUD_ID = os.environ.get('ELASTICSEARCH_CLOUD_ID', None)
|
||||
SSL_ASSERT_FINGERPRINT = os.environ.get('SSL_ASSERT_FINGERPRINT', None)
|
||||
ELASTICSEARCH_INDEX_PREFIX = os.environ.get('ELASTICSEARCH_INDEX_PREFIX', 'open_webui_collections')
|
||||
# Pgvector
|
||||
PGVECTOR_DB_URL = os.getenv('PGVECTOR_DB_URL', DATABASE_URL)
|
||||
PGVECTOR_DB_URL = os.environ.get('PGVECTOR_DB_URL', DATABASE_URL)
|
||||
if VECTOR_DB == 'pgvector' and not PGVECTOR_DB_URL.startswith('postgres'):
|
||||
raise ValueError(
|
||||
'Pgvector requires setting PGVECTOR_DB_URL or using Postgres with vector extension as the primary database.'
|
||||
)
|
||||
PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH = int(os.getenv('PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH', '1536'))
|
||||
PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH = int(os.environ.get('PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH', '1536'))
|
||||
|
||||
PGVECTOR_USE_HALFVEC = os.getenv('PGVECTOR_USE_HALFVEC', 'false').lower() == 'true'
|
||||
|
||||
@@ -659,7 +2444,7 @@ if PGVECTOR_PGCRYPTO and not PGVECTOR_PGCRYPTO_KEY:
|
||||
raise ValueError('PGVECTOR_PGCRYPTO is enabled but PGVECTOR_PGCRYPTO_KEY is not set. Please provide a valid key.')
|
||||
|
||||
|
||||
PGVECTOR_POOL_SIZE = os.getenv('PGVECTOR_POOL_SIZE', None)
|
||||
PGVECTOR_POOL_SIZE = os.environ.get('PGVECTOR_POOL_SIZE', None)
|
||||
|
||||
if PGVECTOR_POOL_SIZE != None:
|
||||
try:
|
||||
@@ -667,7 +2452,7 @@ if PGVECTOR_POOL_SIZE != None:
|
||||
except Exception:
|
||||
PGVECTOR_POOL_SIZE = None
|
||||
|
||||
PGVECTOR_POOL_MAX_OVERFLOW = os.getenv('PGVECTOR_POOL_MAX_OVERFLOW', 0)
|
||||
PGVECTOR_POOL_MAX_OVERFLOW = os.environ.get('PGVECTOR_POOL_MAX_OVERFLOW', 0)
|
||||
|
||||
if PGVECTOR_POOL_MAX_OVERFLOW == '':
|
||||
PGVECTOR_POOL_MAX_OVERFLOW = 0
|
||||
@@ -677,7 +2462,7 @@ else:
|
||||
except Exception:
|
||||
PGVECTOR_POOL_MAX_OVERFLOW = 0
|
||||
|
||||
PGVECTOR_POOL_TIMEOUT = os.getenv('PGVECTOR_POOL_TIMEOUT', 30)
|
||||
PGVECTOR_POOL_TIMEOUT = os.environ.get('PGVECTOR_POOL_TIMEOUT', 30)
|
||||
|
||||
if PGVECTOR_POOL_TIMEOUT == '':
|
||||
PGVECTOR_POOL_TIMEOUT = 30
|
||||
@@ -687,7 +2472,7 @@ else:
|
||||
except Exception:
|
||||
PGVECTOR_POOL_TIMEOUT = 30
|
||||
|
||||
PGVECTOR_POOL_RECYCLE = os.getenv('PGVECTOR_POOL_RECYCLE', 3600)
|
||||
PGVECTOR_POOL_RECYCLE = os.environ.get('PGVECTOR_POOL_RECYCLE', 3600)
|
||||
|
||||
if PGVECTOR_POOL_RECYCLE == '':
|
||||
PGVECTOR_POOL_RECYCLE = 3600
|
||||
@@ -701,7 +2486,7 @@ PGVECTOR_INDEX_METHOD = os.getenv('PGVECTOR_INDEX_METHOD', '').strip().lower()
|
||||
if PGVECTOR_INDEX_METHOD not in ('ivfflat', 'hnsw', ''):
|
||||
PGVECTOR_INDEX_METHOD = ''
|
||||
|
||||
PGVECTOR_HNSW_M = os.getenv('PGVECTOR_HNSW_M', 16)
|
||||
PGVECTOR_HNSW_M = os.environ.get('PGVECTOR_HNSW_M', 16)
|
||||
|
||||
if PGVECTOR_HNSW_M == '':
|
||||
PGVECTOR_HNSW_M = 16
|
||||
@@ -711,7 +2496,7 @@ else:
|
||||
except Exception:
|
||||
PGVECTOR_HNSW_M = 16
|
||||
|
||||
PGVECTOR_HNSW_EF_CONSTRUCTION = os.getenv('PGVECTOR_HNSW_EF_CONSTRUCTION', 64)
|
||||
PGVECTOR_HNSW_EF_CONSTRUCTION = os.environ.get('PGVECTOR_HNSW_EF_CONSTRUCTION', 64)
|
||||
|
||||
if PGVECTOR_HNSW_EF_CONSTRUCTION == '':
|
||||
PGVECTOR_HNSW_EF_CONSTRUCTION = 64
|
||||
@@ -721,7 +2506,7 @@ else:
|
||||
except Exception:
|
||||
PGVECTOR_HNSW_EF_CONSTRUCTION = 64
|
||||
|
||||
PGVECTOR_IVFFLAT_LISTS = os.getenv('PGVECTOR_IVFFLAT_LISTS', 100)
|
||||
PGVECTOR_IVFFLAT_LISTS = os.environ.get('PGVECTOR_IVFFLAT_LISTS', 100)
|
||||
|
||||
if PGVECTOR_IVFFLAT_LISTS == '':
|
||||
PGVECTOR_IVFFLAT_LISTS = 100
|
||||
@@ -732,11 +2517,11 @@ else:
|
||||
PGVECTOR_IVFFLAT_LISTS = 100
|
||||
|
||||
# openGauss
|
||||
OPENGAUSS_DB_URL = os.getenv('OPENGAUSS_DB_URL', DATABASE_URL)
|
||||
OPENGAUSS_DB_URL = os.environ.get('OPENGAUSS_DB_URL', DATABASE_URL)
|
||||
|
||||
OPENGAUSS_INITIALIZE_MAX_VECTOR_LENGTH = int(os.getenv('OPENGAUSS_INITIALIZE_MAX_VECTOR_LENGTH', '1536'))
|
||||
OPENGAUSS_INITIALIZE_MAX_VECTOR_LENGTH = int(os.environ.get('OPENGAUSS_INITIALIZE_MAX_VECTOR_LENGTH', '1536'))
|
||||
|
||||
OPENGAUSS_POOL_SIZE = os.getenv('OPENGAUSS_POOL_SIZE', None)
|
||||
OPENGAUSS_POOL_SIZE = os.environ.get('OPENGAUSS_POOL_SIZE', None)
|
||||
|
||||
if OPENGAUSS_POOL_SIZE != None:
|
||||
try:
|
||||
@@ -744,7 +2529,7 @@ if OPENGAUSS_POOL_SIZE != None:
|
||||
except Exception:
|
||||
OPENGAUSS_POOL_SIZE = None
|
||||
|
||||
OPENGAUSS_POOL_MAX_OVERFLOW = os.getenv('OPENGAUSS_POOL_MAX_OVERFLOW', 0)
|
||||
OPENGAUSS_POOL_MAX_OVERFLOW = os.environ.get('OPENGAUSS_POOL_MAX_OVERFLOW', 0)
|
||||
|
||||
if OPENGAUSS_POOL_MAX_OVERFLOW == '':
|
||||
OPENGAUSS_POOL_MAX_OVERFLOW = 0
|
||||
@@ -754,7 +2539,7 @@ else:
|
||||
except Exception:
|
||||
OPENGAUSS_POOL_MAX_OVERFLOW = 0
|
||||
|
||||
OPENGAUSS_POOL_TIMEOUT = os.getenv('OPENGAUSS_POOL_TIMEOUT', 30)
|
||||
OPENGAUSS_POOL_TIMEOUT = os.environ.get('OPENGAUSS_POOL_TIMEOUT', 30)
|
||||
|
||||
if OPENGAUSS_POOL_TIMEOUT == '':
|
||||
OPENGAUSS_POOL_TIMEOUT = 30
|
||||
@@ -764,7 +2549,7 @@ else:
|
||||
except Exception:
|
||||
OPENGAUSS_POOL_TIMEOUT = 30
|
||||
|
||||
OPENGAUSS_POOL_RECYCLE = os.getenv('OPENGAUSS_POOL_RECYCLE', 3600)
|
||||
OPENGAUSS_POOL_RECYCLE = os.environ.get('OPENGAUSS_POOL_RECYCLE', 3600)
|
||||
|
||||
if OPENGAUSS_POOL_RECYCLE == '':
|
||||
OPENGAUSS_POOL_RECYCLE = 3600
|
||||
@@ -775,8 +2560,8 @@ else:
|
||||
OPENGAUSS_POOL_RECYCLE = 3600
|
||||
|
||||
# Pinecone
|
||||
PINECONE_API_KEY = os.getenv('PINECONE_API_KEY', None)
|
||||
PINECONE_ENVIRONMENT = os.getenv('PINECONE_ENVIRONMENT', None)
|
||||
PINECONE_API_KEY = os.environ.get('PINECONE_API_KEY', None)
|
||||
PINECONE_ENVIRONMENT = os.environ.get('PINECONE_ENVIRONMENT', None)
|
||||
PINECONE_INDEX_NAME = os.getenv('PINECONE_INDEX_NAME', 'open-webui-index')
|
||||
PINECONE_DIMENSION = int(os.getenv('PINECONE_DIMENSION', 1536)) # or 3072, 1024, 768
|
||||
PINECONE_METRIC = os.getenv('PINECONE_METRIC', 'cosine')
|
||||
@@ -784,17 +2569,17 @@ PINECONE_CLOUD = os.getenv('PINECONE_CLOUD', 'aws') # or "gcp" or "azure"
|
||||
|
||||
# ORACLE23AI (Oracle23ai Vector Search)
|
||||
|
||||
ORACLE_DB_USE_WALLET = os.getenv('ORACLE_DB_USE_WALLET', 'false').lower() == 'true'
|
||||
ORACLE_DB_USER = os.getenv('ORACLE_DB_USER', None) #
|
||||
ORACLE_DB_PASSWORD = os.getenv('ORACLE_DB_PASSWORD', None) #
|
||||
ORACLE_DB_DSN = os.getenv('ORACLE_DB_DSN', None) #
|
||||
ORACLE_WALLET_DIR = os.getenv('ORACLE_WALLET_DIR', None)
|
||||
ORACLE_WALLET_PASSWORD = os.getenv('ORACLE_WALLET_PASSWORD', None)
|
||||
ORACLE_VECTOR_LENGTH = os.getenv('ORACLE_VECTOR_LENGTH', 768)
|
||||
ORACLE_DB_USE_WALLET = os.environ.get('ORACLE_DB_USE_WALLET', 'false').lower() == 'true'
|
||||
ORACLE_DB_USER = os.environ.get('ORACLE_DB_USER', None) #
|
||||
ORACLE_DB_PASSWORD = os.environ.get('ORACLE_DB_PASSWORD', None) #
|
||||
ORACLE_DB_DSN = os.environ.get('ORACLE_DB_DSN', None) #
|
||||
ORACLE_WALLET_DIR = os.environ.get('ORACLE_WALLET_DIR', None)
|
||||
ORACLE_WALLET_PASSWORD = os.environ.get('ORACLE_WALLET_PASSWORD', None)
|
||||
ORACLE_VECTOR_LENGTH = os.environ.get('ORACLE_VECTOR_LENGTH', 768)
|
||||
|
||||
ORACLE_DB_POOL_MIN = int(os.getenv('ORACLE_DB_POOL_MIN', 2))
|
||||
ORACLE_DB_POOL_MAX = int(os.getenv('ORACLE_DB_POOL_MAX', 10))
|
||||
ORACLE_DB_POOL_INCREMENT = int(os.getenv('ORACLE_DB_POOL_INCREMENT', 1))
|
||||
ORACLE_DB_POOL_MIN = int(os.environ.get('ORACLE_DB_POOL_MIN', 2))
|
||||
ORACLE_DB_POOL_MAX = int(os.environ.get('ORACLE_DB_POOL_MAX', 10))
|
||||
ORACLE_DB_POOL_INCREMENT = int(os.environ.get('ORACLE_DB_POOL_INCREMENT', 1))
|
||||
|
||||
|
||||
if VECTOR_DB == 'oracle23ai':
|
||||
@@ -808,17 +2593,8 @@ if VECTOR_DB == 'oracle23ai':
|
||||
log.info(f'VECTOR_DB: {VECTOR_DB}')
|
||||
|
||||
# S3 Vector
|
||||
S3_VECTOR_BUCKET_NAME = os.getenv('S3_VECTOR_BUCKET_NAME', None)
|
||||
S3_VECTOR_REGION = os.getenv('S3_VECTOR_REGION', None)
|
||||
|
||||
# Valkey Vector Store
|
||||
VALKEY_URL = os.getenv('VALKEY_URL', '')
|
||||
VALKEY_COLLECTION_PREFIX = os.getenv('VALKEY_COLLECTION_PREFIX', 'open_webui')
|
||||
VALKEY_INDEX_TYPE = os.getenv('VALKEY_INDEX_TYPE', 'HNSW').upper()
|
||||
VALKEY_DISTANCE_METRIC = os.getenv('VALKEY_DISTANCE_METRIC', 'COSINE').upper()
|
||||
VALKEY_HNSW_M = int(os.getenv('VALKEY_HNSW_M', '16'))
|
||||
VALKEY_HNSW_EF_CONSTRUCTION = int(os.getenv('VALKEY_HNSW_EF_CONSTRUCTION', '200'))
|
||||
VALKEY_HNSW_EF_RUNTIME = int(os.getenv('VALKEY_HNSW_EF_RUNTIME', '10'))
|
||||
S3_VECTOR_BUCKET_NAME = os.environ.get('S3_VECTOR_BUCKET_NAME', None)
|
||||
S3_VECTOR_REGION = os.environ.get('S3_VECTOR_REGION', None)
|
||||
|
||||
####################################
|
||||
# Information Retrieval (RAG)
|
||||
@@ -826,64 +2602,146 @@ VALKEY_HNSW_EF_RUNTIME = int(os.getenv('VALKEY_HNSW_EF_RUNTIME', '10'))
|
||||
|
||||
|
||||
# If configured, Google Drive will be available as an upload option.
|
||||
ENABLE_GOOGLE_DRIVE_INTEGRATION = os.getenv('ENABLE_GOOGLE_DRIVE_INTEGRATION', 'False').lower() == 'true'
|
||||
|
||||
GOOGLE_DRIVE_CLIENT_ID = os.getenv('GOOGLE_DRIVE_CLIENT_ID', '')
|
||||
|
||||
GOOGLE_DRIVE_API_KEY = os.getenv('GOOGLE_DRIVE_API_KEY', '')
|
||||
|
||||
ENABLE_ONEDRIVE_INTEGRATION = os.getenv('ENABLE_ONEDRIVE_INTEGRATION', 'False').lower() == 'true'
|
||||
|
||||
|
||||
ONEDRIVE_CLIENT_ID = os.getenv('ONEDRIVE_CLIENT_ID', '')
|
||||
ONEDRIVE_CLIENT_ID_PERSONAL = os.getenv('ONEDRIVE_CLIENT_ID_PERSONAL', ONEDRIVE_CLIENT_ID)
|
||||
ONEDRIVE_CLIENT_ID_BUSINESS = os.getenv('ONEDRIVE_CLIENT_ID_BUSINESS', ONEDRIVE_CLIENT_ID)
|
||||
|
||||
ENABLE_ONEDRIVE_PERSONAL = os.getenv('ENABLE_ONEDRIVE_PERSONAL', 'True').lower() == 'true' and bool(
|
||||
ONEDRIVE_CLIENT_ID_PERSONAL
|
||||
)
|
||||
ENABLE_ONEDRIVE_BUSINESS = os.getenv('ENABLE_ONEDRIVE_BUSINESS', 'True').lower() == 'true' and bool(
|
||||
ONEDRIVE_CLIENT_ID_BUSINESS
|
||||
ENABLE_GOOGLE_DRIVE_INTEGRATION = PersistentConfig(
|
||||
'ENABLE_GOOGLE_DRIVE_INTEGRATION',
|
||||
'google_drive.enable',
|
||||
os.getenv('ENABLE_GOOGLE_DRIVE_INTEGRATION', 'False').lower() == 'true',
|
||||
)
|
||||
|
||||
ONEDRIVE_SHAREPOINT_URL = os.getenv('ONEDRIVE_SHAREPOINT_URL', '')
|
||||
GOOGLE_DRIVE_CLIENT_ID = PersistentConfig(
|
||||
'GOOGLE_DRIVE_CLIENT_ID',
|
||||
'google_drive.client_id',
|
||||
os.environ.get('GOOGLE_DRIVE_CLIENT_ID', ''),
|
||||
)
|
||||
|
||||
ONEDRIVE_SHAREPOINT_TENANT_ID = os.getenv('ONEDRIVE_SHAREPOINT_TENANT_ID', '')
|
||||
GOOGLE_DRIVE_API_KEY = PersistentConfig(
|
||||
'GOOGLE_DRIVE_API_KEY',
|
||||
'google_drive.api_key',
|
||||
os.environ.get('GOOGLE_DRIVE_API_KEY', ''),
|
||||
)
|
||||
|
||||
ENABLE_ONEDRIVE_INTEGRATION = PersistentConfig(
|
||||
'ENABLE_ONEDRIVE_INTEGRATION',
|
||||
'onedrive.enable',
|
||||
os.getenv('ENABLE_ONEDRIVE_INTEGRATION', 'False').lower() == 'true',
|
||||
)
|
||||
|
||||
|
||||
ENABLE_ONEDRIVE_PERSONAL = os.environ.get('ENABLE_ONEDRIVE_PERSONAL', 'True').lower() == 'true'
|
||||
ENABLE_ONEDRIVE_BUSINESS = os.environ.get('ENABLE_ONEDRIVE_BUSINESS', 'True').lower() == 'true'
|
||||
|
||||
ONEDRIVE_CLIENT_ID = os.environ.get('ONEDRIVE_CLIENT_ID', '')
|
||||
ONEDRIVE_CLIENT_ID_PERSONAL = os.environ.get('ONEDRIVE_CLIENT_ID_PERSONAL', ONEDRIVE_CLIENT_ID)
|
||||
ONEDRIVE_CLIENT_ID_BUSINESS = os.environ.get('ONEDRIVE_CLIENT_ID_BUSINESS', ONEDRIVE_CLIENT_ID)
|
||||
|
||||
ONEDRIVE_SHAREPOINT_URL = PersistentConfig(
|
||||
'ONEDRIVE_SHAREPOINT_URL',
|
||||
'onedrive.sharepoint_url',
|
||||
os.environ.get('ONEDRIVE_SHAREPOINT_URL', ''),
|
||||
)
|
||||
|
||||
ONEDRIVE_SHAREPOINT_TENANT_ID = PersistentConfig(
|
||||
'ONEDRIVE_SHAREPOINT_TENANT_ID',
|
||||
'onedrive.sharepoint_tenant_id',
|
||||
os.environ.get('ONEDRIVE_SHAREPOINT_TENANT_ID', ''),
|
||||
)
|
||||
|
||||
# RAG Content Extraction
|
||||
CONTENT_EXTRACTION_ENGINE = os.getenv('CONTENT_EXTRACTION_ENGINE', '').lower()
|
||||
|
||||
DATALAB_MARKER_API_KEY = os.getenv('DATALAB_MARKER_API_KEY', '')
|
||||
|
||||
DATALAB_MARKER_API_BASE_URL = os.getenv('DATALAB_MARKER_API_BASE_URL', '')
|
||||
|
||||
DATALAB_MARKER_ADDITIONAL_CONFIG = os.getenv('DATALAB_MARKER_ADDITIONAL_CONFIG', '')
|
||||
|
||||
DATALAB_MARKER_USE_LLM = os.getenv('DATALAB_MARKER_USE_LLM', 'false').lower() == 'true'
|
||||
|
||||
DATALAB_MARKER_SKIP_CACHE = os.getenv('DATALAB_MARKER_SKIP_CACHE', 'false').lower() == 'true'
|
||||
|
||||
DATALAB_MARKER_FORCE_OCR = os.getenv('DATALAB_MARKER_FORCE_OCR', 'false').lower() == 'true'
|
||||
|
||||
DATALAB_MARKER_PAGINATE = os.getenv('DATALAB_MARKER_PAGINATE', 'false').lower() == 'true'
|
||||
|
||||
DATALAB_MARKER_STRIP_EXISTING_OCR = os.getenv('DATALAB_MARKER_STRIP_EXISTING_OCR', 'false').lower() == 'true'
|
||||
|
||||
DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION = (
|
||||
os.getenv('DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION', 'false').lower() == 'true'
|
||||
CONTENT_EXTRACTION_ENGINE = PersistentConfig(
|
||||
'CONTENT_EXTRACTION_ENGINE',
|
||||
'rag.CONTENT_EXTRACTION_ENGINE',
|
||||
os.environ.get('CONTENT_EXTRACTION_ENGINE', '').lower(),
|
||||
)
|
||||
|
||||
DATALAB_MARKER_FORMAT_LINES = os.getenv('DATALAB_MARKER_FORMAT_LINES', 'false').lower() == 'true'
|
||||
DATALAB_MARKER_API_KEY = PersistentConfig(
|
||||
'DATALAB_MARKER_API_KEY',
|
||||
'rag.datalab_marker_api_key',
|
||||
os.environ.get('DATALAB_MARKER_API_KEY', ''),
|
||||
)
|
||||
|
||||
DATALAB_MARKER_OUTPUT_FORMAT = os.getenv('DATALAB_MARKER_OUTPUT_FORMAT', 'markdown')
|
||||
DATALAB_MARKER_API_BASE_URL = PersistentConfig(
|
||||
'DATALAB_MARKER_API_BASE_URL',
|
||||
'rag.datalab_marker_api_base_url',
|
||||
os.environ.get('DATALAB_MARKER_API_BASE_URL', ''),
|
||||
)
|
||||
|
||||
MINERU_API_MODE = os.getenv('MINERU_API_MODE', 'local')
|
||||
DATALAB_MARKER_ADDITIONAL_CONFIG = PersistentConfig(
|
||||
'DATALAB_MARKER_ADDITIONAL_CONFIG',
|
||||
'rag.datalab_marker_additional_config',
|
||||
os.environ.get('DATALAB_MARKER_ADDITIONAL_CONFIG', ''),
|
||||
)
|
||||
|
||||
MINERU_API_URL = os.getenv('MINERU_API_URL', 'http://localhost:8000')
|
||||
DATALAB_MARKER_USE_LLM = PersistentConfig(
|
||||
'DATALAB_MARKER_USE_LLM',
|
||||
'rag.DATALAB_MARKER_USE_LLM',
|
||||
os.environ.get('DATALAB_MARKER_USE_LLM', 'false').lower() == 'true',
|
||||
)
|
||||
|
||||
MINERU_API_TIMEOUT = os.getenv('MINERU_API_TIMEOUT', '300')
|
||||
DATALAB_MARKER_SKIP_CACHE = PersistentConfig(
|
||||
'DATALAB_MARKER_SKIP_CACHE',
|
||||
'rag.datalab_marker_skip_cache',
|
||||
os.environ.get('DATALAB_MARKER_SKIP_CACHE', 'false').lower() == 'true',
|
||||
)
|
||||
|
||||
MINERU_API_KEY = os.getenv('MINERU_API_KEY', '')
|
||||
DATALAB_MARKER_FORCE_OCR = PersistentConfig(
|
||||
'DATALAB_MARKER_FORCE_OCR',
|
||||
'rag.datalab_marker_force_ocr',
|
||||
os.environ.get('DATALAB_MARKER_FORCE_OCR', 'false').lower() == 'true',
|
||||
)
|
||||
|
||||
DATALAB_MARKER_PAGINATE = PersistentConfig(
|
||||
'DATALAB_MARKER_PAGINATE',
|
||||
'rag.datalab_marker_paginate',
|
||||
os.environ.get('DATALAB_MARKER_PAGINATE', 'false').lower() == 'true',
|
||||
)
|
||||
|
||||
DATALAB_MARKER_STRIP_EXISTING_OCR = PersistentConfig(
|
||||
'DATALAB_MARKER_STRIP_EXISTING_OCR',
|
||||
'rag.datalab_marker_strip_existing_ocr',
|
||||
os.environ.get('DATALAB_MARKER_STRIP_EXISTING_OCR', 'false').lower() == 'true',
|
||||
)
|
||||
|
||||
DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION = PersistentConfig(
|
||||
'DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION',
|
||||
'rag.datalab_marker_disable_image_extraction',
|
||||
os.environ.get('DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION', 'false').lower() == 'true',
|
||||
)
|
||||
|
||||
DATALAB_MARKER_FORMAT_LINES = PersistentConfig(
|
||||
'DATALAB_MARKER_FORMAT_LINES',
|
||||
'rag.datalab_marker_format_lines',
|
||||
os.environ.get('DATALAB_MARKER_FORMAT_LINES', 'false').lower() == 'true',
|
||||
)
|
||||
|
||||
DATALAB_MARKER_OUTPUT_FORMAT = PersistentConfig(
|
||||
'DATALAB_MARKER_OUTPUT_FORMAT',
|
||||
'rag.datalab_marker_output_format',
|
||||
os.environ.get('DATALAB_MARKER_OUTPUT_FORMAT', 'markdown'),
|
||||
)
|
||||
|
||||
MINERU_API_MODE = PersistentConfig(
|
||||
'MINERU_API_MODE',
|
||||
'rag.mineru_api_mode',
|
||||
os.environ.get('MINERU_API_MODE', 'local'), # "local" or "cloud"
|
||||
)
|
||||
|
||||
MINERU_API_URL = PersistentConfig(
|
||||
'MINERU_API_URL',
|
||||
'rag.mineru_api_url',
|
||||
os.environ.get('MINERU_API_URL', 'http://localhost:8000'),
|
||||
)
|
||||
|
||||
MINERU_API_TIMEOUT = PersistentConfig(
|
||||
'MINERU_API_TIMEOUT',
|
||||
'rag.mineru_api_timeout',
|
||||
os.environ.get('MINERU_API_TIMEOUT', '300'),
|
||||
)
|
||||
|
||||
MINERU_API_KEY = PersistentConfig(
|
||||
'MINERU_API_KEY',
|
||||
'rag.mineru_api_key',
|
||||
os.environ.get('MINERU_API_KEY', ''),
|
||||
)
|
||||
|
||||
mineru_params = os.getenv('MINERU_PARAMS', '')
|
||||
try:
|
||||
@@ -891,29 +2749,41 @@ try:
|
||||
except json.JSONDecodeError:
|
||||
mineru_params = {}
|
||||
|
||||
MINERU_PARAMS = mineru_params
|
||||
MINERU_PARAMS = PersistentConfig(
|
||||
'MINERU_PARAMS',
|
||||
'rag.mineru_params',
|
||||
mineru_params,
|
||||
)
|
||||
|
||||
MINERU_FILE_EXTENSIONS = [ext.strip() for ext in os.getenv('MINERU_FILE_EXTENSIONS', 'pdf').split(',') if ext.strip()]
|
||||
EXTERNAL_DOCUMENT_LOADER_URL = PersistentConfig(
|
||||
'EXTERNAL_DOCUMENT_LOADER_URL',
|
||||
'rag.external_document_loader_url',
|
||||
os.environ.get('EXTERNAL_DOCUMENT_LOADER_URL', ''),
|
||||
)
|
||||
|
||||
EXTERNAL_DOCUMENT_LOADER_URL = os.getenv('EXTERNAL_DOCUMENT_LOADER_URL', '')
|
||||
EXTERNAL_DOCUMENT_LOADER_API_KEY = PersistentConfig(
|
||||
'EXTERNAL_DOCUMENT_LOADER_API_KEY',
|
||||
'rag.external_document_loader_api_key',
|
||||
os.environ.get('EXTERNAL_DOCUMENT_LOADER_API_KEY', ''),
|
||||
)
|
||||
|
||||
EXTERNAL_DOCUMENT_LOADER_API_KEY = os.getenv('EXTERNAL_DOCUMENT_LOADER_API_KEY', '')
|
||||
TIKA_SERVER_URL = PersistentConfig(
|
||||
'TIKA_SERVER_URL',
|
||||
'rag.tika_server_url',
|
||||
os.getenv('TIKA_SERVER_URL', 'http://tika:9998'), # Default for sidecar deployment
|
||||
)
|
||||
|
||||
external_document_loader_headers = os.getenv('EXTERNAL_DOCUMENT_LOADER_HEADERS', '')
|
||||
try:
|
||||
external_document_loader_headers = json.loads(external_document_loader_headers)
|
||||
except json.JSONDecodeError:
|
||||
external_document_loader_headers = {}
|
||||
if not isinstance(external_document_loader_headers, dict):
|
||||
external_document_loader_headers = {}
|
||||
DOCLING_SERVER_URL = PersistentConfig(
|
||||
'DOCLING_SERVER_URL',
|
||||
'rag.docling_server_url',
|
||||
os.getenv('DOCLING_SERVER_URL', 'http://docling:5001'),
|
||||
)
|
||||
|
||||
EXTERNAL_DOCUMENT_LOADER_HEADERS = external_document_loader_headers
|
||||
|
||||
TIKA_SERVER_URL = os.getenv('TIKA_SERVER_URL', 'http://tika:9998')
|
||||
|
||||
DOCLING_SERVER_URL = os.getenv('DOCLING_SERVER_URL', 'http://docling:5001')
|
||||
|
||||
DOCLING_API_KEY = os.getenv('DOCLING_API_KEY', '')
|
||||
DOCLING_API_KEY = PersistentConfig(
|
||||
'DOCLING_API_KEY',
|
||||
'rag.docling_api_key',
|
||||
os.getenv('DOCLING_API_KEY', ''),
|
||||
)
|
||||
|
||||
docling_params = os.getenv('DOCLING_PARAMS', '')
|
||||
try:
|
||||
@@ -921,126 +2791,266 @@ try:
|
||||
except json.JSONDecodeError:
|
||||
docling_params = {}
|
||||
|
||||
DOCLING_PARAMS = docling_params
|
||||
|
||||
DOCUMENT_INTELLIGENCE_ENDPOINT = os.getenv('DOCUMENT_INTELLIGENCE_ENDPOINT', '')
|
||||
|
||||
DOCUMENT_INTELLIGENCE_KEY = os.getenv('DOCUMENT_INTELLIGENCE_KEY', '')
|
||||
|
||||
DOCUMENT_INTELLIGENCE_MODEL = os.getenv('DOCUMENT_INTELLIGENCE_MODEL', 'prebuilt-layout')
|
||||
|
||||
MISTRAL_OCR_API_BASE_URL = os.getenv('MISTRAL_OCR_API_BASE_URL', 'https://api.mistral.ai/v1')
|
||||
|
||||
MISTRAL_OCR_API_KEY = os.getenv('MISTRAL_OCR_API_KEY', '')
|
||||
|
||||
MISTRAL_OCR_USE_BASE64 = os.getenv('MISTRAL_OCR_USE_BASE64', 'False').lower() == 'true'
|
||||
|
||||
PADDLEOCR_VL_BASE_URL = os.getenv('PADDLEOCR_VL_BASE_URL', 'http://localhost:8080')
|
||||
|
||||
PADDLEOCR_VL_TOKEN = os.getenv('PADDLEOCR_VL_TOKEN', '')
|
||||
|
||||
BYPASS_EMBEDDING_AND_RETRIEVAL = os.getenv('BYPASS_EMBEDDING_AND_RETRIEVAL', 'False').lower() == 'true'
|
||||
|
||||
|
||||
RAG_TOP_K = int(os.getenv('RAG_TOP_K', '3'))
|
||||
RAG_TOP_K_RERANKER = int(os.getenv('RAG_TOP_K_RERANKER', '3'))
|
||||
RAG_RELEVANCE_THRESHOLD = float(os.getenv('RAG_RELEVANCE_THRESHOLD', '0.0'))
|
||||
RAG_HYBRID_BM25_WEIGHT = float(os.getenv('RAG_HYBRID_BM25_WEIGHT', '0.5'))
|
||||
|
||||
ENABLE_RAG_HYBRID_SEARCH = os.getenv('ENABLE_RAG_HYBRID_SEARCH', '').lower() == 'true'
|
||||
|
||||
ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS = (
|
||||
os.getenv('ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS', 'False').lower() == 'true'
|
||||
DOCLING_PARAMS = PersistentConfig(
|
||||
'DOCLING_PARAMS',
|
||||
'rag.docling_params',
|
||||
docling_params,
|
||||
)
|
||||
|
||||
RAG_FULL_CONTEXT = os.getenv('RAG_FULL_CONTEXT', 'False').lower() == 'true'
|
||||
|
||||
RAG_FILE_MAX_COUNT = int(os.getenv('RAG_FILE_MAX_COUNT')) if os.getenv('RAG_FILE_MAX_COUNT') else None
|
||||
|
||||
RAG_FILE_MAX_SIZE = int(os.getenv('RAG_FILE_MAX_SIZE')) if os.getenv('RAG_FILE_MAX_SIZE') else None
|
||||
|
||||
RAG_FILE_CONTENT_SEARCH_MAX_CHARS = int(os.getenv('RAG_FILE_CONTENT_SEARCH_MAX_CHARS', str(64 * 1024 * 1024)))
|
||||
|
||||
FILE_IMAGE_COMPRESSION_WIDTH = (
|
||||
int(os.getenv('FILE_IMAGE_COMPRESSION_WIDTH')) if os.getenv('FILE_IMAGE_COMPRESSION_WIDTH') else None
|
||||
DOCUMENT_INTELLIGENCE_ENDPOINT = PersistentConfig(
|
||||
'DOCUMENT_INTELLIGENCE_ENDPOINT',
|
||||
'rag.document_intelligence_endpoint',
|
||||
os.getenv('DOCUMENT_INTELLIGENCE_ENDPOINT', ''),
|
||||
)
|
||||
|
||||
FILE_IMAGE_COMPRESSION_HEIGHT = (
|
||||
int(os.getenv('FILE_IMAGE_COMPRESSION_HEIGHT')) if os.getenv('FILE_IMAGE_COMPRESSION_HEIGHT') else None
|
||||
DOCUMENT_INTELLIGENCE_KEY = PersistentConfig(
|
||||
'DOCUMENT_INTELLIGENCE_KEY',
|
||||
'rag.document_intelligence_key',
|
||||
os.getenv('DOCUMENT_INTELLIGENCE_KEY', ''),
|
||||
)
|
||||
|
||||
DOCUMENT_INTELLIGENCE_MODEL = PersistentConfig(
|
||||
'DOCUMENT_INTELLIGENCE_MODEL',
|
||||
'rag.document_intelligence_model',
|
||||
os.getenv('DOCUMENT_INTELLIGENCE_MODEL', 'prebuilt-layout'),
|
||||
)
|
||||
|
||||
MISTRAL_OCR_API_BASE_URL = PersistentConfig(
|
||||
'MISTRAL_OCR_API_BASE_URL',
|
||||
'rag.MISTRAL_OCR_API_BASE_URL',
|
||||
os.getenv('MISTRAL_OCR_API_BASE_URL', 'https://api.mistral.ai/v1'),
|
||||
)
|
||||
|
||||
MISTRAL_OCR_API_KEY = PersistentConfig(
|
||||
'MISTRAL_OCR_API_KEY',
|
||||
'rag.mistral_ocr_api_key',
|
||||
os.getenv('MISTRAL_OCR_API_KEY', ''),
|
||||
)
|
||||
|
||||
PADDLEOCR_VL_BASE_URL = PersistentConfig(
|
||||
'PADDLEOCR_VL_BASE_URL',
|
||||
'rag.paddleocr_vl_base_url',
|
||||
os.getenv('PADDLEOCR_VL_BASE_URL', 'http://localhost:8080'),
|
||||
)
|
||||
|
||||
PADDLEOCR_VL_TOKEN = PersistentConfig(
|
||||
'PADDLEOCR_VL_TOKEN',
|
||||
'rag.paddleocr_vl_token',
|
||||
os.getenv('PADDLEOCR_VL_TOKEN', ''),
|
||||
)
|
||||
|
||||
BYPASS_EMBEDDING_AND_RETRIEVAL = PersistentConfig(
|
||||
'BYPASS_EMBEDDING_AND_RETRIEVAL',
|
||||
'rag.bypass_embedding_and_retrieval',
|
||||
os.environ.get('BYPASS_EMBEDDING_AND_RETRIEVAL', 'False').lower() == 'true',
|
||||
)
|
||||
|
||||
|
||||
RAG_ALLOWED_FILE_EXTENSIONS = [
|
||||
ext.strip() for ext in os.getenv('RAG_ALLOWED_FILE_EXTENSIONS', '').split(',') if ext.strip()
|
||||
]
|
||||
RAG_TOP_K = PersistentConfig('RAG_TOP_K', 'rag.top_k', int(os.environ.get('RAG_TOP_K', '3')))
|
||||
RAG_TOP_K_RERANKER = PersistentConfig(
|
||||
'RAG_TOP_K_RERANKER',
|
||||
'rag.top_k_reranker',
|
||||
int(os.environ.get('RAG_TOP_K_RERANKER', '3')),
|
||||
)
|
||||
RAG_RELEVANCE_THRESHOLD = PersistentConfig(
|
||||
'RAG_RELEVANCE_THRESHOLD',
|
||||
'rag.relevance_threshold',
|
||||
float(os.environ.get('RAG_RELEVANCE_THRESHOLD', '0.0')),
|
||||
)
|
||||
RAG_HYBRID_BM25_WEIGHT = PersistentConfig(
|
||||
'RAG_HYBRID_BM25_WEIGHT',
|
||||
'rag.hybrid_bm25_weight',
|
||||
float(os.environ.get('RAG_HYBRID_BM25_WEIGHT', '0.5')),
|
||||
)
|
||||
|
||||
RAG_EMBEDDING_ENGINE = os.getenv('RAG_EMBEDDING_ENGINE', '')
|
||||
ENABLE_RAG_HYBRID_SEARCH = PersistentConfig(
|
||||
'ENABLE_RAG_HYBRID_SEARCH',
|
||||
'rag.enable_hybrid_search',
|
||||
os.environ.get('ENABLE_RAG_HYBRID_SEARCH', '').lower() == 'true',
|
||||
)
|
||||
|
||||
PDF_EXTRACT_IMAGES = os.getenv('PDF_EXTRACT_IMAGES', 'False').lower() == 'true'
|
||||
ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS = PersistentConfig(
|
||||
'ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS',
|
||||
'rag.enable_hybrid_search_enriched_texts',
|
||||
os.environ.get('ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS', 'False').lower() == 'true',
|
||||
)
|
||||
|
||||
PDF_LOADER_MODE = os.getenv('PDF_LOADER_MODE', 'page')
|
||||
RAG_FULL_CONTEXT = PersistentConfig(
|
||||
'RAG_FULL_CONTEXT',
|
||||
'rag.full_context',
|
||||
os.getenv('RAG_FULL_CONTEXT', 'False').lower() == 'true',
|
||||
)
|
||||
|
||||
RAG_EMBEDDING_MODEL = os.getenv('RAG_EMBEDDING_MODEL', 'sentence-transformers/all-MiniLM-L6-v2')
|
||||
log.info(f'Embedding model set: {RAG_EMBEDDING_MODEL}')
|
||||
RAG_FILE_MAX_COUNT = PersistentConfig(
|
||||
'RAG_FILE_MAX_COUNT',
|
||||
'rag.file.max_count',
|
||||
(int(os.environ.get('RAG_FILE_MAX_COUNT')) if os.environ.get('RAG_FILE_MAX_COUNT') else None),
|
||||
)
|
||||
|
||||
RAG_TOKENIZER_MODEL = os.getenv('RAG_TOKENIZER_MODEL', '')
|
||||
RAG_FILE_MAX_SIZE = PersistentConfig(
|
||||
'RAG_FILE_MAX_SIZE',
|
||||
'rag.file.max_size',
|
||||
(int(os.environ.get('RAG_FILE_MAX_SIZE')) if os.environ.get('RAG_FILE_MAX_SIZE') else None),
|
||||
)
|
||||
|
||||
FILE_IMAGE_COMPRESSION_WIDTH = PersistentConfig(
|
||||
'FILE_IMAGE_COMPRESSION_WIDTH',
|
||||
'file.image_compression_width',
|
||||
(int(os.environ.get('FILE_IMAGE_COMPRESSION_WIDTH')) if os.environ.get('FILE_IMAGE_COMPRESSION_WIDTH') else None),
|
||||
)
|
||||
|
||||
FILE_IMAGE_COMPRESSION_HEIGHT = PersistentConfig(
|
||||
'FILE_IMAGE_COMPRESSION_HEIGHT',
|
||||
'file.image_compression_height',
|
||||
(int(os.environ.get('FILE_IMAGE_COMPRESSION_HEIGHT')) if os.environ.get('FILE_IMAGE_COMPRESSION_HEIGHT') else None),
|
||||
)
|
||||
|
||||
|
||||
RAG_ALLOWED_FILE_EXTENSIONS = PersistentConfig(
|
||||
'RAG_ALLOWED_FILE_EXTENSIONS',
|
||||
'rag.file.allowed_extensions',
|
||||
[ext.strip() for ext in os.environ.get('RAG_ALLOWED_FILE_EXTENSIONS', '').split(',') if ext.strip()],
|
||||
)
|
||||
|
||||
RAG_EMBEDDING_ENGINE = PersistentConfig(
|
||||
'RAG_EMBEDDING_ENGINE',
|
||||
'rag.embedding_engine',
|
||||
os.environ.get('RAG_EMBEDDING_ENGINE', ''),
|
||||
)
|
||||
|
||||
PDF_EXTRACT_IMAGES = PersistentConfig(
|
||||
'PDF_EXTRACT_IMAGES',
|
||||
'rag.pdf_extract_images',
|
||||
os.environ.get('PDF_EXTRACT_IMAGES', 'False').lower() == 'true',
|
||||
)
|
||||
|
||||
PDF_LOADER_MODE = PersistentConfig(
|
||||
'PDF_LOADER_MODE',
|
||||
'rag.pdf_loader_mode',
|
||||
os.environ.get('PDF_LOADER_MODE', 'page'),
|
||||
)
|
||||
|
||||
RAG_EMBEDDING_MODEL = PersistentConfig(
|
||||
'RAG_EMBEDDING_MODEL',
|
||||
'rag.embedding_model',
|
||||
os.environ.get('RAG_EMBEDDING_MODEL', 'sentence-transformers/all-MiniLM-L6-v2'),
|
||||
)
|
||||
log.info(f'Embedding model set: {RAG_EMBEDDING_MODEL.value}')
|
||||
|
||||
RAG_EMBEDDING_MODEL_AUTO_UPDATE = (
|
||||
not OFFLINE_MODE and os.getenv('RAG_EMBEDDING_MODEL_AUTO_UPDATE', 'True').lower() == 'true'
|
||||
not OFFLINE_MODE and os.environ.get('RAG_EMBEDDING_MODEL_AUTO_UPDATE', 'True').lower() == 'true'
|
||||
)
|
||||
|
||||
RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE = os.getenv('RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE', 'True').lower() == 'true'
|
||||
|
||||
RAG_EMBEDDING_BATCH_SIZE = int(
|
||||
os.getenv('RAG_EMBEDDING_BATCH_SIZE') or os.getenv('RAG_EMBEDDING_OPENAI_BATCH_SIZE', '1')
|
||||
RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE = (
|
||||
os.environ.get('RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE', 'True').lower() == 'true'
|
||||
)
|
||||
|
||||
ENABLE_ASYNC_EMBEDDING = os.getenv('ENABLE_ASYNC_EMBEDDING', 'True').lower() == 'true'
|
||||
RAG_EMBEDDING_BATCH_SIZE = PersistentConfig(
|
||||
'RAG_EMBEDDING_BATCH_SIZE',
|
||||
'rag.embedding_batch_size',
|
||||
int(os.environ.get('RAG_EMBEDDING_BATCH_SIZE') or os.environ.get('RAG_EMBEDDING_OPENAI_BATCH_SIZE', '1')),
|
||||
)
|
||||
|
||||
RAG_EMBEDDING_CONCURRENT_REQUESTS = int(os.getenv('RAG_EMBEDDING_CONCURRENT_REQUESTS', '0'))
|
||||
ENABLE_ASYNC_EMBEDDING = PersistentConfig(
|
||||
'ENABLE_ASYNC_EMBEDDING',
|
||||
'rag.enable_async_embedding',
|
||||
os.environ.get('ENABLE_ASYNC_EMBEDDING', 'True').lower() == 'true',
|
||||
)
|
||||
|
||||
RAG_EMBEDDING_QUERY_PREFIX = os.getenv('RAG_EMBEDDING_QUERY_PREFIX', None)
|
||||
RAG_EMBEDDING_CONCURRENT_REQUESTS = PersistentConfig(
|
||||
'RAG_EMBEDDING_CONCURRENT_REQUESTS',
|
||||
'rag.embedding_concurrent_requests',
|
||||
int(os.getenv('RAG_EMBEDDING_CONCURRENT_REQUESTS', '0')),
|
||||
)
|
||||
|
||||
RAG_EMBEDDING_CONTENT_PREFIX = os.getenv('RAG_EMBEDDING_CONTENT_PREFIX', None)
|
||||
RAG_EMBEDDING_QUERY_PREFIX = os.environ.get('RAG_EMBEDDING_QUERY_PREFIX', None)
|
||||
|
||||
RAG_EMBEDDING_PREFIX_FIELD_NAME = os.getenv('RAG_EMBEDDING_PREFIX_FIELD_NAME', None)
|
||||
RAG_EMBEDDING_CONTENT_PREFIX = os.environ.get('RAG_EMBEDDING_CONTENT_PREFIX', None)
|
||||
|
||||
RAG_RERANKING_ENGINE = os.getenv('RAG_RERANKING_ENGINE', '')
|
||||
RAG_EMBEDDING_PREFIX_FIELD_NAME = os.environ.get('RAG_EMBEDDING_PREFIX_FIELD_NAME', None)
|
||||
|
||||
RAG_RERANKING_MODEL = os.getenv('RAG_RERANKING_MODEL', '')
|
||||
if RAG_RERANKING_MODEL != '':
|
||||
log.info(f'Reranking model set: {RAG_RERANKING_MODEL}')
|
||||
RAG_RERANKING_ENGINE = PersistentConfig(
|
||||
'RAG_RERANKING_ENGINE',
|
||||
'rag.reranking_engine',
|
||||
os.environ.get('RAG_RERANKING_ENGINE', ''),
|
||||
)
|
||||
|
||||
RAG_RERANKING_MODEL = PersistentConfig(
|
||||
'RAG_RERANKING_MODEL',
|
||||
'rag.reranking_model',
|
||||
os.environ.get('RAG_RERANKING_MODEL', ''),
|
||||
)
|
||||
if RAG_RERANKING_MODEL.value != '':
|
||||
log.info(f'Reranking model set: {RAG_RERANKING_MODEL.value}')
|
||||
|
||||
|
||||
RAG_RERANKING_MODEL_AUTO_UPDATE = (
|
||||
not OFFLINE_MODE and os.getenv('RAG_RERANKING_MODEL_AUTO_UPDATE', 'True').lower() == 'true'
|
||||
not OFFLINE_MODE and os.environ.get('RAG_RERANKING_MODEL_AUTO_UPDATE', 'True').lower() == 'true'
|
||||
)
|
||||
|
||||
RAG_RERANKING_MODEL_TRUST_REMOTE_CODE = os.getenv('RAG_RERANKING_MODEL_TRUST_REMOTE_CODE', 'True').lower() == 'true'
|
||||
RAG_RERANKING_MODEL_TRUST_REMOTE_CODE = (
|
||||
os.environ.get('RAG_RERANKING_MODEL_TRUST_REMOTE_CODE', 'True').lower() == 'true'
|
||||
)
|
||||
|
||||
RAG_RERANKING_BATCH_SIZE = int(os.getenv('RAG_RERANKING_BATCH_SIZE', '32'))
|
||||
RAG_RERANKING_BATCH_SIZE = PersistentConfig(
|
||||
'RAG_RERANKING_BATCH_SIZE',
|
||||
'rag.reranking_batch_size',
|
||||
int(os.environ.get('RAG_RERANKING_BATCH_SIZE', '32')),
|
||||
)
|
||||
|
||||
RAG_EXTERNAL_RERANKER_URL = os.getenv('RAG_EXTERNAL_RERANKER_URL', '')
|
||||
RAG_EXTERNAL_RERANKER_URL = PersistentConfig(
|
||||
'RAG_EXTERNAL_RERANKER_URL',
|
||||
'rag.external_reranker_url',
|
||||
os.environ.get('RAG_EXTERNAL_RERANKER_URL', ''),
|
||||
)
|
||||
|
||||
RAG_EXTERNAL_RERANKER_API_KEY = os.getenv('RAG_EXTERNAL_RERANKER_API_KEY', '')
|
||||
RAG_EXTERNAL_RERANKER_API_KEY = PersistentConfig(
|
||||
'RAG_EXTERNAL_RERANKER_API_KEY',
|
||||
'rag.external_reranker_api_key',
|
||||
os.environ.get('RAG_EXTERNAL_RERANKER_API_KEY', ''),
|
||||
)
|
||||
|
||||
RAG_EXTERNAL_RERANKER_TIMEOUT = os.getenv('RAG_EXTERNAL_RERANKER_TIMEOUT', '')
|
||||
RAG_EXTERNAL_RERANKER_TIMEOUT = PersistentConfig(
|
||||
'RAG_EXTERNAL_RERANKER_TIMEOUT',
|
||||
'rag.external_reranker_timeout',
|
||||
os.environ.get('RAG_EXTERNAL_RERANKER_TIMEOUT', ''),
|
||||
)
|
||||
|
||||
|
||||
RAG_TEXT_SPLITTER = os.getenv('RAG_TEXT_SPLITTER', '')
|
||||
RAG_TEXT_SPLITTER = PersistentConfig(
|
||||
'RAG_TEXT_SPLITTER',
|
||||
'rag.text_splitter',
|
||||
os.environ.get('RAG_TEXT_SPLITTER', ''),
|
||||
)
|
||||
|
||||
ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER = os.getenv('ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER', 'True').lower() == 'true'
|
||||
ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER = PersistentConfig(
|
||||
'ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER',
|
||||
'rag.enable_markdown_header_text_splitter',
|
||||
os.environ.get('ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER', 'True').lower() == 'true',
|
||||
)
|
||||
|
||||
|
||||
TIKTOKEN_CACHE_DIR = os.getenv('TIKTOKEN_CACHE_DIR', f'{CACHE_DIR}/tiktoken')
|
||||
TIKTOKEN_ENCODING_NAME = os.getenv('TIKTOKEN_ENCODING_NAME', 'cl100k_base')
|
||||
TIKTOKEN_CACHE_DIR = os.environ.get('TIKTOKEN_CACHE_DIR', f'{CACHE_DIR}/tiktoken')
|
||||
TIKTOKEN_ENCODING_NAME = PersistentConfig(
|
||||
'TIKTOKEN_ENCODING_NAME',
|
||||
'rag.tiktoken_encoding_name',
|
||||
os.environ.get('TIKTOKEN_ENCODING_NAME', 'cl100k_base'),
|
||||
)
|
||||
|
||||
|
||||
CHUNK_SIZE = int(os.getenv('CHUNK_SIZE', '1000'))
|
||||
CHUNK_SIZE = PersistentConfig('CHUNK_SIZE', 'rag.chunk_size', int(os.environ.get('CHUNK_SIZE', '1000')))
|
||||
|
||||
CHUNK_MIN_SIZE_TARGET = int(os.getenv('CHUNK_MIN_SIZE_TARGET', '0'))
|
||||
CHUNK_MIN_SIZE_TARGET = PersistentConfig(
|
||||
'CHUNK_MIN_SIZE_TARGET',
|
||||
'rag.chunk_min_size_target',
|
||||
int(os.environ.get('CHUNK_MIN_SIZE_TARGET', '0')),
|
||||
)
|
||||
|
||||
CHUNK_OVERLAP = int(os.getenv('CHUNK_OVERLAP', '100'))
|
||||
CHUNK_OVERLAP = PersistentConfig(
|
||||
'CHUNK_OVERLAP',
|
||||
'rag.chunk_overlap',
|
||||
int(os.environ.get('CHUNK_OVERLAP', '100')),
|
||||
)
|
||||
|
||||
DEFAULT_RAG_TEMPLATE = """### Task:
|
||||
Respond to the user query using the provided context, incorporating inline citations in the format [id] **only when the <source> tag includes an explicit id attribute** (e.g., <source id="1">).
|
||||
@@ -1068,29 +3078,53 @@ Provide a clear and direct response to the user's query, including inline citati
|
||||
</context>
|
||||
"""
|
||||
|
||||
RAG_TEMPLATE = os.getenv('RAG_TEMPLATE', DEFAULT_RAG_TEMPLATE)
|
||||
|
||||
RAG_OPENAI_API_BASE_URL = os.getenv('RAG_OPENAI_API_BASE_URL', OPENAI_API_BASE_URL)
|
||||
RAG_OPENAI_API_KEY = os.getenv('RAG_OPENAI_API_KEY', OPENAI_API_KEY)
|
||||
|
||||
RAG_AZURE_OPENAI_BASE_URL = os.getenv('RAG_AZURE_OPENAI_BASE_URL', '')
|
||||
RAG_AZURE_OPENAI_API_KEY = os.getenv('RAG_AZURE_OPENAI_API_KEY', '')
|
||||
RAG_AZURE_OPENAI_API_VERSION = os.getenv('RAG_AZURE_OPENAI_API_VERSION', '')
|
||||
|
||||
RAG_OLLAMA_BASE_URL = os.getenv('RAG_OLLAMA_BASE_URL', OLLAMA_BASE_URL)
|
||||
|
||||
RAG_OLLAMA_API_KEY = os.getenv('RAG_OLLAMA_API_KEY', '')
|
||||
|
||||
|
||||
ENABLE_LOCAL_WEB_FETCH = (
|
||||
os.getenv(
|
||||
'ENABLE_LOCAL_WEB_FETCH',
|
||||
os.getenv('ENABLE_RAG_LOCAL_WEB_FETCH', 'False'),
|
||||
).lower()
|
||||
== 'true'
|
||||
RAG_TEMPLATE = PersistentConfig(
|
||||
'RAG_TEMPLATE',
|
||||
'rag.template',
|
||||
os.environ.get('RAG_TEMPLATE', DEFAULT_RAG_TEMPLATE),
|
||||
)
|
||||
# Deprecated compatibility alias; use ENABLE_LOCAL_WEB_FETCH for new deployments.
|
||||
ENABLE_RAG_LOCAL_WEB_FETCH = ENABLE_LOCAL_WEB_FETCH
|
||||
|
||||
RAG_OPENAI_API_BASE_URL = PersistentConfig(
|
||||
'RAG_OPENAI_API_BASE_URL',
|
||||
'rag.openai_api_base_url',
|
||||
os.getenv('RAG_OPENAI_API_BASE_URL', OPENAI_API_BASE_URL),
|
||||
)
|
||||
RAG_OPENAI_API_KEY = PersistentConfig(
|
||||
'RAG_OPENAI_API_KEY',
|
||||
'rag.openai_api_key',
|
||||
os.getenv('RAG_OPENAI_API_KEY', OPENAI_API_KEY),
|
||||
)
|
||||
|
||||
RAG_AZURE_OPENAI_BASE_URL = PersistentConfig(
|
||||
'RAG_AZURE_OPENAI_BASE_URL',
|
||||
'rag.azure_openai.base_url',
|
||||
os.getenv('RAG_AZURE_OPENAI_BASE_URL', ''),
|
||||
)
|
||||
RAG_AZURE_OPENAI_API_KEY = PersistentConfig(
|
||||
'RAG_AZURE_OPENAI_API_KEY',
|
||||
'rag.azure_openai.api_key',
|
||||
os.getenv('RAG_AZURE_OPENAI_API_KEY', ''),
|
||||
)
|
||||
RAG_AZURE_OPENAI_API_VERSION = PersistentConfig(
|
||||
'RAG_AZURE_OPENAI_API_VERSION',
|
||||
'rag.azure_openai.api_version',
|
||||
os.getenv('RAG_AZURE_OPENAI_API_VERSION', ''),
|
||||
)
|
||||
|
||||
RAG_OLLAMA_BASE_URL = PersistentConfig(
|
||||
'RAG_OLLAMA_BASE_URL',
|
||||
'rag.ollama.url',
|
||||
os.getenv('RAG_OLLAMA_BASE_URL', OLLAMA_BASE_URL),
|
||||
)
|
||||
|
||||
RAG_OLLAMA_API_KEY = PersistentConfig(
|
||||
'RAG_OLLAMA_API_KEY',
|
||||
'rag.ollama.key',
|
||||
os.getenv('RAG_OLLAMA_API_KEY', ''),
|
||||
)
|
||||
|
||||
|
||||
ENABLE_RAG_LOCAL_WEB_FETCH = os.getenv('ENABLE_RAG_LOCAL_WEB_FETCH', 'False').lower() == 'true'
|
||||
|
||||
|
||||
DEFAULT_WEB_FETCH_FILTER_LIST = [
|
||||
@@ -1110,34 +3144,53 @@ else:
|
||||
WEB_FETCH_FILTER_LIST = list(set(DEFAULT_WEB_FETCH_FILTER_LIST + web_fetch_filter_list))
|
||||
|
||||
|
||||
YOUTUBE_LOADER_LANGUAGE = os.getenv('YOUTUBE_LOADER_LANGUAGE', 'en').split(',')
|
||||
|
||||
YOUTUBE_LOADER_PROXY_URL = os.getenv('YOUTUBE_LOADER_PROXY_URL', '')
|
||||
|
||||
|
||||
####################################
|
||||
# Web Search
|
||||
####################################
|
||||
|
||||
ENABLE_WEB_SEARCH = os.getenv('ENABLE_WEB_SEARCH', 'False').lower() == 'true'
|
||||
|
||||
ENABLE_WEB_SEARCH_CONFIRMATION = os.getenv('ENABLE_WEB_SEARCH_CONFIRMATION', 'False').lower() == 'true'
|
||||
|
||||
WEB_SEARCH_CONFIRMATION_CONTENT = os.getenv(
|
||||
'WEB_SEARCH_CONFIRMATION_CONTENT',
|
||||
'Your query will be sent to the configured web search provider.',
|
||||
YOUTUBE_LOADER_LANGUAGE = PersistentConfig(
|
||||
'YOUTUBE_LOADER_LANGUAGE',
|
||||
'rag.youtube_loader_language',
|
||||
os.getenv('YOUTUBE_LOADER_LANGUAGE', 'en').split(','),
|
||||
)
|
||||
|
||||
WEB_SEARCH_ENGINE = os.getenv('WEB_SEARCH_ENGINE', '')
|
||||
|
||||
BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = (
|
||||
os.getenv('BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL', 'False').lower() == 'true'
|
||||
YOUTUBE_LOADER_PROXY_URL = PersistentConfig(
|
||||
'YOUTUBE_LOADER_PROXY_URL',
|
||||
'rag.youtube_loader_proxy_url',
|
||||
os.getenv('YOUTUBE_LOADER_PROXY_URL', ''),
|
||||
)
|
||||
|
||||
|
||||
BYPASS_WEB_SEARCH_WEB_LOADER = os.getenv('BYPASS_WEB_SEARCH_WEB_LOADER', 'False').lower() == 'true'
|
||||
####################################
|
||||
# Web Search (RAG)
|
||||
####################################
|
||||
|
||||
WEB_SEARCH_RESULT_COUNT = int(os.getenv('WEB_SEARCH_RESULT_COUNT', '3'))
|
||||
ENABLE_WEB_SEARCH = PersistentConfig(
|
||||
'ENABLE_WEB_SEARCH',
|
||||
'rag.web.search.enable',
|
||||
os.getenv('ENABLE_WEB_SEARCH', 'False').lower() == 'true',
|
||||
)
|
||||
|
||||
WEB_SEARCH_ENGINE = PersistentConfig(
|
||||
'WEB_SEARCH_ENGINE',
|
||||
'rag.web.search.engine',
|
||||
os.getenv('WEB_SEARCH_ENGINE', ''),
|
||||
)
|
||||
|
||||
BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = PersistentConfig(
|
||||
'BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL',
|
||||
'rag.web.search.bypass_embedding_and_retrieval',
|
||||
os.getenv('BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL', 'False').lower() == 'true',
|
||||
)
|
||||
|
||||
|
||||
BYPASS_WEB_SEARCH_WEB_LOADER = PersistentConfig(
|
||||
'BYPASS_WEB_SEARCH_WEB_LOADER',
|
||||
'rag.web.search.bypass_web_loader',
|
||||
os.getenv('BYPASS_WEB_SEARCH_WEB_LOADER', 'False').lower() == 'true',
|
||||
)
|
||||
|
||||
WEB_SEARCH_RESULT_COUNT = PersistentConfig(
|
||||
'WEB_SEARCH_RESULT_COUNT',
|
||||
'rag.web.search.result_count',
|
||||
int(os.getenv('WEB_SEARCH_RESULT_COUNT', '3')),
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
@@ -1152,158 +3205,378 @@ except Exception as e:
|
||||
|
||||
# You can provide a list of your own websites to filter after performing a web search.
|
||||
# This ensures the highest level of safety and reliability of the information sources.
|
||||
WEB_SEARCH_DOMAIN_FILTER_LIST = web_search_domain_filter_list
|
||||
|
||||
WEB_SEARCH_CONCURRENT_REQUESTS = int(os.getenv('WEB_SEARCH_CONCURRENT_REQUESTS', '0'))
|
||||
|
||||
WEB_FETCH_MAX_CONTENT_LENGTH = (
|
||||
int(os.getenv('WEB_FETCH_MAX_CONTENT_LENGTH')) if os.getenv('WEB_FETCH_MAX_CONTENT_LENGTH') else None
|
||||
WEB_SEARCH_DOMAIN_FILTER_LIST = PersistentConfig(
|
||||
'WEB_SEARCH_DOMAIN_FILTER_LIST',
|
||||
'rag.web.search.domain.filter_list',
|
||||
web_search_domain_filter_list,
|
||||
)
|
||||
|
||||
WEB_LOADER_ENGINE = os.getenv('WEB_LOADER_ENGINE', '')
|
||||
|
||||
|
||||
WEB_LOADER_CONCURRENT_REQUESTS = int(os.getenv('WEB_LOADER_CONCURRENT_REQUESTS', '10'))
|
||||
|
||||
WEB_LOADER_TIMEOUT = os.getenv('WEB_LOADER_TIMEOUT', '')
|
||||
|
||||
|
||||
ENABLE_WEB_LOADER_SSL_VERIFICATION = os.getenv('ENABLE_WEB_LOADER_SSL_VERIFICATION', 'True').lower() == 'true'
|
||||
|
||||
WEB_SEARCH_TRUST_ENV = os.getenv('WEB_SEARCH_TRUST_ENV', 'True').lower() == 'true'
|
||||
|
||||
|
||||
OLLAMA_CLOUD_WEB_SEARCH_API_KEY = os.getenv('OLLAMA_CLOUD_API_KEY', '')
|
||||
|
||||
SEARXNG_QUERY_URL = os.getenv('SEARXNG_QUERY_URL', '')
|
||||
|
||||
SEARXNG_LANGUAGE = os.getenv('SEARXNG_LANGUAGE', 'all')
|
||||
|
||||
YACY_QUERY_URL = os.getenv('YACY_QUERY_URL', '')
|
||||
|
||||
YACY_USERNAME = os.getenv('YACY_USERNAME', '')
|
||||
|
||||
YACY_PASSWORD = os.getenv('YACY_PASSWORD', '')
|
||||
|
||||
GOOGLE_PSE_API_KEY = os.getenv('GOOGLE_PSE_API_KEY', '')
|
||||
|
||||
GOOGLE_PSE_ENGINE_ID = os.getenv('GOOGLE_PSE_ENGINE_ID', '')
|
||||
|
||||
BRAVE_SEARCH_API_KEY = os.getenv('BRAVE_SEARCH_API_KEY', '')
|
||||
|
||||
BRAVE_SEARCH_CONTEXT_TOKENS = int(os.getenv('BRAVE_SEARCH_CONTEXT_TOKENS', '8192'))
|
||||
|
||||
KAGI_SEARCH_API_KEY = os.getenv('KAGI_SEARCH_API_KEY', '')
|
||||
|
||||
MOJEEK_SEARCH_API_KEY = os.getenv('MOJEEK_SEARCH_API_KEY', '')
|
||||
|
||||
BOCHA_SEARCH_API_KEY = os.getenv('BOCHA_SEARCH_API_KEY', '')
|
||||
|
||||
SERPSTACK_API_KEY = os.getenv('SERPSTACK_API_KEY', '')
|
||||
|
||||
SERPSTACK_HTTPS = os.getenv('SERPSTACK_HTTPS', 'True').lower() == 'true'
|
||||
|
||||
SERPER_API_KEY = os.getenv('SERPER_API_KEY', '')
|
||||
|
||||
SERPLY_API_KEY = os.getenv('SERPLY_API_KEY', '')
|
||||
|
||||
SERPHOUSE_API_KEY = os.getenv('SERPHOUSE_API_KEY', '')
|
||||
|
||||
SERPHOUSE_DOMAIN = os.getenv('SERPHOUSE_DOMAIN', 'google.com')
|
||||
|
||||
DDGS_BACKEND = os.getenv('DDGS_BACKEND', 'auto')
|
||||
|
||||
JINA_API_KEY = os.getenv('JINA_API_KEY', '')
|
||||
|
||||
JINA_API_BASE_URL = os.getenv('JINA_API_BASE_URL', '')
|
||||
|
||||
SEARCHAPI_API_KEY = os.getenv('SEARCHAPI_API_KEY', '')
|
||||
|
||||
SEARCHAPI_ENGINE = os.getenv('SEARCHAPI_ENGINE', '')
|
||||
|
||||
SERPAPI_API_KEY = os.getenv('SERPAPI_API_KEY', '')
|
||||
|
||||
SERPAPI_ENGINE = os.getenv('SERPAPI_ENGINE', '')
|
||||
|
||||
BING_SEARCH_V7_ENDPOINT = os.getenv('BING_SEARCH_V7_ENDPOINT', 'https://api.bing.microsoft.com/v7.0/search')
|
||||
|
||||
BING_SEARCH_V7_SUBSCRIPTION_KEY = os.getenv('BING_SEARCH_V7_SUBSCRIPTION_KEY', '')
|
||||
|
||||
AZURE_AI_SEARCH_API_KEY = os.getenv('AZURE_AI_SEARCH_API_KEY', '')
|
||||
|
||||
AZURE_AI_SEARCH_ENDPOINT = os.getenv('AZURE_AI_SEARCH_ENDPOINT', '')
|
||||
|
||||
AZURE_AI_SEARCH_INDEX_NAME = os.getenv('AZURE_AI_SEARCH_INDEX_NAME', '')
|
||||
|
||||
EXA_API_KEY = os.getenv('EXA_API_KEY', '')
|
||||
|
||||
PERPLEXITY_API_KEY = os.getenv('PERPLEXITY_API_KEY', '')
|
||||
|
||||
PERPLEXITY_MODEL = os.getenv('PERPLEXITY_MODEL', 'sonar')
|
||||
|
||||
PERPLEXITY_SEARCH_CONTEXT_USAGE = os.getenv('PERPLEXITY_SEARCH_CONTEXT_USAGE', 'medium')
|
||||
|
||||
PERPLEXITY_SEARCH_API_URL = os.getenv('PERPLEXITY_SEARCH_API_URL', 'https://api.perplexity.ai/search')
|
||||
|
||||
MICROSOFT_WEB_IQ_API_BASE_URL = os.getenv('MICROSOFT_WEB_IQ_API_BASE_URL', 'https://api.microsoft.ai/v3')
|
||||
|
||||
MICROSOFT_WEB_IQ_API_KEY = os.getenv('MICROSOFT_WEB_IQ_API_KEY', '')
|
||||
|
||||
MICROSOFT_WEB_IQ_LANGUAGE = os.getenv('MICROSOFT_WEB_IQ_LANGUAGE', 'en')
|
||||
|
||||
SOUGOU_API_SID = os.getenv('SOUGOU_API_SID', '')
|
||||
|
||||
SOUGOU_API_SK = os.getenv('SOUGOU_API_SK', '')
|
||||
|
||||
TAVILY_API_KEY = os.getenv('TAVILY_API_KEY', '')
|
||||
|
||||
TAVILY_EXTRACT_DEPTH = os.getenv('TAVILY_EXTRACT_DEPTH', 'basic')
|
||||
|
||||
PLAYWRIGHT_WS_URL = os.getenv('PLAYWRIGHT_WS_URL', '')
|
||||
|
||||
PLAYWRIGHT_TIMEOUT = int(os.getenv('PLAYWRIGHT_TIMEOUT', '10000'))
|
||||
|
||||
FIRECRAWL_API_KEY = os.getenv('FIRECRAWL_API_KEY', '')
|
||||
|
||||
FIRECRAWL_API_BASE_URL = os.getenv('FIRECRAWL_API_BASE_URL', 'https://api.firecrawl.dev')
|
||||
|
||||
FIRECRAWL_TIMEOUT = os.getenv('FIRECRAWL_TIMEOUT', '')
|
||||
|
||||
EXTERNAL_WEB_SEARCH_URL = os.getenv('EXTERNAL_WEB_SEARCH_URL', '')
|
||||
|
||||
EXTERNAL_WEB_SEARCH_API_KEY = os.getenv('EXTERNAL_WEB_SEARCH_API_KEY', '')
|
||||
|
||||
EXTERNAL_WEB_LOADER_URL = os.getenv('EXTERNAL_WEB_LOADER_URL', '')
|
||||
|
||||
EXTERNAL_WEB_LOADER_API_KEY = os.getenv('EXTERNAL_WEB_LOADER_API_KEY', '')
|
||||
|
||||
YANDEX_WEB_SEARCH_URL = os.getenv('YANDEX_WEB_SEARCH_URL', '')
|
||||
|
||||
YANDEX_WEB_SEARCH_API_KEY = os.getenv('YANDEX_WEB_SEARCH_API_KEY', '')
|
||||
|
||||
YANDEX_WEB_SEARCH_CONFIG = os.getenv('YANDEX_WEB_SEARCH_CONFIG', '')
|
||||
|
||||
YOUCOM_API_KEY = os.getenv('YOUCOM_API_KEY', os.getenv('YDC_API_KEY', ''))
|
||||
|
||||
LINKUP_API_KEY = os.getenv('LINKUP_API_KEY', '')
|
||||
|
||||
linkup_search_params = os.getenv('LINKUP_SEARCH_PARAMS', '')
|
||||
try:
|
||||
linkup_search_params = json.loads(linkup_search_params)
|
||||
except json.JSONDecodeError:
|
||||
linkup_search_params = {}
|
||||
|
||||
LINKUP_SEARCH_PARAMS = linkup_search_params
|
||||
WEB_SEARCH_CONCURRENT_REQUESTS = PersistentConfig(
|
||||
'WEB_SEARCH_CONCURRENT_REQUESTS',
|
||||
'rag.web.search.concurrent_requests',
|
||||
int(os.getenv('WEB_SEARCH_CONCURRENT_REQUESTS', '0')),
|
||||
)
|
||||
|
||||
WEB_FETCH_MAX_CONTENT_LENGTH = PersistentConfig(
|
||||
'WEB_FETCH_MAX_CONTENT_LENGTH',
|
||||
'rag.web.fetch.max_content_length',
|
||||
(int(os.environ.get('WEB_FETCH_MAX_CONTENT_LENGTH')) if os.environ.get('WEB_FETCH_MAX_CONTENT_LENGTH') else None),
|
||||
)
|
||||
|
||||
WEB_LOADER_ENGINE = PersistentConfig(
|
||||
'WEB_LOADER_ENGINE',
|
||||
'rag.web.loader.engine',
|
||||
os.environ.get('WEB_LOADER_ENGINE', ''),
|
||||
)
|
||||
|
||||
|
||||
WEB_LOADER_CONCURRENT_REQUESTS = PersistentConfig(
|
||||
'WEB_LOADER_CONCURRENT_REQUESTS',
|
||||
'rag.web.loader.concurrent_requests',
|
||||
int(os.getenv('WEB_LOADER_CONCURRENT_REQUESTS', '10')),
|
||||
)
|
||||
|
||||
WEB_LOADER_TIMEOUT = PersistentConfig(
|
||||
'WEB_LOADER_TIMEOUT',
|
||||
'rag.web.loader.timeout',
|
||||
os.getenv('WEB_LOADER_TIMEOUT', ''),
|
||||
)
|
||||
|
||||
|
||||
ENABLE_WEB_LOADER_SSL_VERIFICATION = PersistentConfig(
|
||||
'ENABLE_WEB_LOADER_SSL_VERIFICATION',
|
||||
'rag.web.loader.ssl_verification',
|
||||
os.environ.get('ENABLE_WEB_LOADER_SSL_VERIFICATION', 'True').lower() == 'true',
|
||||
)
|
||||
|
||||
WEB_SEARCH_TRUST_ENV = PersistentConfig(
|
||||
'WEB_SEARCH_TRUST_ENV',
|
||||
'rag.web.search.trust_env',
|
||||
os.getenv('WEB_SEARCH_TRUST_ENV', 'False').lower() == 'true',
|
||||
)
|
||||
|
||||
|
||||
OLLAMA_CLOUD_WEB_SEARCH_API_KEY = PersistentConfig(
|
||||
'OLLAMA_CLOUD_WEB_SEARCH_API_KEY',
|
||||
'rag.web.search.ollama_cloud_api_key',
|
||||
os.getenv('OLLAMA_CLOUD_API_KEY', ''),
|
||||
)
|
||||
|
||||
SEARXNG_QUERY_URL = PersistentConfig(
|
||||
'SEARXNG_QUERY_URL',
|
||||
'rag.web.search.searxng_query_url',
|
||||
os.getenv('SEARXNG_QUERY_URL', ''),
|
||||
)
|
||||
|
||||
SEARXNG_LANGUAGE = PersistentConfig(
|
||||
'SEARXNG_LANGUAGE',
|
||||
'rag.web.search.searxng_language',
|
||||
os.getenv('SEARXNG_LANGUAGE', 'all'),
|
||||
)
|
||||
|
||||
YACY_QUERY_URL = PersistentConfig(
|
||||
'YACY_QUERY_URL',
|
||||
'rag.web.search.yacy_query_url',
|
||||
os.getenv('YACY_QUERY_URL', ''),
|
||||
)
|
||||
|
||||
YACY_USERNAME = PersistentConfig(
|
||||
'YACY_USERNAME',
|
||||
'rag.web.search.yacy_username',
|
||||
os.getenv('YACY_USERNAME', ''),
|
||||
)
|
||||
|
||||
YACY_PASSWORD = PersistentConfig(
|
||||
'YACY_PASSWORD',
|
||||
'rag.web.search.yacy_password',
|
||||
os.getenv('YACY_PASSWORD', ''),
|
||||
)
|
||||
|
||||
GOOGLE_PSE_API_KEY = PersistentConfig(
|
||||
'GOOGLE_PSE_API_KEY',
|
||||
'rag.web.search.google_pse_api_key',
|
||||
os.getenv('GOOGLE_PSE_API_KEY', ''),
|
||||
)
|
||||
|
||||
GOOGLE_PSE_ENGINE_ID = PersistentConfig(
|
||||
'GOOGLE_PSE_ENGINE_ID',
|
||||
'rag.web.search.google_pse_engine_id',
|
||||
os.getenv('GOOGLE_PSE_ENGINE_ID', ''),
|
||||
)
|
||||
|
||||
BRAVE_SEARCH_API_KEY = PersistentConfig(
|
||||
'BRAVE_SEARCH_API_KEY',
|
||||
'rag.web.search.brave_search_api_key',
|
||||
os.getenv('BRAVE_SEARCH_API_KEY', ''),
|
||||
)
|
||||
|
||||
KAGI_SEARCH_API_KEY = PersistentConfig(
|
||||
'KAGI_SEARCH_API_KEY',
|
||||
'rag.web.search.kagi_search_api_key',
|
||||
os.getenv('KAGI_SEARCH_API_KEY', ''),
|
||||
)
|
||||
|
||||
MOJEEK_SEARCH_API_KEY = PersistentConfig(
|
||||
'MOJEEK_SEARCH_API_KEY',
|
||||
'rag.web.search.mojeek_search_api_key',
|
||||
os.getenv('MOJEEK_SEARCH_API_KEY', ''),
|
||||
)
|
||||
|
||||
BOCHA_SEARCH_API_KEY = PersistentConfig(
|
||||
'BOCHA_SEARCH_API_KEY',
|
||||
'rag.web.search.bocha_search_api_key',
|
||||
os.getenv('BOCHA_SEARCH_API_KEY', ''),
|
||||
)
|
||||
|
||||
SERPSTACK_API_KEY = PersistentConfig(
|
||||
'SERPSTACK_API_KEY',
|
||||
'rag.web.search.serpstack_api_key',
|
||||
os.getenv('SERPSTACK_API_KEY', ''),
|
||||
)
|
||||
|
||||
SERPSTACK_HTTPS = PersistentConfig(
|
||||
'SERPSTACK_HTTPS',
|
||||
'rag.web.search.serpstack_https',
|
||||
os.getenv('SERPSTACK_HTTPS', 'True').lower() == 'true',
|
||||
)
|
||||
|
||||
SERPER_API_KEY = PersistentConfig(
|
||||
'SERPER_API_KEY',
|
||||
'rag.web.search.serper_api_key',
|
||||
os.getenv('SERPER_API_KEY', ''),
|
||||
)
|
||||
|
||||
SERPLY_API_KEY = PersistentConfig(
|
||||
'SERPLY_API_KEY',
|
||||
'rag.web.search.serply_api_key',
|
||||
os.getenv('SERPLY_API_KEY', ''),
|
||||
)
|
||||
|
||||
DDGS_BACKEND = PersistentConfig(
|
||||
'DDGS_BACKEND',
|
||||
'rag.web.search.ddgs_backend',
|
||||
os.getenv('DDGS_BACKEND', 'auto'),
|
||||
)
|
||||
|
||||
JINA_API_KEY = PersistentConfig(
|
||||
'JINA_API_KEY',
|
||||
'rag.web.search.jina_api_key',
|
||||
os.getenv('JINA_API_KEY', ''),
|
||||
)
|
||||
|
||||
JINA_API_BASE_URL = PersistentConfig(
|
||||
'JINA_API_BASE_URL',
|
||||
'rag.web.search.jina_api_base_url',
|
||||
os.getenv('JINA_API_BASE_URL', ''),
|
||||
)
|
||||
|
||||
SEARCHAPI_API_KEY = PersistentConfig(
|
||||
'SEARCHAPI_API_KEY',
|
||||
'rag.web.search.searchapi_api_key',
|
||||
os.getenv('SEARCHAPI_API_KEY', ''),
|
||||
)
|
||||
|
||||
SEARCHAPI_ENGINE = PersistentConfig(
|
||||
'SEARCHAPI_ENGINE',
|
||||
'rag.web.search.searchapi_engine',
|
||||
os.getenv('SEARCHAPI_ENGINE', ''),
|
||||
)
|
||||
|
||||
SERPAPI_API_KEY = PersistentConfig(
|
||||
'SERPAPI_API_KEY',
|
||||
'rag.web.search.serpapi_api_key',
|
||||
os.getenv('SERPAPI_API_KEY', ''),
|
||||
)
|
||||
|
||||
SERPAPI_ENGINE = PersistentConfig(
|
||||
'SERPAPI_ENGINE',
|
||||
'rag.web.search.serpapi_engine',
|
||||
os.getenv('SERPAPI_ENGINE', ''),
|
||||
)
|
||||
|
||||
BING_SEARCH_V7_ENDPOINT = PersistentConfig(
|
||||
'BING_SEARCH_V7_ENDPOINT',
|
||||
'rag.web.search.bing_search_v7_endpoint',
|
||||
os.environ.get('BING_SEARCH_V7_ENDPOINT', 'https://api.bing.microsoft.com/v7.0/search'),
|
||||
)
|
||||
|
||||
BING_SEARCH_V7_SUBSCRIPTION_KEY = PersistentConfig(
|
||||
'BING_SEARCH_V7_SUBSCRIPTION_KEY',
|
||||
'rag.web.search.bing_search_v7_subscription_key',
|
||||
os.environ.get('BING_SEARCH_V7_SUBSCRIPTION_KEY', ''),
|
||||
)
|
||||
|
||||
AZURE_AI_SEARCH_API_KEY = PersistentConfig(
|
||||
'AZURE_AI_SEARCH_API_KEY',
|
||||
'rag.web.search.azure_ai_search_api_key',
|
||||
os.environ.get('AZURE_AI_SEARCH_API_KEY', ''),
|
||||
)
|
||||
|
||||
AZURE_AI_SEARCH_ENDPOINT = PersistentConfig(
|
||||
'AZURE_AI_SEARCH_ENDPOINT',
|
||||
'rag.web.search.azure_ai_search_endpoint',
|
||||
os.environ.get('AZURE_AI_SEARCH_ENDPOINT', ''),
|
||||
)
|
||||
|
||||
AZURE_AI_SEARCH_INDEX_NAME = PersistentConfig(
|
||||
'AZURE_AI_SEARCH_INDEX_NAME',
|
||||
'rag.web.search.azure_ai_search_index_name',
|
||||
os.environ.get('AZURE_AI_SEARCH_INDEX_NAME', ''),
|
||||
)
|
||||
|
||||
EXA_API_KEY = PersistentConfig(
|
||||
'EXA_API_KEY',
|
||||
'rag.web.search.exa_api_key',
|
||||
os.getenv('EXA_API_KEY', ''),
|
||||
)
|
||||
|
||||
PERPLEXITY_API_KEY = PersistentConfig(
|
||||
'PERPLEXITY_API_KEY',
|
||||
'rag.web.search.perplexity_api_key',
|
||||
os.getenv('PERPLEXITY_API_KEY', ''),
|
||||
)
|
||||
|
||||
PERPLEXITY_MODEL = PersistentConfig(
|
||||
'PERPLEXITY_MODEL',
|
||||
'rag.web.search.perplexity_model',
|
||||
os.getenv('PERPLEXITY_MODEL', 'sonar'),
|
||||
)
|
||||
|
||||
PERPLEXITY_SEARCH_CONTEXT_USAGE = PersistentConfig(
|
||||
'PERPLEXITY_SEARCH_CONTEXT_USAGE',
|
||||
'rag.web.search.perplexity_search_context_usage',
|
||||
os.getenv('PERPLEXITY_SEARCH_CONTEXT_USAGE', 'medium'),
|
||||
)
|
||||
|
||||
PERPLEXITY_SEARCH_API_URL = PersistentConfig(
|
||||
'PERPLEXITY_SEARCH_API_URL',
|
||||
'rag.web.search.perplexity_search_api_url',
|
||||
os.getenv('PERPLEXITY_SEARCH_API_URL', 'https://api.perplexity.ai/search'),
|
||||
)
|
||||
|
||||
SOUGOU_API_SID = PersistentConfig(
|
||||
'SOUGOU_API_SID',
|
||||
'rag.web.search.sougou_api_sid',
|
||||
os.getenv('SOUGOU_API_SID', ''),
|
||||
)
|
||||
|
||||
SOUGOU_API_SK = PersistentConfig(
|
||||
'SOUGOU_API_SK',
|
||||
'rag.web.search.sougou_api_sk',
|
||||
os.getenv('SOUGOU_API_SK', ''),
|
||||
)
|
||||
|
||||
TAVILY_API_KEY = PersistentConfig(
|
||||
'TAVILY_API_KEY',
|
||||
'rag.web.search.tavily_api_key',
|
||||
os.getenv('TAVILY_API_KEY', ''),
|
||||
)
|
||||
|
||||
TAVILY_EXTRACT_DEPTH = PersistentConfig(
|
||||
'TAVILY_EXTRACT_DEPTH',
|
||||
'rag.web.search.tavily_extract_depth',
|
||||
os.getenv('TAVILY_EXTRACT_DEPTH', 'basic'),
|
||||
)
|
||||
|
||||
PLAYWRIGHT_WS_URL = PersistentConfig(
|
||||
'PLAYWRIGHT_WS_URL',
|
||||
'rag.web.loader.playwright_ws_url',
|
||||
os.environ.get('PLAYWRIGHT_WS_URL', ''),
|
||||
)
|
||||
|
||||
PLAYWRIGHT_TIMEOUT = PersistentConfig(
|
||||
'PLAYWRIGHT_TIMEOUT',
|
||||
'rag.web.loader.playwright_timeout',
|
||||
int(os.environ.get('PLAYWRIGHT_TIMEOUT', '10000')),
|
||||
)
|
||||
|
||||
FIRECRAWL_API_KEY = PersistentConfig(
|
||||
'FIRECRAWL_API_KEY',
|
||||
'rag.web.loader.firecrawl_api_key',
|
||||
os.environ.get('FIRECRAWL_API_KEY', ''),
|
||||
)
|
||||
|
||||
FIRECRAWL_API_BASE_URL = PersistentConfig(
|
||||
'FIRECRAWL_API_BASE_URL',
|
||||
'rag.web.loader.firecrawl_api_url',
|
||||
os.environ.get('FIRECRAWL_API_BASE_URL', 'https://api.firecrawl.dev'),
|
||||
)
|
||||
|
||||
FIRECRAWL_TIMEOUT = PersistentConfig(
|
||||
'FIRECRAWL_TIMEOUT',
|
||||
'rag.web.loader.firecrawl_timeout',
|
||||
os.environ.get('FIRECRAWL_TIMEOUT', ''),
|
||||
)
|
||||
|
||||
EXTERNAL_WEB_SEARCH_URL = PersistentConfig(
|
||||
'EXTERNAL_WEB_SEARCH_URL',
|
||||
'rag.web.search.external_web_search_url',
|
||||
os.environ.get('EXTERNAL_WEB_SEARCH_URL', ''),
|
||||
)
|
||||
|
||||
EXTERNAL_WEB_SEARCH_API_KEY = PersistentConfig(
|
||||
'EXTERNAL_WEB_SEARCH_API_KEY',
|
||||
'rag.web.search.external_web_search_api_key',
|
||||
os.environ.get('EXTERNAL_WEB_SEARCH_API_KEY', ''),
|
||||
)
|
||||
|
||||
EXTERNAL_WEB_LOADER_URL = PersistentConfig(
|
||||
'EXTERNAL_WEB_LOADER_URL',
|
||||
'rag.web.loader.external_web_loader_url',
|
||||
os.environ.get('EXTERNAL_WEB_LOADER_URL', ''),
|
||||
)
|
||||
|
||||
EXTERNAL_WEB_LOADER_API_KEY = PersistentConfig(
|
||||
'EXTERNAL_WEB_LOADER_API_KEY',
|
||||
'rag.web.loader.external_web_loader_api_key',
|
||||
os.environ.get('EXTERNAL_WEB_LOADER_API_KEY', ''),
|
||||
)
|
||||
|
||||
YANDEX_WEB_SEARCH_URL = PersistentConfig(
|
||||
'YANDEX_WEB_SEARCH_URL',
|
||||
'rag.web.search.yandex_web_search_url',
|
||||
os.environ.get('YANDEX_WEB_SEARCH_URL', ''),
|
||||
)
|
||||
|
||||
YANDEX_WEB_SEARCH_API_KEY = PersistentConfig(
|
||||
'YANDEX_WEB_SEARCH_API_KEY',
|
||||
'rag.web.search.yandex_web_search_api_key',
|
||||
os.environ.get('YANDEX_WEB_SEARCH_API_KEY', ''),
|
||||
)
|
||||
|
||||
YANDEX_WEB_SEARCH_CONFIG = PersistentConfig(
|
||||
'YANDEX_WEB_SEARCH_CONFIG',
|
||||
'rag.web.search.yandex_web_search_config',
|
||||
os.environ.get('YANDEX_WEB_SEARCH_CONFIG', ''),
|
||||
)
|
||||
|
||||
YOUCOM_API_KEY = PersistentConfig(
|
||||
'YOUCOM_API_KEY',
|
||||
'rag.web.search.youcom_api_key',
|
||||
os.environ.get('YOUCOM_API_KEY', ''),
|
||||
)
|
||||
|
||||
####################################
|
||||
# Images
|
||||
####################################
|
||||
|
||||
ENABLE_IMAGE_GENERATION = os.getenv('ENABLE_IMAGE_GENERATION', '').lower() == 'true'
|
||||
ENABLE_IMAGE_GENERATION = PersistentConfig(
|
||||
'ENABLE_IMAGE_GENERATION',
|
||||
'image_generation.enable',
|
||||
os.environ.get('ENABLE_IMAGE_GENERATION', '').lower() == 'true',
|
||||
)
|
||||
|
||||
IMAGE_GENERATION_ENGINE = os.getenv('IMAGE_GENERATION_ENGINE', 'openai')
|
||||
IMAGE_GENERATION_ENGINE = PersistentConfig(
|
||||
'IMAGE_GENERATION_ENGINE',
|
||||
'image_generation.engine',
|
||||
os.getenv('IMAGE_GENERATION_ENGINE', 'openai'),
|
||||
)
|
||||
|
||||
IMAGE_GENERATION_MODEL = os.getenv('IMAGE_GENERATION_MODEL', '')
|
||||
IMAGE_GENERATION_MODEL = PersistentConfig(
|
||||
'IMAGE_GENERATION_MODEL',
|
||||
'image_generation.model',
|
||||
os.getenv('IMAGE_GENERATION_MODEL', ''),
|
||||
)
|
||||
|
||||
# Regex pattern for models that support IMAGE_SIZE = "auto".
|
||||
IMAGE_AUTO_SIZE_MODELS_REGEX_PATTERN = os.getenv('IMAGE_AUTO_SIZE_MODELS_REGEX_PATTERN', '^gpt-image')
|
||||
@@ -1311,14 +3584,26 @@ IMAGE_AUTO_SIZE_MODELS_REGEX_PATTERN = os.getenv('IMAGE_AUTO_SIZE_MODELS_REGEX_P
|
||||
# Regex pattern for models that return URLs instead of base64 data.
|
||||
IMAGE_URL_RESPONSE_MODELS_REGEX_PATTERN = os.getenv('IMAGE_URL_RESPONSE_MODELS_REGEX_PATTERN', '^gpt-image')
|
||||
|
||||
IMAGE_SIZE = os.getenv('IMAGE_SIZE', '512x512')
|
||||
IMAGE_SIZE = PersistentConfig('IMAGE_SIZE', 'image_generation.size', os.getenv('IMAGE_SIZE', '512x512'))
|
||||
|
||||
IMAGE_STEPS = int(os.getenv('IMAGE_STEPS', 50))
|
||||
IMAGE_STEPS = PersistentConfig('IMAGE_STEPS', 'image_generation.steps', int(os.getenv('IMAGE_STEPS', 50)))
|
||||
|
||||
ENABLE_IMAGE_PROMPT_GENERATION = os.getenv('ENABLE_IMAGE_PROMPT_GENERATION', 'true').lower() == 'true'
|
||||
ENABLE_IMAGE_PROMPT_GENERATION = PersistentConfig(
|
||||
'ENABLE_IMAGE_PROMPT_GENERATION',
|
||||
'image_generation.prompt.enable',
|
||||
os.environ.get('ENABLE_IMAGE_PROMPT_GENERATION', 'true').lower() == 'true',
|
||||
)
|
||||
|
||||
AUTOMATIC1111_BASE_URL = os.getenv('AUTOMATIC1111_BASE_URL', '')
|
||||
AUTOMATIC1111_API_AUTH = os.getenv('AUTOMATIC1111_API_AUTH', '')
|
||||
AUTOMATIC1111_BASE_URL = PersistentConfig(
|
||||
'AUTOMATIC1111_BASE_URL',
|
||||
'image_generation.automatic1111.base_url',
|
||||
os.getenv('AUTOMATIC1111_BASE_URL', ''),
|
||||
)
|
||||
AUTOMATIC1111_API_AUTH = PersistentConfig(
|
||||
'AUTOMATIC1111_API_AUTH',
|
||||
'image_generation.automatic1111.api_auth',
|
||||
os.getenv('AUTOMATIC1111_API_AUTH', ''),
|
||||
)
|
||||
|
||||
automatic1111_params = os.getenv('AUTOMATIC1111_PARAMS', '')
|
||||
try:
|
||||
@@ -1326,11 +3611,23 @@ try:
|
||||
except json.JSONDecodeError:
|
||||
automatic1111_params = {}
|
||||
|
||||
AUTOMATIC1111_PARAMS = automatic1111_params
|
||||
AUTOMATIC1111_PARAMS = PersistentConfig(
|
||||
'AUTOMATIC1111_PARAMS',
|
||||
'image_generation.automatic1111.api_params',
|
||||
automatic1111_params,
|
||||
)
|
||||
|
||||
COMFYUI_BASE_URL = os.getenv('COMFYUI_BASE_URL', '')
|
||||
COMFYUI_BASE_URL = PersistentConfig(
|
||||
'COMFYUI_BASE_URL',
|
||||
'image_generation.comfyui.base_url',
|
||||
os.getenv('COMFYUI_BASE_URL', ''),
|
||||
)
|
||||
|
||||
COMFYUI_API_KEY = os.getenv('COMFYUI_API_KEY', '')
|
||||
COMFYUI_API_KEY = PersistentConfig(
|
||||
'COMFYUI_API_KEY',
|
||||
'image_generation.comfyui.api_key',
|
||||
os.getenv('COMFYUI_API_KEY', ''),
|
||||
)
|
||||
|
||||
COMFYUI_DEFAULT_WORKFLOW = """
|
||||
{
|
||||
@@ -1443,7 +3740,11 @@ COMFYUI_DEFAULT_WORKFLOW = """
|
||||
"""
|
||||
|
||||
|
||||
COMFYUI_WORKFLOW = os.getenv('COMFYUI_WORKFLOW', COMFYUI_DEFAULT_WORKFLOW)
|
||||
COMFYUI_WORKFLOW = PersistentConfig(
|
||||
'COMFYUI_WORKFLOW',
|
||||
'image_generation.comfyui.workflow',
|
||||
os.getenv('COMFYUI_WORKFLOW', COMFYUI_DEFAULT_WORKFLOW),
|
||||
)
|
||||
|
||||
comfyui_workflow_nodes = os.getenv('COMFYUI_WORKFLOW_NODES', '')
|
||||
try:
|
||||
@@ -1451,12 +3752,28 @@ try:
|
||||
except json.JSONDecodeError:
|
||||
comfyui_workflow_nodes = []
|
||||
|
||||
COMFYUI_WORKFLOW_NODES = comfyui_workflow_nodes
|
||||
COMFYUI_WORKFLOW_NODES = PersistentConfig(
|
||||
'COMFYUI_WORKFLOW_NODES',
|
||||
'image_generation.comfyui.nodes',
|
||||
comfyui_workflow_nodes,
|
||||
)
|
||||
|
||||
IMAGES_OPENAI_API_BASE_URL = os.getenv('IMAGES_OPENAI_API_BASE_URL', OPENAI_API_BASE_URL)
|
||||
IMAGES_OPENAI_API_VERSION = os.getenv('IMAGES_OPENAI_API_VERSION', '')
|
||||
IMAGES_OPENAI_API_BASE_URL = PersistentConfig(
|
||||
'IMAGES_OPENAI_API_BASE_URL',
|
||||
'image_generation.openai.api_base_url',
|
||||
os.getenv('IMAGES_OPENAI_API_BASE_URL', OPENAI_API_BASE_URL),
|
||||
)
|
||||
IMAGES_OPENAI_API_VERSION = PersistentConfig(
|
||||
'IMAGES_OPENAI_API_VERSION',
|
||||
'image_generation.openai.api_version',
|
||||
os.getenv('IMAGES_OPENAI_API_VERSION', ''),
|
||||
)
|
||||
|
||||
IMAGES_OPENAI_API_KEY = os.getenv('IMAGES_OPENAI_API_KEY', OPENAI_API_KEY)
|
||||
IMAGES_OPENAI_API_KEY = PersistentConfig(
|
||||
'IMAGES_OPENAI_API_KEY',
|
||||
'image_generation.openai.api_key',
|
||||
os.getenv('IMAGES_OPENAI_API_KEY', OPENAI_API_KEY),
|
||||
)
|
||||
|
||||
images_openai_params = os.getenv('IMAGES_OPENAI_PARAMS', '')
|
||||
try:
|
||||
@@ -1465,37 +3782,93 @@ except json.JSONDecodeError:
|
||||
images_openai_params = {}
|
||||
|
||||
|
||||
IMAGES_OPENAI_API_PARAMS = images_openai_params
|
||||
IMAGES_OPENAI_API_PARAMS = PersistentConfig(
|
||||
'IMAGES_OPENAI_API_PARAMS', 'image_generation.openai.params', images_openai_params
|
||||
)
|
||||
|
||||
|
||||
IMAGES_GEMINI_API_BASE_URL = os.getenv('IMAGES_GEMINI_API_BASE_URL', GEMINI_API_BASE_URL)
|
||||
IMAGES_GEMINI_API_KEY = os.getenv('IMAGES_GEMINI_API_KEY', GEMINI_API_KEY)
|
||||
IMAGES_GEMINI_API_BASE_URL = PersistentConfig(
|
||||
'IMAGES_GEMINI_API_BASE_URL',
|
||||
'image_generation.gemini.api_base_url',
|
||||
os.getenv('IMAGES_GEMINI_API_BASE_URL', GEMINI_API_BASE_URL),
|
||||
)
|
||||
IMAGES_GEMINI_API_KEY = PersistentConfig(
|
||||
'IMAGES_GEMINI_API_KEY',
|
||||
'image_generation.gemini.api_key',
|
||||
os.getenv('IMAGES_GEMINI_API_KEY', GEMINI_API_KEY),
|
||||
)
|
||||
|
||||
IMAGES_GEMINI_ENDPOINT_METHOD = os.getenv('IMAGES_GEMINI_ENDPOINT_METHOD', '')
|
||||
IMAGES_GEMINI_ENDPOINT_METHOD = PersistentConfig(
|
||||
'IMAGES_GEMINI_ENDPOINT_METHOD',
|
||||
'image_generation.gemini.endpoint_method',
|
||||
os.getenv('IMAGES_GEMINI_ENDPOINT_METHOD', ''),
|
||||
)
|
||||
|
||||
ENABLE_IMAGE_EDIT = os.getenv('ENABLE_IMAGE_EDIT', '').lower() == 'true'
|
||||
ENABLE_IMAGE_EDIT = PersistentConfig(
|
||||
'ENABLE_IMAGE_EDIT',
|
||||
'images.edit.enable',
|
||||
os.environ.get('ENABLE_IMAGE_EDIT', '').lower() == 'true',
|
||||
)
|
||||
|
||||
IMAGE_EDIT_ENGINE = os.getenv('IMAGE_EDIT_ENGINE', 'openai')
|
||||
IMAGE_EDIT_ENGINE = PersistentConfig(
|
||||
'IMAGE_EDIT_ENGINE',
|
||||
'images.edit.engine',
|
||||
os.getenv('IMAGE_EDIT_ENGINE', 'openai'),
|
||||
)
|
||||
|
||||
IMAGE_EDIT_MODEL = os.getenv('IMAGE_EDIT_MODEL', '')
|
||||
IMAGE_EDIT_MODEL = PersistentConfig(
|
||||
'IMAGE_EDIT_MODEL',
|
||||
'images.edit.model',
|
||||
os.getenv('IMAGE_EDIT_MODEL', ''),
|
||||
)
|
||||
|
||||
IMAGE_EDIT_SIZE = os.getenv('IMAGE_EDIT_SIZE', '')
|
||||
IMAGE_EDIT_SIZE = PersistentConfig('IMAGE_EDIT_SIZE', 'images.edit.size', os.getenv('IMAGE_EDIT_SIZE', ''))
|
||||
|
||||
ENABLE_OPENAI_IMAGE_EDIT_NORMALIZATION = os.getenv('ENABLE_OPENAI_IMAGE_EDIT_NORMALIZATION', 'true').lower() == 'true'
|
||||
IMAGES_EDIT_OPENAI_API_BASE_URL = PersistentConfig(
|
||||
'IMAGES_EDIT_OPENAI_API_BASE_URL',
|
||||
'images.edit.openai.api_base_url',
|
||||
os.getenv('IMAGES_EDIT_OPENAI_API_BASE_URL', OPENAI_API_BASE_URL),
|
||||
)
|
||||
IMAGES_EDIT_OPENAI_API_VERSION = PersistentConfig(
|
||||
'IMAGES_EDIT_OPENAI_API_VERSION',
|
||||
'images.edit.openai.api_version',
|
||||
os.getenv('IMAGES_EDIT_OPENAI_API_VERSION', ''),
|
||||
)
|
||||
|
||||
IMAGES_EDIT_OPENAI_API_BASE_URL = os.getenv('IMAGES_EDIT_OPENAI_API_BASE_URL', OPENAI_API_BASE_URL)
|
||||
IMAGES_EDIT_OPENAI_API_VERSION = os.getenv('IMAGES_EDIT_OPENAI_API_VERSION', '')
|
||||
IMAGES_EDIT_OPENAI_API_KEY = PersistentConfig(
|
||||
'IMAGES_EDIT_OPENAI_API_KEY',
|
||||
'images.edit.openai.api_key',
|
||||
os.getenv('IMAGES_EDIT_OPENAI_API_KEY', OPENAI_API_KEY),
|
||||
)
|
||||
|
||||
IMAGES_EDIT_OPENAI_API_KEY = os.getenv('IMAGES_EDIT_OPENAI_API_KEY', OPENAI_API_KEY)
|
||||
|
||||
IMAGES_EDIT_GEMINI_API_BASE_URL = os.getenv('IMAGES_EDIT_GEMINI_API_BASE_URL', GEMINI_API_BASE_URL)
|
||||
IMAGES_EDIT_GEMINI_API_KEY = os.getenv('IMAGES_EDIT_GEMINI_API_KEY', GEMINI_API_KEY)
|
||||
IMAGES_EDIT_GEMINI_API_BASE_URL = PersistentConfig(
|
||||
'IMAGES_EDIT_GEMINI_API_BASE_URL',
|
||||
'images.edit.gemini.api_base_url',
|
||||
os.getenv('IMAGES_EDIT_GEMINI_API_BASE_URL', GEMINI_API_BASE_URL),
|
||||
)
|
||||
IMAGES_EDIT_GEMINI_API_KEY = PersistentConfig(
|
||||
'IMAGES_EDIT_GEMINI_API_KEY',
|
||||
'images.edit.gemini.api_key',
|
||||
os.getenv('IMAGES_EDIT_GEMINI_API_KEY', GEMINI_API_KEY),
|
||||
)
|
||||
|
||||
|
||||
IMAGES_EDIT_COMFYUI_BASE_URL = os.getenv('IMAGES_EDIT_COMFYUI_BASE_URL', '')
|
||||
IMAGES_EDIT_COMFYUI_API_KEY = os.getenv('IMAGES_EDIT_COMFYUI_API_KEY', '')
|
||||
IMAGES_EDIT_COMFYUI_BASE_URL = PersistentConfig(
|
||||
'IMAGES_EDIT_COMFYUI_BASE_URL',
|
||||
'images.edit.comfyui.base_url',
|
||||
os.getenv('IMAGES_EDIT_COMFYUI_BASE_URL', ''),
|
||||
)
|
||||
IMAGES_EDIT_COMFYUI_API_KEY = PersistentConfig(
|
||||
'IMAGES_EDIT_COMFYUI_API_KEY',
|
||||
'images.edit.comfyui.api_key',
|
||||
os.getenv('IMAGES_EDIT_COMFYUI_API_KEY', ''),
|
||||
)
|
||||
|
||||
IMAGES_EDIT_COMFYUI_WORKFLOW = os.getenv('IMAGES_EDIT_COMFYUI_WORKFLOW', '')
|
||||
IMAGES_EDIT_COMFYUI_WORKFLOW = PersistentConfig(
|
||||
'IMAGES_EDIT_COMFYUI_WORKFLOW',
|
||||
'images.edit.comfyui.workflow',
|
||||
os.getenv('IMAGES_EDIT_COMFYUI_WORKFLOW', ''),
|
||||
)
|
||||
|
||||
images_edit_comfyui_workflow_nodes = os.getenv('IMAGES_EDIT_COMFYUI_WORKFLOW_NODES', '')
|
||||
try:
|
||||
@@ -1503,18 +3876,26 @@ try:
|
||||
except json.JSONDecodeError:
|
||||
images_edit_comfyui_workflow_nodes = []
|
||||
|
||||
IMAGES_EDIT_COMFYUI_WORKFLOW_NODES = images_edit_comfyui_workflow_nodes
|
||||
IMAGES_EDIT_COMFYUI_WORKFLOW_NODES = PersistentConfig(
|
||||
'IMAGES_EDIT_COMFYUI_WORKFLOW_NODES',
|
||||
'images.edit.comfyui.nodes',
|
||||
images_edit_comfyui_workflow_nodes,
|
||||
)
|
||||
|
||||
####################################
|
||||
# Audio
|
||||
####################################
|
||||
|
||||
# Transcription
|
||||
WHISPER_MODEL = os.getenv('WHISPER_MODEL', 'base')
|
||||
WHISPER_MODEL = PersistentConfig(
|
||||
'WHISPER_MODEL',
|
||||
'audio.stt.whisper_model',
|
||||
os.getenv('WHISPER_MODEL', 'base'),
|
||||
)
|
||||
|
||||
WHISPER_COMPUTE_TYPE = os.getenv('WHISPER_COMPUTE_TYPE', 'int8')
|
||||
WHISPER_MODEL_DIR = os.getenv('WHISPER_MODEL_DIR', f'{CACHE_DIR}/whisper/models')
|
||||
WHISPER_MODEL_AUTO_UPDATE = not OFFLINE_MODE and os.getenv('WHISPER_MODEL_AUTO_UPDATE', '').lower() == 'true'
|
||||
WHISPER_MODEL_AUTO_UPDATE = not OFFLINE_MODE and os.environ.get('WHISPER_MODEL_AUTO_UPDATE', '').lower() == 'true'
|
||||
|
||||
WHISPER_VAD_FILTER = os.getenv('WHISPER_VAD_FILTER', 'False').lower() == 'true'
|
||||
|
||||
@@ -1523,54 +3904,107 @@ WHISPER_MULTILINGUAL = os.getenv('WHISPER_MULTILINGUAL', 'False').lower() == 'tr
|
||||
WHISPER_LANGUAGE = os.getenv('WHISPER_LANGUAGE', '').lower() or None
|
||||
|
||||
# Add Deepgram configuration
|
||||
DEEPGRAM_API_KEY = os.getenv('DEEPGRAM_API_KEY', '')
|
||||
DEEPGRAM_API_KEY = PersistentConfig(
|
||||
'DEEPGRAM_API_KEY',
|
||||
'audio.stt.deepgram.api_key',
|
||||
os.getenv('DEEPGRAM_API_KEY', ''),
|
||||
)
|
||||
|
||||
# ElevenLabs configuration
|
||||
ELEVENLABS_API_BASE_URL = os.getenv('ELEVENLABS_API_BASE_URL', 'https://api.elevenlabs.io')
|
||||
|
||||
AUDIO_STT_OPENAI_API_BASE_URL = os.getenv('AUDIO_STT_OPENAI_API_BASE_URL', OPENAI_API_BASE_URL)
|
||||
AUDIO_STT_OPENAI_API_BASE_URL = PersistentConfig(
|
||||
'AUDIO_STT_OPENAI_API_BASE_URL',
|
||||
'audio.stt.openai.api_base_url',
|
||||
os.getenv('AUDIO_STT_OPENAI_API_BASE_URL', OPENAI_API_BASE_URL),
|
||||
)
|
||||
|
||||
AUDIO_STT_OPENAI_API_KEY = os.getenv('AUDIO_STT_OPENAI_API_KEY', OPENAI_API_KEY)
|
||||
AUDIO_STT_OPENAI_API_KEY = PersistentConfig(
|
||||
'AUDIO_STT_OPENAI_API_KEY',
|
||||
'audio.stt.openai.api_key',
|
||||
os.getenv('AUDIO_STT_OPENAI_API_KEY', OPENAI_API_KEY),
|
||||
)
|
||||
|
||||
AUDIO_STT_OPENAI_API_REQUEST_FORMAT = os.getenv('AUDIO_STT_OPENAI_API_REQUEST_FORMAT', 'multipart')
|
||||
AUDIO_STT_ENGINE = PersistentConfig(
|
||||
'AUDIO_STT_ENGINE',
|
||||
'audio.stt.engine',
|
||||
os.getenv('AUDIO_STT_ENGINE', ''),
|
||||
)
|
||||
|
||||
AUDIO_STT_ENGINE = os.getenv('AUDIO_STT_ENGINE', '')
|
||||
AUDIO_STT_MODEL = PersistentConfig(
|
||||
'AUDIO_STT_MODEL',
|
||||
'audio.stt.model',
|
||||
os.getenv('AUDIO_STT_MODEL', ''),
|
||||
)
|
||||
|
||||
AUDIO_STT_MODEL = os.getenv('AUDIO_STT_MODEL', '')
|
||||
AUDIO_STT_SUPPORTED_CONTENT_TYPES = PersistentConfig(
|
||||
'AUDIO_STT_SUPPORTED_CONTENT_TYPES',
|
||||
'audio.stt.supported_content_types',
|
||||
[
|
||||
content_type.strip()
|
||||
for content_type in os.environ.get('AUDIO_STT_SUPPORTED_CONTENT_TYPES', '').split(',')
|
||||
if content_type.strip()
|
||||
],
|
||||
)
|
||||
|
||||
AUDIO_STT_SUPPORTED_CONTENT_TYPES = [
|
||||
content_type.strip()
|
||||
for content_type in os.getenv('AUDIO_STT_SUPPORTED_CONTENT_TYPES', '').split(',')
|
||||
if content_type.strip()
|
||||
]
|
||||
AUDIO_STT_AZURE_API_KEY = PersistentConfig(
|
||||
'AUDIO_STT_AZURE_API_KEY',
|
||||
'audio.stt.azure.api_key',
|
||||
os.getenv('AUDIO_STT_AZURE_API_KEY', ''),
|
||||
)
|
||||
|
||||
AUDIO_STT_ALLOWED_EXTENSIONS = [
|
||||
ext.strip()
|
||||
for ext in os.getenv(
|
||||
'AUDIO_STT_ALLOWED_EXTENSIONS',
|
||||
'mp3,wav,m4a,webm,ogg,flac,mp4,mpga,mpeg',
|
||||
).split(',')
|
||||
if ext.strip()
|
||||
]
|
||||
AUDIO_STT_AZURE_REGION = PersistentConfig(
|
||||
'AUDIO_STT_AZURE_REGION',
|
||||
'audio.stt.azure.region',
|
||||
os.getenv('AUDIO_STT_AZURE_REGION', ''),
|
||||
)
|
||||
|
||||
AUDIO_STT_AZURE_API_KEY = os.getenv('AUDIO_STT_AZURE_API_KEY', '')
|
||||
AUDIO_STT_AZURE_LOCALES = PersistentConfig(
|
||||
'AUDIO_STT_AZURE_LOCALES',
|
||||
'audio.stt.azure.locales',
|
||||
os.getenv('AUDIO_STT_AZURE_LOCALES', ''),
|
||||
)
|
||||
|
||||
AUDIO_STT_AZURE_REGION = os.getenv('AUDIO_STT_AZURE_REGION', '')
|
||||
AUDIO_STT_AZURE_BASE_URL = PersistentConfig(
|
||||
'AUDIO_STT_AZURE_BASE_URL',
|
||||
'audio.stt.azure.base_url',
|
||||
os.getenv('AUDIO_STT_AZURE_BASE_URL', ''),
|
||||
)
|
||||
|
||||
AUDIO_STT_AZURE_LOCALES = os.getenv('AUDIO_STT_AZURE_LOCALES', '')
|
||||
AUDIO_STT_AZURE_MAX_SPEAKERS = PersistentConfig(
|
||||
'AUDIO_STT_AZURE_MAX_SPEAKERS',
|
||||
'audio.stt.azure.max_speakers',
|
||||
os.getenv('AUDIO_STT_AZURE_MAX_SPEAKERS', ''),
|
||||
)
|
||||
|
||||
AUDIO_STT_AZURE_BASE_URL = os.getenv('AUDIO_STT_AZURE_BASE_URL', '')
|
||||
AUDIO_STT_MISTRAL_API_KEY = PersistentConfig(
|
||||
'AUDIO_STT_MISTRAL_API_KEY',
|
||||
'audio.stt.mistral.api_key',
|
||||
os.getenv('AUDIO_STT_MISTRAL_API_KEY', ''),
|
||||
)
|
||||
|
||||
AUDIO_STT_AZURE_MAX_SPEAKERS = os.getenv('AUDIO_STT_AZURE_MAX_SPEAKERS', '')
|
||||
AUDIO_STT_MISTRAL_API_BASE_URL = PersistentConfig(
|
||||
'AUDIO_STT_MISTRAL_API_BASE_URL',
|
||||
'audio.stt.mistral.api_base_url',
|
||||
os.getenv('AUDIO_STT_MISTRAL_API_BASE_URL', 'https://api.mistral.ai/v1'),
|
||||
)
|
||||
|
||||
AUDIO_STT_MISTRAL_API_KEY = os.getenv('AUDIO_STT_MISTRAL_API_KEY', '')
|
||||
AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS = PersistentConfig(
|
||||
'AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS',
|
||||
'audio.stt.mistral.use_chat_completions',
|
||||
os.getenv('AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS', 'false').lower() == 'true',
|
||||
)
|
||||
|
||||
AUDIO_STT_MISTRAL_API_BASE_URL = os.getenv('AUDIO_STT_MISTRAL_API_BASE_URL', 'https://api.mistral.ai/v1')
|
||||
|
||||
AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS = os.getenv('AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS', 'false').lower() == 'true'
|
||||
|
||||
AUDIO_TTS_OPENAI_API_BASE_URL = os.getenv('AUDIO_TTS_OPENAI_API_BASE_URL', OPENAI_API_BASE_URL)
|
||||
AUDIO_TTS_OPENAI_API_KEY = os.getenv('AUDIO_TTS_OPENAI_API_KEY', OPENAI_API_KEY)
|
||||
AUDIO_TTS_OPENAI_API_BASE_URL = PersistentConfig(
|
||||
'AUDIO_TTS_OPENAI_API_BASE_URL',
|
||||
'audio.tts.openai.api_base_url',
|
||||
os.getenv('AUDIO_TTS_OPENAI_API_BASE_URL', OPENAI_API_BASE_URL),
|
||||
)
|
||||
AUDIO_TTS_OPENAI_API_KEY = PersistentConfig(
|
||||
'AUDIO_TTS_OPENAI_API_KEY',
|
||||
'audio.tts.openai.api_key',
|
||||
os.getenv('AUDIO_TTS_OPENAI_API_KEY', OPENAI_API_KEY),
|
||||
)
|
||||
|
||||
audio_tts_openai_params = os.getenv('AUDIO_TTS_OPENAI_PARAMS', '')
|
||||
try:
|
||||
@@ -1578,1559 +4012,166 @@ try:
|
||||
except json.JSONDecodeError:
|
||||
audio_tts_openai_params = {}
|
||||
|
||||
AUDIO_TTS_OPENAI_PARAMS = audio_tts_openai_params
|
||||
|
||||
|
||||
AUDIO_TTS_API_KEY = os.getenv('AUDIO_TTS_API_KEY', '')
|
||||
|
||||
AUDIO_TTS_ENGINE = os.getenv('AUDIO_TTS_ENGINE', '')
|
||||
|
||||
|
||||
AUDIO_TTS_MODEL = os.getenv('AUDIO_TTS_MODEL', 'tts-1')
|
||||
|
||||
AUDIO_TTS_VOICE = os.getenv('AUDIO_TTS_VOICE', 'alloy')
|
||||
|
||||
AUDIO_TTS_SPLIT_ON = os.getenv('AUDIO_TTS_SPLIT_ON', 'punctuation')
|
||||
|
||||
AUDIO_TTS_AZURE_SPEECH_REGION = os.getenv('AUDIO_TTS_AZURE_SPEECH_REGION', '')
|
||||
|
||||
AUDIO_TTS_AZURE_SPEECH_BASE_URL = os.getenv('AUDIO_TTS_AZURE_SPEECH_BASE_URL', '')
|
||||
|
||||
AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT = os.getenv(
|
||||
'AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT', 'audio-24khz-160kbitrate-mono-mp3'
|
||||
)
|
||||
|
||||
AUDIO_TTS_MISTRAL_API_KEY = os.getenv('AUDIO_TTS_MISTRAL_API_KEY', '')
|
||||
|
||||
AUDIO_TTS_MISTRAL_API_BASE_URL = os.getenv('AUDIO_TTS_MISTRAL_API_BASE_URL', 'https://api.mistral.ai/v1')
|
||||
|
||||
####################################
|
||||
# WEBUI
|
||||
####################################
|
||||
|
||||
|
||||
WEBUI_URL = os.getenv('WEBUI_URL', '')
|
||||
|
||||
|
||||
ENABLE_SIGNUP = False if not WEBUI_AUTH else os.getenv('ENABLE_SIGNUP', 'True').lower() == 'true'
|
||||
|
||||
ENABLE_LOGIN_FORM = os.getenv('ENABLE_LOGIN_FORM', 'True').lower() == 'true'
|
||||
|
||||
ENABLE_PASSWORD_CHANGE_FORM = os.getenv('ENABLE_PASSWORD_CHANGE_FORM', 'True').lower() == 'true'
|
||||
|
||||
ENABLE_PASSWORD_AUTH = os.getenv('ENABLE_PASSWORD_AUTH', 'True').lower() == 'true'
|
||||
|
||||
DEFAULT_LOCALE = os.getenv('DEFAULT_LOCALE', '')
|
||||
|
||||
DEFAULT_MODELS = os.getenv('DEFAULT_MODELS', None)
|
||||
|
||||
DEFAULT_PINNED_MODELS = os.getenv('DEFAULT_PINNED_MODELS', None)
|
||||
|
||||
try:
|
||||
default_prompt_suggestions = json.loads(os.getenv('DEFAULT_PROMPT_SUGGESTIONS', '[]'))
|
||||
except Exception as e:
|
||||
log.exception(f'Error loading DEFAULT_PROMPT_SUGGESTIONS: {e}')
|
||||
default_prompt_suggestions = []
|
||||
if default_prompt_suggestions == []:
|
||||
default_prompt_suggestions = [
|
||||
{
|
||||
'title': ['Help me study', 'vocabulary for a college entrance exam'],
|
||||
'content': "Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option.",
|
||||
},
|
||||
{
|
||||
'title': ['Give me ideas', "for what to do with my kids' art"],
|
||||
'content': "What are 5 creative things I could do with my kids' art? I don't want to throw them away, but it's also so much clutter.",
|
||||
},
|
||||
{
|
||||
'title': ['Tell me a fun fact', 'about the Roman Empire'],
|
||||
'content': 'Tell me a random fun fact about the Roman Empire',
|
||||
},
|
||||
{
|
||||
'title': ['Show me a code snippet', "of a website's sticky header"],
|
||||
'content': "Show me a code snippet of a website's sticky header in CSS and JavaScript.",
|
||||
},
|
||||
{
|
||||
'title': [
|
||||
'Explain options trading',
|
||||
"if I'm familiar with buying and selling stocks",
|
||||
],
|
||||
'content': "Explain options trading in simple terms if I'm familiar with buying and selling stocks.",
|
||||
},
|
||||
{
|
||||
'title': ['Overcome procrastination', 'give me tips'],
|
||||
'content': 'Could you start by asking me about instances when I procrastinate the most and then give me some suggestions to overcome it?',
|
||||
},
|
||||
]
|
||||
|
||||
DEFAULT_PROMPT_SUGGESTIONS = default_prompt_suggestions
|
||||
|
||||
MODEL_ORDER_LIST = []
|
||||
|
||||
try:
|
||||
default_model_metadata = json.loads(os.getenv('DEFAULT_MODEL_METADATA', '{}'))
|
||||
except Exception as e:
|
||||
log.exception(f'Error loading DEFAULT_MODEL_METADATA: {e}')
|
||||
default_model_metadata = {}
|
||||
|
||||
DEFAULT_MODEL_METADATA = default_model_metadata
|
||||
|
||||
try:
|
||||
default_model_params = json.loads(os.getenv('DEFAULT_MODEL_PARAMS', '{}'))
|
||||
except Exception as e:
|
||||
log.exception(f'Error loading DEFAULT_MODEL_PARAMS: {e}')
|
||||
default_model_params = {}
|
||||
|
||||
DEFAULT_MODEL_PARAMS = default_model_params
|
||||
|
||||
DEFAULT_USER_ROLE = os.getenv('DEFAULT_USER_ROLE', 'pending')
|
||||
|
||||
DEFAULT_GROUP_ID = os.getenv('DEFAULT_GROUP_ID', '')
|
||||
|
||||
PENDING_USER_OVERLAY_TITLE = os.getenv('PENDING_USER_OVERLAY_TITLE', '')
|
||||
|
||||
PENDING_USER_OVERLAY_CONTENT = os.getenv('PENDING_USER_OVERLAY_CONTENT', '')
|
||||
|
||||
|
||||
RESPONSE_WATERMARK = os.getenv('RESPONSE_WATERMARK', '')
|
||||
|
||||
IFRAME_CSP = os.getenv('IFRAME_CSP', '')
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS = (
|
||||
os.getenv('USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ACCESS = (
|
||||
os.getenv('USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ACCESS', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_PROMPTS_ACCESS = (
|
||||
os.getenv('USER_PERMISSIONS_WORKSPACE_PROMPTS_ACCESS', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS = (
|
||||
os.getenv('USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_SKILLS_ACCESS = (
|
||||
os.getenv('USER_PERMISSIONS_WORKSPACE_SKILLS_ACCESS', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_MODELS_IMPORT = (
|
||||
os.getenv('USER_PERMISSIONS_WORKSPACE_MODELS_IMPORT', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_MODELS_EXPORT = (
|
||||
os.getenv('USER_PERMISSIONS_WORKSPACE_MODELS_EXPORT', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_PROMPTS_IMPORT = (
|
||||
os.getenv('USER_PERMISSIONS_WORKSPACE_PROMPTS_IMPORT', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_PROMPTS_EXPORT = (
|
||||
os.getenv('USER_PERMISSIONS_WORKSPACE_PROMPTS_EXPORT', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_TOOLS_IMPORT = (
|
||||
os.getenv('USER_PERMISSIONS_WORKSPACE_TOOLS_IMPORT', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_TOOLS_EXPORT = (
|
||||
os.getenv('USER_PERMISSIONS_WORKSPACE_TOOLS_EXPORT', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_SKILLS_IMPORT = (
|
||||
os.getenv('USER_PERMISSIONS_WORKSPACE_SKILLS_IMPORT', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_SKILLS_EXPORT = (
|
||||
os.getenv('USER_PERMISSIONS_WORKSPACE_SKILLS_EXPORT', 'False').lower() == 'true'
|
||||
AUDIO_TTS_OPENAI_PARAMS = PersistentConfig(
|
||||
'AUDIO_TTS_OPENAI_PARAMS',
|
||||
'audio.tts.openai.params',
|
||||
audio_tts_openai_params,
|
||||
)
|
||||
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_SHARING = (
|
||||
os.getenv('USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_SHARING', 'False').lower() == 'true'
|
||||
AUDIO_TTS_API_KEY = PersistentConfig(
|
||||
'AUDIO_TTS_API_KEY',
|
||||
'audio.tts.api_key',
|
||||
os.getenv('AUDIO_TTS_API_KEY', ''),
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_PUBLIC_SHARING = (
|
||||
os.getenv('USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_SHARING = (
|
||||
os.getenv('USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_SHARING', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_PUBLIC_SHARING = (
|
||||
os.getenv('USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_SHARING = (
|
||||
os.getenv('USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_SHARING', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_PUBLIC_SHARING = (
|
||||
os.getenv('USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true'
|
||||
AUDIO_TTS_ENGINE = PersistentConfig(
|
||||
'AUDIO_TTS_ENGINE',
|
||||
'audio.tts.engine',
|
||||
os.getenv('AUDIO_TTS_ENGINE', ''),
|
||||
)
|
||||
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_SHARING = (
|
||||
os.getenv('USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_SHARING', 'False').lower() == 'true'
|
||||
AUDIO_TTS_MODEL = PersistentConfig(
|
||||
'AUDIO_TTS_MODEL',
|
||||
'audio.tts.model',
|
||||
os.getenv('AUDIO_TTS_MODEL', 'tts-1'), # OpenAI default model
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_PUBLIC_SHARING = (
|
||||
os.getenv('USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true'
|
||||
AUDIO_TTS_VOICE = PersistentConfig(
|
||||
'AUDIO_TTS_VOICE',
|
||||
'audio.tts.voice',
|
||||
os.getenv('AUDIO_TTS_VOICE', 'alloy'), # OpenAI default voice
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_SHARING = (
|
||||
os.getenv('USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_SHARING', 'False').lower() == 'true'
|
||||
AUDIO_TTS_SPLIT_ON = PersistentConfig(
|
||||
'AUDIO_TTS_SPLIT_ON',
|
||||
'audio.tts.split_on',
|
||||
os.getenv('AUDIO_TTS_SPLIT_ON', 'punctuation'),
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_PUBLIC_SHARING = (
|
||||
os.getenv('USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true'
|
||||
AUDIO_TTS_AZURE_SPEECH_REGION = PersistentConfig(
|
||||
'AUDIO_TTS_AZURE_SPEECH_REGION',
|
||||
'audio.tts.azure.speech_region',
|
||||
os.getenv('AUDIO_TTS_AZURE_SPEECH_REGION', ''),
|
||||
)
|
||||
|
||||
|
||||
USER_PERMISSIONS_NOTES_ALLOW_SHARING = os.getenv('USER_PERMISSIONS_NOTES_ALLOW_SHARING', 'False').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING = (
|
||||
os.getenv('USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true'
|
||||
AUDIO_TTS_AZURE_SPEECH_BASE_URL = PersistentConfig(
|
||||
'AUDIO_TTS_AZURE_SPEECH_BASE_URL',
|
||||
'audio.tts.azure.speech_base_url',
|
||||
os.getenv('AUDIO_TTS_AZURE_SPEECH_BASE_URL', ''),
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_FOLDERS_ALLOW_SHARING = os.getenv('USER_PERMISSIONS_FOLDERS_ALLOW_SHARING', 'False').lower() == 'true'
|
||||
|
||||
|
||||
USER_PERMISSIONS_CALENDAR_ALLOW_PUBLIC_SHARING = (
|
||||
os.getenv('USER_PERMISSIONS_CALENDAR_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true'
|
||||
AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT = PersistentConfig(
|
||||
'AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT',
|
||||
'audio.tts.azure.speech_output_format',
|
||||
os.getenv('AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT', 'audio-24khz-160kbitrate-mono-mp3'),
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS = (
|
||||
os.getenv('USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS', 'True').lower() == 'true'
|
||||
AUDIO_TTS_MISTRAL_API_KEY = PersistentConfig(
|
||||
'AUDIO_TTS_MISTRAL_API_KEY',
|
||||
'audio.tts.mistral.api_key',
|
||||
os.getenv('AUDIO_TTS_MISTRAL_API_KEY', ''),
|
||||
)
|
||||
|
||||
|
||||
USER_PERMISSIONS_CHAT_CONTROLS = os.getenv('USER_PERMISSIONS_CHAT_CONTROLS', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_CHAT_VALVES = os.getenv('USER_PERMISSIONS_CHAT_VALVES', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_CHAT_SYSTEM_PROMPT = os.getenv('USER_PERMISSIONS_CHAT_SYSTEM_PROMPT', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_CHAT_PARAMS = os.getenv('USER_PERMISSIONS_CHAT_PARAMS', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_CHAT_FILE_UPLOAD = os.getenv('USER_PERMISSIONS_CHAT_FILE_UPLOAD', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_CHAT_WEB_UPLOAD = os.getenv('USER_PERMISSIONS_CHAT_WEB_UPLOAD', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_CHAT_DELETE = os.getenv('USER_PERMISSIONS_CHAT_DELETE', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_CHAT_DELETE_MESSAGE = os.getenv('USER_PERMISSIONS_CHAT_DELETE_MESSAGE', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_CHAT_CONTINUE_RESPONSE = os.getenv('USER_PERMISSIONS_CHAT_CONTINUE_RESPONSE', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_CHAT_REGENERATE_RESPONSE = (
|
||||
os.getenv('USER_PERMISSIONS_CHAT_REGENERATE_RESPONSE', 'True').lower() == 'true'
|
||||
AUDIO_TTS_MISTRAL_API_BASE_URL = PersistentConfig(
|
||||
'AUDIO_TTS_MISTRAL_API_BASE_URL',
|
||||
'audio.tts.mistral.api_base_url',
|
||||
os.getenv('AUDIO_TTS_MISTRAL_API_BASE_URL', 'https://api.mistral.ai/v1'),
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_CHAT_RATE_RESPONSE = os.getenv('USER_PERMISSIONS_CHAT_RATE_RESPONSE', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_CHAT_EDIT = os.getenv('USER_PERMISSIONS_CHAT_EDIT', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_CHAT_SHARE = os.getenv('USER_PERMISSIONS_CHAT_SHARE', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_CHAT_ALLOW_PUBLIC_SHARING = (
|
||||
os.getenv('USER_PERMISSIONS_CHAT_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_CHAT_EXPORT = os.getenv('USER_PERMISSIONS_CHAT_EXPORT', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_CHAT_IMPORT = os.getenv('USER_PERMISSIONS_CHAT_IMPORT', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_CHAT_STT = os.getenv('USER_PERMISSIONS_CHAT_STT', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_CHAT_TTS = os.getenv('USER_PERMISSIONS_CHAT_TTS', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_CHAT_CALL = os.getenv('USER_PERMISSIONS_CHAT_CALL', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_CHAT_MULTIPLE_MODELS = os.getenv('USER_PERMISSIONS_CHAT_MULTIPLE_MODELS', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_CHAT_TEMPORARY = os.getenv('USER_PERMISSIONS_CHAT_TEMPORARY', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_CHAT_TEMPORARY_ENFORCED = (
|
||||
os.getenv('USER_PERMISSIONS_CHAT_TEMPORARY_ENFORCED', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
|
||||
USER_PERMISSIONS_FEATURES_DIRECT_TOOL_SERVERS = (
|
||||
os.getenv('USER_PERMISSIONS_FEATURES_DIRECT_TOOL_SERVERS', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_FEATURES_WEB_SEARCH = os.getenv('USER_PERMISSIONS_FEATURES_WEB_SEARCH', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_FEATURES_IMAGE_GENERATION = (
|
||||
os.getenv('USER_PERMISSIONS_FEATURES_IMAGE_GENERATION', 'True').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_FEATURES_CODE_INTERPRETER = (
|
||||
os.getenv('USER_PERMISSIONS_FEATURES_CODE_INTERPRETER', 'True').lower() == 'true'
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_FEATURES_FOLDERS = os.getenv('USER_PERMISSIONS_FEATURES_FOLDERS', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_FEATURES_NOTES = os.getenv('USER_PERMISSIONS_FEATURES_NOTES', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_FEATURES_CHANNELS = os.getenv('USER_PERMISSIONS_FEATURES_CHANNELS', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_FEATURES_API_KEYS = os.getenv('USER_PERMISSIONS_FEATURES_API_KEYS', 'False').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_FEATURES_MEMORIES = os.getenv('USER_PERMISSIONS_FEATURES_MEMORIES', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_FEATURES_AUTOMATIONS = os.getenv('USER_PERMISSIONS_FEATURES_AUTOMATIONS', 'False').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_FEATURES_CALENDAR = os.getenv('USER_PERMISSIONS_FEATURES_CALENDAR', 'True').lower() == 'true'
|
||||
|
||||
USER_PERMISSIONS_FEATURES_USER_WEBHOOKS = (
|
||||
os.getenv('USER_PERMISSIONS_FEATURES_USER_WEBHOOKS', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
|
||||
USER_PERMISSIONS_SETTINGS_INTERFACE = os.getenv('USER_PERMISSIONS_SETTINGS_INTERFACE', 'True').lower() == 'true'
|
||||
|
||||
|
||||
DEFAULT_USER_PERMISSIONS = {
|
||||
'workspace': {
|
||||
'models': USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS,
|
||||
'knowledge': USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ACCESS,
|
||||
'prompts': USER_PERMISSIONS_WORKSPACE_PROMPTS_ACCESS,
|
||||
'tools': USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS,
|
||||
'skills': USER_PERMISSIONS_WORKSPACE_SKILLS_ACCESS,
|
||||
'models_import': USER_PERMISSIONS_WORKSPACE_MODELS_IMPORT,
|
||||
'models_export': USER_PERMISSIONS_WORKSPACE_MODELS_EXPORT,
|
||||
'prompts_import': USER_PERMISSIONS_WORKSPACE_PROMPTS_IMPORT,
|
||||
'prompts_export': USER_PERMISSIONS_WORKSPACE_PROMPTS_EXPORT,
|
||||
'tools_import': USER_PERMISSIONS_WORKSPACE_TOOLS_IMPORT,
|
||||
'tools_export': USER_PERMISSIONS_WORKSPACE_TOOLS_EXPORT,
|
||||
'skills_import': USER_PERMISSIONS_WORKSPACE_SKILLS_IMPORT,
|
||||
'skills_export': USER_PERMISSIONS_WORKSPACE_SKILLS_EXPORT,
|
||||
},
|
||||
'sharing': {
|
||||
'models': USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_SHARING,
|
||||
'public_models': USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_PUBLIC_SHARING,
|
||||
'knowledge': USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_SHARING,
|
||||
'public_knowledge': USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_PUBLIC_SHARING,
|
||||
'prompts': USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_SHARING,
|
||||
'public_prompts': USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_PUBLIC_SHARING,
|
||||
'tools': USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_SHARING,
|
||||
'public_tools': USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_PUBLIC_SHARING,
|
||||
'skills': USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_SHARING,
|
||||
'public_skills': USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_PUBLIC_SHARING,
|
||||
'notes': USER_PERMISSIONS_NOTES_ALLOW_SHARING,
|
||||
'public_notes': USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING,
|
||||
'folders': USER_PERMISSIONS_FOLDERS_ALLOW_SHARING,
|
||||
'public_chats': USER_PERMISSIONS_CHAT_ALLOW_PUBLIC_SHARING,
|
||||
'public_calendars': USER_PERMISSIONS_CALENDAR_ALLOW_PUBLIC_SHARING,
|
||||
},
|
||||
'access_grants': {
|
||||
'allow_users': USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS,
|
||||
},
|
||||
'chat': {
|
||||
'controls': USER_PERMISSIONS_CHAT_CONTROLS,
|
||||
'valves': USER_PERMISSIONS_CHAT_VALVES,
|
||||
'system_prompt': USER_PERMISSIONS_CHAT_SYSTEM_PROMPT,
|
||||
'params': USER_PERMISSIONS_CHAT_PARAMS,
|
||||
'file_upload': USER_PERMISSIONS_CHAT_FILE_UPLOAD,
|
||||
'web_upload': USER_PERMISSIONS_CHAT_WEB_UPLOAD,
|
||||
'delete': USER_PERMISSIONS_CHAT_DELETE,
|
||||
'delete_message': USER_PERMISSIONS_CHAT_DELETE_MESSAGE,
|
||||
'continue_response': USER_PERMISSIONS_CHAT_CONTINUE_RESPONSE,
|
||||
'regenerate_response': USER_PERMISSIONS_CHAT_REGENERATE_RESPONSE,
|
||||
'rate_response': USER_PERMISSIONS_CHAT_RATE_RESPONSE,
|
||||
'edit': USER_PERMISSIONS_CHAT_EDIT,
|
||||
'share': USER_PERMISSIONS_CHAT_SHARE,
|
||||
'export': USER_PERMISSIONS_CHAT_EXPORT,
|
||||
'import': USER_PERMISSIONS_CHAT_IMPORT,
|
||||
'stt': USER_PERMISSIONS_CHAT_STT,
|
||||
'tts': USER_PERMISSIONS_CHAT_TTS,
|
||||
'call': USER_PERMISSIONS_CHAT_CALL,
|
||||
'multiple_models': USER_PERMISSIONS_CHAT_MULTIPLE_MODELS,
|
||||
'temporary': USER_PERMISSIONS_CHAT_TEMPORARY,
|
||||
'temporary_enforced': USER_PERMISSIONS_CHAT_TEMPORARY_ENFORCED,
|
||||
},
|
||||
'features': {
|
||||
# General features
|
||||
'api_keys': USER_PERMISSIONS_FEATURES_API_KEYS,
|
||||
'notes': USER_PERMISSIONS_FEATURES_NOTES,
|
||||
'folders': USER_PERMISSIONS_FEATURES_FOLDERS,
|
||||
'channels': USER_PERMISSIONS_FEATURES_CHANNELS,
|
||||
'direct_tool_servers': USER_PERMISSIONS_FEATURES_DIRECT_TOOL_SERVERS,
|
||||
# Chat features
|
||||
'web_search': USER_PERMISSIONS_FEATURES_WEB_SEARCH,
|
||||
'image_generation': USER_PERMISSIONS_FEATURES_IMAGE_GENERATION,
|
||||
'code_interpreter': USER_PERMISSIONS_FEATURES_CODE_INTERPRETER,
|
||||
'memories': USER_PERMISSIONS_FEATURES_MEMORIES,
|
||||
'automations': USER_PERMISSIONS_FEATURES_AUTOMATIONS,
|
||||
'calendar': USER_PERMISSIONS_FEATURES_CALENDAR,
|
||||
'webhooks': USER_PERMISSIONS_FEATURES_USER_WEBHOOKS,
|
||||
},
|
||||
'settings': {
|
||||
'interface': USER_PERMISSIONS_SETTINGS_INTERFACE,
|
||||
},
|
||||
}
|
||||
|
||||
USER_PERMISSIONS = DEFAULT_USER_PERMISSIONS
|
||||
|
||||
ENABLE_FOLDERS = os.getenv('ENABLE_FOLDERS', 'True').lower() == 'true'
|
||||
|
||||
FOLDER_MAX_FILE_COUNT = os.getenv('FOLDER_MAX_FILE_COUNT', '')
|
||||
|
||||
ENABLE_CHANNELS = os.getenv('ENABLE_CHANNELS', 'False').lower() == 'true'
|
||||
|
||||
ENABLE_CALENDAR = os.getenv('ENABLE_CALENDAR', 'True').lower() == 'true'
|
||||
|
||||
ENABLE_AUTOMATIONS = os.getenv('ENABLE_AUTOMATIONS', 'True').lower() == 'true'
|
||||
|
||||
AUTOMATION_MAX_COUNT = os.getenv('AUTOMATION_MAX_COUNT', '')
|
||||
|
||||
AUTOMATION_MIN_INTERVAL = os.getenv('AUTOMATION_MIN_INTERVAL', '')
|
||||
|
||||
AUTOMATION_AUTH_TOKEN_EXPIRES_IN = os.getenv('AUTOMATION_AUTH_TOKEN_EXPIRES_IN', '1h')
|
||||
|
||||
ENABLE_NOTES = os.getenv('ENABLE_NOTES', 'True').lower() == 'true'
|
||||
|
||||
ENABLE_USER_STATUS = os.getenv('ENABLE_USER_STATUS', 'True').lower() == 'true'
|
||||
|
||||
ENABLE_EVALUATION_ARENA_MODELS = os.getenv('ENABLE_EVALUATION_ARENA_MODELS', 'True').lower() == 'true'
|
||||
try:
|
||||
evaluation_arena_models = json.loads(os.getenv('EVALUATION_ARENA_MODELS', '[]'))
|
||||
if not isinstance(evaluation_arena_models, list) or not all(
|
||||
isinstance(model, dict) for model in evaluation_arena_models
|
||||
):
|
||||
raise ValueError('EVALUATION_ARENA_MODELS must be a JSON list of objects')
|
||||
except Exception as e:
|
||||
log.exception(f'Error loading EVALUATION_ARENA_MODELS: {e}')
|
||||
evaluation_arena_models = []
|
||||
|
||||
EVALUATION_ARENA_MODELS = evaluation_arena_models
|
||||
|
||||
DEFAULT_ARENA_MODEL = {
|
||||
'id': 'arena-model',
|
||||
'name': 'Arena Model',
|
||||
'meta': {
|
||||
'profile_image_url': '/favicon.png',
|
||||
'description': 'Submit your questions to anonymous AI chatbots and vote on the best response.',
|
||||
'model_ids': None,
|
||||
},
|
||||
}
|
||||
|
||||
WEBHOOK_URL = os.getenv('WEBHOOK_URL', '')
|
||||
|
||||
ENABLE_ADMIN_EXPORT = os.getenv('ENABLE_ADMIN_EXPORT', 'True').lower() == 'true'
|
||||
|
||||
ENABLE_ADMIN_WORKSPACE_CONTENT_ACCESS = os.getenv('ENABLE_ADMIN_WORKSPACE_CONTENT_ACCESS', 'True').lower() == 'true'
|
||||
|
||||
BYPASS_ADMIN_ACCESS_CONTROL = (
|
||||
os.getenv(
|
||||
'BYPASS_ADMIN_ACCESS_CONTROL',
|
||||
os.getenv('ENABLE_ADMIN_WORKSPACE_CONTENT_ACCESS', 'True'),
|
||||
).lower()
|
||||
== 'true'
|
||||
)
|
||||
|
||||
ENABLE_ADMIN_CHAT_ACCESS = os.getenv('ENABLE_ADMIN_CHAT_ACCESS', 'True').lower() == 'true'
|
||||
|
||||
ENABLE_ADMIN_ANALYTICS = os.getenv('ENABLE_ADMIN_ANALYTICS', 'True').lower() == 'true'
|
||||
|
||||
ENABLE_COMMUNITY_SHARING = os.getenv('ENABLE_COMMUNITY_SHARING', 'True').lower() == 'true'
|
||||
|
||||
ENABLE_MESSAGE_RATING = os.getenv('ENABLE_MESSAGE_RATING', 'True').lower() == 'true'
|
||||
|
||||
ENABLE_USER_WEBHOOKS = os.getenv('ENABLE_USER_WEBHOOKS', 'False').lower() == 'true'
|
||||
|
||||
# FastAPI / AnyIO settings
|
||||
THREAD_POOL_SIZE = os.getenv('THREAD_POOL_SIZE', None)
|
||||
|
||||
if THREAD_POOL_SIZE is not None and isinstance(THREAD_POOL_SIZE, str):
|
||||
try:
|
||||
THREAD_POOL_SIZE = int(THREAD_POOL_SIZE)
|
||||
except ValueError:
|
||||
log.warning(f'THREAD_POOL_SIZE is not a valid integer: {THREAD_POOL_SIZE}. Defaulting to None.')
|
||||
THREAD_POOL_SIZE = None
|
||||
|
||||
|
||||
def validate_cors_origin(origin):
|
||||
parsed_url = urlparse(origin)
|
||||
|
||||
# Check if the scheme is either http or https, or a custom scheme
|
||||
schemes = ['http', 'https'] + CORS_ALLOW_CUSTOM_SCHEME
|
||||
if parsed_url.scheme not in schemes:
|
||||
raise ValueError(
|
||||
f"Invalid scheme in CORS_ALLOW_ORIGIN: '{origin}'. Only 'http' and 'https' and CORS_ALLOW_CUSTOM_SCHEME are allowed."
|
||||
)
|
||||
|
||||
# Ensure that the netloc (domain + port) is present, indicating it's a valid URL
|
||||
if not parsed_url.netloc:
|
||||
raise ValueError(f"Invalid URL structure in CORS_ALLOW_ORIGIN: '{origin}'.")
|
||||
|
||||
|
||||
# For production, you should only need one host as
|
||||
# fastapi serves the svelte-kit built frontend and backend from the same host and port.
|
||||
# To test CORS_ALLOW_ORIGIN locally, you can set something like
|
||||
# CORS_ALLOW_ORIGIN=http://localhost:5173;http://localhost:8080
|
||||
# in your .env file depending on your frontend port, 5173 in this case.
|
||||
CORS_ALLOW_ORIGIN = os.getenv('CORS_ALLOW_ORIGIN', '*').split(';')
|
||||
|
||||
# Allows custom URL schemes (e.g., app://) to be used as origins for CORS.
|
||||
# Useful for local development or desktop clients with schemes like app:// or other custom protocols.
|
||||
# Provide a semicolon-separated list of allowed schemes in the environment variable CORS_ALLOW_CUSTOM_SCHEMES.
|
||||
CORS_ALLOW_CUSTOM_SCHEME = os.getenv('CORS_ALLOW_CUSTOM_SCHEME', '').split(';')
|
||||
|
||||
if CORS_ALLOW_ORIGIN == ['*']:
|
||||
log.warning("\n\nWARNING: CORS_ALLOW_ORIGIN IS SET TO '*' - NOT RECOMMENDED FOR PRODUCTION DEPLOYMENTS.\n")
|
||||
else:
|
||||
# You have to pick between a single wildcard or a list of origins.
|
||||
# Doing both will result in CORS errors in the browser.
|
||||
for origin in CORS_ALLOW_ORIGIN:
|
||||
validate_cors_origin(origin)
|
||||
|
||||
|
||||
class BannerModel(BaseModel):
|
||||
id: str
|
||||
type: str
|
||||
title: str | None = None
|
||||
content: str
|
||||
dismissible: bool
|
||||
timestamp: int
|
||||
|
||||
|
||||
try:
|
||||
banners = json.loads(os.getenv('WEBUI_BANNERS', '[]'))
|
||||
banners = [BannerModel(**banner) for banner in banners]
|
||||
except Exception as e:
|
||||
log.exception(f'Error loading WEBUI_BANNERS: {e}')
|
||||
banners = []
|
||||
|
||||
WEBUI_BANNERS = banners
|
||||
|
||||
|
||||
SHOW_ADMIN_DETAILS = os.getenv('SHOW_ADMIN_DETAILS', 'true').lower() == 'true'
|
||||
|
||||
ADMIN_EMAIL = os.getenv('ADMIN_EMAIL', None)
|
||||
|
||||
|
||||
####################################
|
||||
# TASKS
|
||||
####################################
|
||||
|
||||
|
||||
TASK_MODEL = os.getenv('TASK_MODEL', '')
|
||||
|
||||
TASK_MODEL_EXTERNAL = os.getenv('TASK_MODEL_EXTERNAL', '')
|
||||
|
||||
ENABLE_CONTEXT_COMPACTION = os.getenv('ENABLE_CONTEXT_COMPACTION', 'False').lower() == 'true'
|
||||
|
||||
CONTEXT_COMPACTION_TOKEN_THRESHOLD = int(os.getenv('CONTEXT_COMPACTION_TOKEN_THRESHOLD', '80000'))
|
||||
|
||||
CONTEXT_COMPACTION_PROMPT_TEMPLATE = os.getenv('CONTEXT_COMPACTION_PROMPT_TEMPLATE', '')
|
||||
|
||||
TITLE_GENERATION_PROMPT_TEMPLATE = os.getenv('TITLE_GENERATION_PROMPT_TEMPLATE', '')
|
||||
|
||||
DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE = """### Task:
|
||||
Generate a concise, 3-5 word title with an emoji summarizing the chat history.
|
||||
### Guidelines:
|
||||
- The title should clearly represent the main theme or subject of the conversation.
|
||||
- Use emojis that enhance understanding of the topic, but avoid quotation marks or special formatting.
|
||||
- Write the title in the chat's primary language; default to English if multilingual.
|
||||
- Prioritize accuracy over excessive creativity; keep it clear and simple.
|
||||
- Your entire response must consist solely of the JSON object, without any introductory or concluding text.
|
||||
- The output must be a single, raw JSON object, without any markdown code fences or other encapsulating text.
|
||||
- Ensure no conversational text, affirmations, or explanations precede or follow the raw JSON output, as this will cause direct parsing failure.
|
||||
### Output:
|
||||
JSON format: { "title": "your concise title here" }
|
||||
### Examples:
|
||||
- { "title": "📉 Stock Market Trends" },
|
||||
- { "title": "🍪 Perfect Chocolate Chip Recipe" },
|
||||
- { "title": "Evolution of Music Streaming" },
|
||||
- { "title": "Remote Work Productivity Tips" },
|
||||
- { "title": "Artificial Intelligence in Healthcare" },
|
||||
- { "title": "🎮 Video Game Development Insights" }
|
||||
### Chat History:
|
||||
<chat_history>
|
||||
{{MESSAGES:END:2}}
|
||||
</chat_history>"""
|
||||
|
||||
TAGS_GENERATION_PROMPT_TEMPLATE = os.getenv('TAGS_GENERATION_PROMPT_TEMPLATE', '')
|
||||
|
||||
DEFAULT_TAGS_GENERATION_PROMPT_TEMPLATE = """### Task:
|
||||
Generate 1-3 broad tags categorizing the main themes of the chat history, along with 1-3 more specific subtopic tags.
|
||||
|
||||
### Guidelines:
|
||||
- Start with high-level domains (e.g. Science, Technology, Philosophy, Arts, Politics, Business, Health, Sports, Entertainment, Education)
|
||||
- Consider including relevant subfields/subdomains if they are strongly represented throughout the conversation
|
||||
- If content is too short (less than 3 messages) or too diverse, use only ["General"]
|
||||
- Use the chat's primary language; default to English if multilingual
|
||||
- Prioritize accuracy over specificity
|
||||
|
||||
### Output:
|
||||
JSON format: { "tags": ["tag1", "tag2", "tag3"] }
|
||||
|
||||
### Chat History:
|
||||
<chat_history>
|
||||
{{MESSAGES:END:6}}
|
||||
</chat_history>"""
|
||||
|
||||
IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE = os.getenv('IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE', '')
|
||||
|
||||
DEFAULT_IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE = """### Task:
|
||||
Generate a detailed prompt for am image generation task based on the given language and context. Describe the image as if you were explaining it to someone who cannot see it. Include relevant details, colors, shapes, and any other important elements.
|
||||
|
||||
### Guidelines:
|
||||
- Be descriptive and detailed, focusing on the most important aspects of the image.
|
||||
- Avoid making assumptions or adding information not present in the image.
|
||||
- Use the chat's primary language; default to English if multilingual.
|
||||
- If the image is too complex, focus on the most prominent elements.
|
||||
|
||||
### Output:
|
||||
Strictly return in JSON format:
|
||||
{
|
||||
"prompt": "Your detailed description here."
|
||||
}
|
||||
|
||||
### Chat History:
|
||||
<chat_history>
|
||||
{{MESSAGES:END:6}}
|
||||
</chat_history>"""
|
||||
|
||||
|
||||
FOLLOW_UP_GENERATION_PROMPT_TEMPLATE = os.getenv('FOLLOW_UP_GENERATION_PROMPT_TEMPLATE', '')
|
||||
|
||||
DEFAULT_FOLLOW_UP_GENERATION_PROMPT_TEMPLATE = """### Task:
|
||||
Suggest 3-5 relevant follow-up questions or prompts that the user might naturally ask next in this conversation as a **user**, based on the chat history, to help continue or deepen the discussion.
|
||||
### Guidelines:
|
||||
- Write all follow-up questions from the user’s point of view, directed to the assistant.
|
||||
- Make questions concise, clear, and directly related to the discussed topic(s).
|
||||
- Only suggest follow-ups that make sense given the chat content and do not repeat what was already covered.
|
||||
- If the conversation is very short or not specific, suggest more general (but relevant) follow-ups the user might ask.
|
||||
- Use the conversation's primary language; default to English if multilingual.
|
||||
- Response must be a JSON object with a "follow_ups" key containing an array of strings, no extra text or formatting.
|
||||
### Output:
|
||||
JSON format: { "follow_ups": ["Question 1?", "Question 2?", "Question 3?"] }
|
||||
### Chat History:
|
||||
<chat_history>
|
||||
{{MESSAGES:END:6}}
|
||||
</chat_history>"""
|
||||
|
||||
ENABLE_FOLLOW_UP_GENERATION = os.getenv('ENABLE_FOLLOW_UP_GENERATION', 'True').lower() == 'true'
|
||||
|
||||
ENABLE_TAGS_GENERATION = os.getenv('ENABLE_TAGS_GENERATION', 'True').lower() == 'true'
|
||||
|
||||
ENABLE_TITLE_GENERATION = os.getenv('ENABLE_TITLE_GENERATION', 'True').lower() == 'true'
|
||||
|
||||
|
||||
ENABLE_SEARCH_QUERY_GENERATION = os.getenv('ENABLE_SEARCH_QUERY_GENERATION', 'True').lower() == 'true'
|
||||
|
||||
ENABLE_RETRIEVAL_QUERY_GENERATION = os.getenv('ENABLE_RETRIEVAL_QUERY_GENERATION', 'True').lower() == 'true'
|
||||
|
||||
|
||||
QUERY_GENERATION_PROMPT_TEMPLATE = os.getenv('QUERY_GENERATION_PROMPT_TEMPLATE', '')
|
||||
|
||||
DEFAULT_QUERY_GENERATION_PROMPT_TEMPLATE = """### Task:
|
||||
Analyze the chat history to determine the necessity of generating search queries, in the given language. By default, **prioritize generating 1-3 broad and relevant search queries** unless it is absolutely certain that no additional information is required. The aim is to retrieve comprehensive, updated, and valuable information even with minimal uncertainty. If no search is unequivocally needed, return an empty list.
|
||||
|
||||
### Guidelines:
|
||||
- Respond **EXCLUSIVELY** with a JSON object. Any form of extra commentary, explanation, or additional text is strictly prohibited.
|
||||
- When generating search queries, respond in the format: { "queries": ["query1", "query2"] }, ensuring each query is distinct, concise, and relevant to the topic.
|
||||
- If and only if it is entirely certain that no useful results can be retrieved by a search, return: { "queries": [] }.
|
||||
- Err on the side of suggesting search queries if there is **any chance** they might provide useful or updated information.
|
||||
- Be concise and focused on composing high-quality search queries, avoiding unnecessary elaboration, commentary, or assumptions.
|
||||
- Today's date is: {{CURRENT_DATE}}.
|
||||
- Always prioritize providing actionable and broad queries that maximize informational coverage.
|
||||
|
||||
### Output:
|
||||
Strictly return in JSON format:
|
||||
{
|
||||
"queries": ["query1", "query2"]
|
||||
}
|
||||
|
||||
### Chat History:
|
||||
<chat_history>
|
||||
{{MESSAGES:END:6}}
|
||||
</chat_history>
|
||||
"""
|
||||
|
||||
ENABLE_AUTOCOMPLETE_GENERATION = os.getenv('ENABLE_AUTOCOMPLETE_GENERATION', 'False').lower() == 'true'
|
||||
|
||||
AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH = int(os.getenv('AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH', '-1'))
|
||||
|
||||
AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE = os.getenv('AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE', '')
|
||||
|
||||
|
||||
DEFAULT_AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE = """### Task:
|
||||
You are an autocompletion system. Continue the text in `<text>` based on the **completion type** in `<type>` and the given language.
|
||||
|
||||
### **Instructions**:
|
||||
1. Analyze `<text>` for context and meaning.
|
||||
2. Use `<type>` to guide your output:
|
||||
- **General**: Provide a natural, concise continuation.
|
||||
- **Search Query**: Complete as if generating a realistic search query.
|
||||
3. Start as if you are directly continuing `<text>`. Do **not** repeat, paraphrase, or respond as a model. Simply complete the text.
|
||||
4. Ensure the continuation:
|
||||
- Flows naturally from `<text>`.
|
||||
- Avoids repetition, overexplaining, or unrelated ideas.
|
||||
5. If unsure, return: `{ "text": "" }`.
|
||||
|
||||
### **Output Rules**:
|
||||
- Respond only in JSON format: `{ "text": "<your_completion>" }`.
|
||||
|
||||
### **Examples**:
|
||||
#### Example 1:
|
||||
Input:
|
||||
<type>General</type>
|
||||
<text>The sun was setting over the horizon, painting the sky</text>
|
||||
Output:
|
||||
{ "text": "with vibrant shades of orange and pink." }
|
||||
|
||||
#### Example 2:
|
||||
Input:
|
||||
<type>Search Query</type>
|
||||
<text>Top-rated restaurants in</text>
|
||||
Output:
|
||||
{ "text": "New York City for Italian cuisine." }
|
||||
|
||||
---
|
||||
### Context:
|
||||
<chat_history>
|
||||
{{MESSAGES:END:6}}
|
||||
</chat_history>
|
||||
<type>{{TYPE}}</type>
|
||||
<text>{{PROMPT}}</text>
|
||||
#### Output:
|
||||
"""
|
||||
|
||||
|
||||
VOICE_MODE_PROMPT_TEMPLATE = os.getenv('VOICE_MODE_PROMPT_TEMPLATE', '')
|
||||
|
||||
ENABLE_VOICE_MODE_PROMPT = os.getenv('ENABLE_VOICE_MODE_PROMPT', 'True').lower() == 'true'
|
||||
|
||||
DEFAULT_VOICE_MODE_PROMPT_TEMPLATE = """You are a friendly, concise voice assistant.
|
||||
|
||||
Everything you say will be spoken aloud.
|
||||
Keep responses short, clear, and natural.
|
||||
|
||||
STYLE:
|
||||
- Use simple words and short sentences.
|
||||
- Sound warm and conversational.
|
||||
- Avoid long explanations, lists, or complex phrasing.
|
||||
|
||||
BEHAVIOR:
|
||||
- Give the quickest helpful answer first.
|
||||
- Offer extra detail only if needed.
|
||||
- Ask for clarification only when necessary.
|
||||
|
||||
VOICE OPTIMIZATION:
|
||||
- Break information into small, easy-to-hear chunks.
|
||||
- Avoid dense wording or anything that sounds like reading text.
|
||||
|
||||
ERROR HANDLING:
|
||||
- If unsure, say so briefly and offer options.
|
||||
- If something is unsafe or impossible, decline kindly and suggest a safe alternative.
|
||||
|
||||
Stay consistent, helpful, and easy to listen to."""
|
||||
|
||||
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = os.getenv('TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE', '')
|
||||
|
||||
|
||||
DEFAULT_TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = """Available Tools: {{TOOLS}}
|
||||
|
||||
Your task is to choose and return the correct tool(s) from the list of available tools based on the query. Follow these guidelines:
|
||||
|
||||
- Return only the JSON object, without any additional text or explanation.
|
||||
|
||||
- If no tools match the query, return an empty array:
|
||||
{
|
||||
"tool_calls": []
|
||||
}
|
||||
|
||||
- If one or more tools match the query, construct a JSON response containing a "tool_calls" array with objects that include:
|
||||
- "name": The tool's name.
|
||||
- "parameters": A dictionary of required parameters and their corresponding values.
|
||||
|
||||
The format for the JSON response is strictly:
|
||||
{
|
||||
"tool_calls": [
|
||||
{"name": "toolName1", "parameters": {"key1": "value1"}},
|
||||
{"name": "toolName2", "parameters": {"key2": "value2"}}
|
||||
]
|
||||
}"""
|
||||
|
||||
|
||||
DEFAULT_EMOJI_GENERATION_PROMPT_TEMPLATE = """Your task is to reflect the speaker's likely facial expression through a fitting emoji. Interpret emotions from the message and reflect their facial expression using fitting, diverse emojis (e.g., 😊, 😢, 😡, 😱).
|
||||
|
||||
Message: ```{{prompt}}```"""
|
||||
|
||||
DEFAULT_MOA_GENERATION_PROMPT_TEMPLATE = """You have been provided with a set of responses from various models to the latest user query: "{{prompt}}"
|
||||
|
||||
Your task is to synthesize these responses into a single, high-quality response. It is crucial to critically evaluate the information provided in these responses, recognizing that some of it may be biased or incorrect. Your response should not simply replicate the given answers but should offer a refined, accurate, and comprehensive reply to the instruction. Ensure your response is well-structured, coherent, and adheres to the highest standards of accuracy and reliability.
|
||||
|
||||
Responses from models: {{responses}}"""
|
||||
|
||||
|
||||
####################################
|
||||
# Auth
|
||||
####################################
|
||||
|
||||
ENABLE_API_KEYS = os.getenv('ENABLE_API_KEYS', 'False').lower() == 'true'
|
||||
|
||||
ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS = (
|
||||
os.getenv(
|
||||
'ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS',
|
||||
os.getenv('ENABLE_API_KEY_ENDPOINT_RESTRICTIONS', 'False'),
|
||||
).lower()
|
||||
== 'true'
|
||||
)
|
||||
|
||||
API_KEYS_ALLOWED_ENDPOINTS = os.getenv('API_KEYS_ALLOWED_ENDPOINTS', os.getenv('API_KEY_ALLOWED_ENDPOINTS', ''))
|
||||
|
||||
JWT_EXPIRES_IN = os.getenv('JWT_EXPIRES_IN', '4w')
|
||||
|
||||
if JWT_EXPIRES_IN == '-1':
|
||||
log.warning(
|
||||
"⚠️ SECURITY WARNING: JWT_EXPIRES_IN is set to '-1'\n"
|
||||
' See: https://docs.openwebui.com/reference/env-configuration\n'
|
||||
)
|
||||
|
||||
####################################
|
||||
# OAuth config
|
||||
####################################
|
||||
|
||||
ENABLE_OAUTH_SIGNUP = os.getenv('ENABLE_OAUTH_SIGNUP', 'False').lower() == 'true'
|
||||
|
||||
OAUTH_AUTO_REDIRECT = os.getenv('OAUTH_AUTO_REDIRECT', 'False').lower() == 'true'
|
||||
|
||||
OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE = os.getenv('OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE', 'False').lower() == 'true'
|
||||
|
||||
|
||||
OAUTH_MERGE_ACCOUNTS_BY_EMAIL = os.getenv('OAUTH_MERGE_ACCOUNTS_BY_EMAIL', 'False').lower() == 'true'
|
||||
|
||||
OAUTH_PROVIDERS = {}
|
||||
|
||||
GOOGLE_CLIENT_ID = os.getenv('GOOGLE_CLIENT_ID', '')
|
||||
|
||||
GOOGLE_CLIENT_SECRET = os.getenv('GOOGLE_CLIENT_SECRET', '')
|
||||
|
||||
|
||||
GOOGLE_OAUTH_SCOPE = os.getenv('GOOGLE_OAUTH_SCOPE', 'openid email profile')
|
||||
|
||||
GOOGLE_REDIRECT_URI = os.getenv('GOOGLE_REDIRECT_URI', '')
|
||||
|
||||
GOOGLE_OAUTH_AUTHORIZE_PARAMS = {}
|
||||
_google_oauth_authorize_params = os.getenv('GOOGLE_OAUTH_AUTHORIZE_PARAMS', '')
|
||||
if _google_oauth_authorize_params:
|
||||
try:
|
||||
_parsed = json.loads(_google_oauth_authorize_params)
|
||||
if isinstance(_parsed, dict):
|
||||
GOOGLE_OAUTH_AUTHORIZE_PARAMS = _parsed
|
||||
else:
|
||||
log.warning('GOOGLE_OAUTH_AUTHORIZE_PARAMS must be a JSON object, ignoring')
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
log.warning('GOOGLE_OAUTH_AUTHORIZE_PARAMS is not valid JSON, ignoring')
|
||||
|
||||
MICROSOFT_CLIENT_ID = os.getenv('MICROSOFT_CLIENT_ID', '')
|
||||
|
||||
MICROSOFT_CLIENT_SECRET = os.getenv('MICROSOFT_CLIENT_SECRET', '')
|
||||
|
||||
MICROSOFT_CLIENT_TENANT_ID = os.getenv('MICROSOFT_CLIENT_TENANT_ID', '')
|
||||
|
||||
MICROSOFT_CLIENT_LOGIN_BASE_URL = os.getenv('MICROSOFT_CLIENT_LOGIN_BASE_URL', 'https://login.microsoftonline.com')
|
||||
|
||||
MICROSOFT_CLIENT_PICTURE_URL = os.getenv(
|
||||
'MICROSOFT_CLIENT_PICTURE_URL',
|
||||
'https://graph.microsoft.com/v1.0/me/photo/$value',
|
||||
)
|
||||
|
||||
|
||||
MICROSOFT_OAUTH_SCOPE = os.getenv('MICROSOFT_OAUTH_SCOPE', 'openid email profile')
|
||||
|
||||
MICROSOFT_REDIRECT_URI = os.getenv('MICROSOFT_REDIRECT_URI', '')
|
||||
|
||||
GITHUB_CLIENT_ID = os.getenv('GITHUB_CLIENT_ID', '')
|
||||
|
||||
GITHUB_CLIENT_SECRET = os.getenv('GITHUB_CLIENT_SECRET', '')
|
||||
|
||||
GITHUB_CLIENT_SCOPE = os.getenv('GITHUB_CLIENT_SCOPE', 'user:email')
|
||||
|
||||
GITHUB_CLIENT_REDIRECT_URI = os.getenv('GITHUB_CLIENT_REDIRECT_URI', '')
|
||||
|
||||
OAUTH_CLIENT_ID = os.getenv('OAUTH_CLIENT_ID', '')
|
||||
|
||||
OAUTH_CLIENT_SECRET = os.getenv('OAUTH_CLIENT_SECRET', '')
|
||||
|
||||
OPENID_PROVIDER_URL = os.getenv('OPENID_PROVIDER_URL', '')
|
||||
|
||||
OPENID_END_SESSION_ENDPOINT = os.getenv('OPENID_END_SESSION_ENDPOINT', '')
|
||||
|
||||
OPENID_REDIRECT_URI = os.getenv('OPENID_REDIRECT_URI', '')
|
||||
|
||||
OAUTH_SCOPES = os.getenv('OAUTH_SCOPES', 'openid email profile')
|
||||
|
||||
OAUTH_TIMEOUT = os.getenv('OAUTH_TIMEOUT', '')
|
||||
|
||||
OAUTH_TOKEN_ENDPOINT_AUTH_METHOD = os.getenv('OAUTH_TOKEN_ENDPOINT_AUTH_METHOD', None)
|
||||
|
||||
OAUTH_CODE_CHALLENGE_METHOD = os.getenv('OAUTH_CODE_CHALLENGE_METHOD', None)
|
||||
|
||||
OAUTH_PROVIDER_NAME = os.getenv('OAUTH_PROVIDER_NAME', 'SSO')
|
||||
|
||||
OAUTH_SUB_CLAIM = os.getenv('OAUTH_SUB_CLAIM', None)
|
||||
|
||||
OAUTH_USERNAME_CLAIM = os.getenv('OAUTH_USERNAME_CLAIM', 'name')
|
||||
|
||||
|
||||
OAUTH_PICTURE_CLAIM = os.getenv('OAUTH_PICTURE_CLAIM', 'picture')
|
||||
|
||||
OAUTH_EMAIL_CLAIM = os.getenv('OAUTH_EMAIL_CLAIM', 'email')
|
||||
|
||||
OAUTH_GROUPS_CLAIM = os.getenv('OAUTH_GROUPS_CLAIM', os.getenv('OAUTH_GROUP_CLAIM', 'groups'))
|
||||
|
||||
FEISHU_CLIENT_ID = os.getenv('FEISHU_CLIENT_ID', '')
|
||||
|
||||
FEISHU_CLIENT_SECRET = os.getenv('FEISHU_CLIENT_SECRET', '')
|
||||
|
||||
FEISHU_OAUTH_SCOPE = os.getenv('FEISHU_OAUTH_SCOPE', 'contact:user.base:readonly')
|
||||
|
||||
FEISHU_REDIRECT_URI = os.getenv('FEISHU_REDIRECT_URI', '')
|
||||
|
||||
ENABLE_OAUTH_ROLE_MANAGEMENT = os.getenv('ENABLE_OAUTH_ROLE_MANAGEMENT', 'False').lower() == 'true'
|
||||
|
||||
ENABLE_OAUTH_GROUP_MANAGEMENT = os.getenv('ENABLE_OAUTH_GROUP_MANAGEMENT', 'False').lower() == 'true'
|
||||
|
||||
ENABLE_OAUTH_GROUP_CREATION = os.getenv('ENABLE_OAUTH_GROUP_CREATION', 'False').lower() == 'true'
|
||||
|
||||
|
||||
oauth_group_default_share = os.getenv('OAUTH_GROUP_DEFAULT_SHARE', 'true').strip().lower()
|
||||
OAUTH_GROUP_DEFAULT_SHARE = 'members' if oauth_group_default_share == 'members' else oauth_group_default_share == 'true'
|
||||
|
||||
|
||||
OAUTH_BLOCKED_GROUPS = os.getenv('OAUTH_BLOCKED_GROUPS', '[]')
|
||||
|
||||
OAUTH_GROUPS_SEPARATOR = os.getenv('OAUTH_GROUPS_SEPARATOR', ';')
|
||||
|
||||
OAUTH_ROLES_CLAIM = os.getenv('OAUTH_ROLES_CLAIM', 'roles')
|
||||
|
||||
OAUTH_ROLES_SEPARATOR = os.getenv('OAUTH_ROLES_SEPARATOR', ',')
|
||||
|
||||
OAUTH_ALLOWED_ROLES = [
|
||||
role.strip()
|
||||
for role in os.getenv('OAUTH_ALLOWED_ROLES', f'user{OAUTH_ROLES_SEPARATOR}admin').split(OAUTH_ROLES_SEPARATOR)
|
||||
if role
|
||||
]
|
||||
|
||||
OAUTH_ADMIN_ROLES = [
|
||||
role.strip() for role in os.getenv('OAUTH_ADMIN_ROLES', 'admin').split(OAUTH_ROLES_SEPARATOR) if role
|
||||
]
|
||||
|
||||
OAUTH_ALLOWED_DOMAINS = [domain.strip() for domain in os.getenv('OAUTH_ALLOWED_DOMAINS', '*').split(',')]
|
||||
|
||||
OAUTH_UPDATE_PICTURE_ON_LOGIN = os.getenv('OAUTH_UPDATE_PICTURE_ON_LOGIN', 'False').lower() == 'true'
|
||||
|
||||
OAUTH_UPDATE_NAME_ON_LOGIN = os.getenv('OAUTH_UPDATE_NAME_ON_LOGIN', 'False').lower() == 'true'
|
||||
|
||||
OAUTH_UPDATE_EMAIL_ON_LOGIN = os.getenv('OAUTH_UPDATE_EMAIL_ON_LOGIN', 'False').lower() == 'true'
|
||||
|
||||
OAUTH_ACCESS_TOKEN_REQUEST_INCLUDE_CLIENT_ID = (
|
||||
os.getenv('OAUTH_ACCESS_TOKEN_REQUEST_INCLUDE_CLIENT_ID', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
OAUTH_AUDIENCE = os.getenv('OAUTH_AUDIENCE', '')
|
||||
|
||||
OAUTH_AUTHORIZE_PARAMS = {}
|
||||
_oauth_authorize_params = os.getenv('OAUTH_AUTHORIZE_PARAMS', '')
|
||||
if _oauth_authorize_params:
|
||||
try:
|
||||
_parsed = json.loads(_oauth_authorize_params)
|
||||
if isinstance(_parsed, dict):
|
||||
OAUTH_AUTHORIZE_PARAMS = _parsed
|
||||
else:
|
||||
log.warning('OAUTH_AUTHORIZE_PARAMS must be a JSON object, ignoring')
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
log.warning('OAUTH_AUTHORIZE_PARAMS is not valid JSON, ignoring')
|
||||
|
||||
|
||||
def load_oauth_providers():
|
||||
OAUTH_PROVIDERS.clear()
|
||||
if GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET:
|
||||
|
||||
def google_oauth_register(oauth: OAuth):
|
||||
client = oauth.register(
|
||||
name='google',
|
||||
client_id=GOOGLE_CLIENT_ID,
|
||||
client_secret=GOOGLE_CLIENT_SECRET,
|
||||
server_metadata_url='https://accounts.google.com/.well-known/openid-configuration',
|
||||
client_kwargs={
|
||||
'scope': GOOGLE_OAUTH_SCOPE,
|
||||
**({'timeout': int(OAUTH_TIMEOUT)} if OAUTH_TIMEOUT else {}),
|
||||
},
|
||||
redirect_uri=GOOGLE_REDIRECT_URI,
|
||||
**({'authorize_params': GOOGLE_OAUTH_AUTHORIZE_PARAMS} if GOOGLE_OAUTH_AUTHORIZE_PARAMS else {}),
|
||||
)
|
||||
return client
|
||||
|
||||
OAUTH_PROVIDERS['google'] = {
|
||||
'register': google_oauth_register,
|
||||
}
|
||||
|
||||
if MICROSOFT_CLIENT_ID and MICROSOFT_CLIENT_SECRET and MICROSOFT_CLIENT_TENANT_ID:
|
||||
|
||||
def microsoft_oauth_register(oauth: OAuth):
|
||||
client = oauth.register(
|
||||
name='microsoft',
|
||||
client_id=MICROSOFT_CLIENT_ID,
|
||||
client_secret=MICROSOFT_CLIENT_SECRET,
|
||||
server_metadata_url=f'{MICROSOFT_CLIENT_LOGIN_BASE_URL}/{MICROSOFT_CLIENT_TENANT_ID}/v2.0/.well-known/openid-configuration?appid={MICROSOFT_CLIENT_ID}',
|
||||
client_kwargs={
|
||||
'scope': MICROSOFT_OAUTH_SCOPE,
|
||||
**({'timeout': int(OAUTH_TIMEOUT)} if OAUTH_TIMEOUT else {}),
|
||||
},
|
||||
redirect_uri=MICROSOFT_REDIRECT_URI,
|
||||
)
|
||||
return client
|
||||
|
||||
OAUTH_PROVIDERS['microsoft'] = {
|
||||
'picture_url': MICROSOFT_CLIENT_PICTURE_URL,
|
||||
'register': microsoft_oauth_register,
|
||||
}
|
||||
|
||||
if GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET:
|
||||
|
||||
def github_oauth_register(oauth: OAuth):
|
||||
client = oauth.register(
|
||||
name='github',
|
||||
client_id=GITHUB_CLIENT_ID,
|
||||
client_secret=GITHUB_CLIENT_SECRET,
|
||||
access_token_url='https://github.com/login/oauth/access_token',
|
||||
authorize_url='https://github.com/login/oauth/authorize',
|
||||
api_base_url='https://api.github.com',
|
||||
userinfo_endpoint='https://api.github.com/user',
|
||||
client_kwargs={
|
||||
'scope': GITHUB_CLIENT_SCOPE,
|
||||
**({'timeout': int(OAUTH_TIMEOUT)} if OAUTH_TIMEOUT else {}),
|
||||
},
|
||||
redirect_uri=GITHUB_CLIENT_REDIRECT_URI,
|
||||
)
|
||||
return client
|
||||
|
||||
OAUTH_PROVIDERS['github'] = {
|
||||
'register': github_oauth_register,
|
||||
'sub_claim': 'id',
|
||||
}
|
||||
|
||||
if OAUTH_CLIENT_ID and (OAUTH_CLIENT_SECRET or OAUTH_CODE_CHALLENGE_METHOD) and OPENID_PROVIDER_URL:
|
||||
|
||||
def oidc_oauth_register(oauth: OAuth):
|
||||
client_kwargs = {
|
||||
'scope': OAUTH_SCOPES,
|
||||
**(
|
||||
{'token_endpoint_auth_method': OAUTH_TOKEN_ENDPOINT_AUTH_METHOD}
|
||||
if OAUTH_TOKEN_ENDPOINT_AUTH_METHOD
|
||||
else {}
|
||||
),
|
||||
**({'timeout': int(OAUTH_TIMEOUT)} if OAUTH_TIMEOUT else {}),
|
||||
}
|
||||
|
||||
if OAUTH_CODE_CHALLENGE_METHOD and OAUTH_CODE_CHALLENGE_METHOD == 'S256':
|
||||
client_kwargs['code_challenge_method'] = 'S256'
|
||||
elif OAUTH_CODE_CHALLENGE_METHOD:
|
||||
raise Exception(
|
||||
'Code challenge methods other than "%s" not supported. Given: "%s"'
|
||||
% ('S256', OAUTH_CODE_CHALLENGE_METHOD)
|
||||
)
|
||||
|
||||
client = oauth.register(
|
||||
name='oidc',
|
||||
client_id=OAUTH_CLIENT_ID,
|
||||
client_secret=OAUTH_CLIENT_SECRET,
|
||||
server_metadata_url=OPENID_PROVIDER_URL,
|
||||
client_kwargs=client_kwargs,
|
||||
redirect_uri=OPENID_REDIRECT_URI,
|
||||
)
|
||||
return client
|
||||
|
||||
OAUTH_PROVIDERS['oidc'] = {
|
||||
'name': OAUTH_PROVIDER_NAME,
|
||||
'register': oidc_oauth_register,
|
||||
}
|
||||
|
||||
if FEISHU_CLIENT_ID and FEISHU_CLIENT_SECRET:
|
||||
|
||||
def feishu_oauth_register(oauth: OAuth):
|
||||
client = oauth.register(
|
||||
name='feishu',
|
||||
client_id=FEISHU_CLIENT_ID,
|
||||
client_secret=FEISHU_CLIENT_SECRET,
|
||||
access_token_url='https://open.feishu.cn/open-apis/authen/v2/oauth/token',
|
||||
authorize_url='https://accounts.feishu.cn/open-apis/authen/v1/authorize',
|
||||
api_base_url='https://open.feishu.cn/open-apis',
|
||||
userinfo_endpoint='https://open.feishu.cn/open-apis/authen/v1/user_info',
|
||||
client_kwargs={
|
||||
'scope': FEISHU_OAUTH_SCOPE,
|
||||
**({'timeout': int(OAUTH_TIMEOUT)} if OAUTH_TIMEOUT else {}),
|
||||
},
|
||||
redirect_uri=FEISHU_REDIRECT_URI,
|
||||
)
|
||||
return client
|
||||
|
||||
OAUTH_PROVIDERS['feishu'] = {
|
||||
'register': feishu_oauth_register,
|
||||
'sub_claim': 'user_id',
|
||||
}
|
||||
|
||||
configured_providers = []
|
||||
if GOOGLE_CLIENT_ID:
|
||||
configured_providers.append('Google')
|
||||
if MICROSOFT_CLIENT_ID:
|
||||
configured_providers.append('Microsoft')
|
||||
if GITHUB_CLIENT_ID:
|
||||
configured_providers.append('GitHub')
|
||||
if FEISHU_CLIENT_ID:
|
||||
configured_providers.append('Feishu')
|
||||
|
||||
if configured_providers and not OPENID_PROVIDER_URL and not OPENID_END_SESSION_ENDPOINT:
|
||||
provider_list = ', '.join(configured_providers)
|
||||
log.warning(
|
||||
f'⚠️ OAuth providers configured ({provider_list}) but OPENID_PROVIDER_URL not set - logout will not work!'
|
||||
)
|
||||
log.warning(
|
||||
f"Set OPENID_PROVIDER_URL to your OAuth provider's OpenID Connect discovery endpoint,"
|
||||
f' or set OPENID_END_SESSION_ENDPOINT to a custom logout URL to fix logout functionality.'
|
||||
)
|
||||
|
||||
|
||||
load_oauth_providers()
|
||||
|
||||
####################################
|
||||
# LDAP
|
||||
####################################
|
||||
|
||||
ENABLE_LDAP = os.getenv('ENABLE_LDAP', 'false').lower() == 'true'
|
||||
|
||||
LDAP_SERVER_LABEL = os.getenv('LDAP_SERVER_LABEL', 'LDAP Server')
|
||||
|
||||
LDAP_SERVER_HOST = os.getenv('LDAP_SERVER_HOST', 'localhost')
|
||||
|
||||
LDAP_SERVER_PORT = int(os.getenv('LDAP_SERVER_PORT', '389'))
|
||||
|
||||
LDAP_ATTRIBUTE_FOR_MAIL = os.getenv('LDAP_ATTRIBUTE_FOR_MAIL', 'mail')
|
||||
|
||||
LDAP_ATTRIBUTE_FOR_USERNAME = os.getenv('LDAP_ATTRIBUTE_FOR_USERNAME', 'uid')
|
||||
|
||||
LDAP_APP_DN = os.getenv('LDAP_APP_DN', '')
|
||||
|
||||
LDAP_APP_PASSWORD = os.getenv('LDAP_APP_PASSWORD', '')
|
||||
|
||||
LDAP_SEARCH_BASE = os.getenv('LDAP_SEARCH_BASE', '')
|
||||
|
||||
LDAP_SEARCH_FILTERS = os.getenv('LDAP_SEARCH_FILTER', os.getenv('LDAP_SEARCH_FILTERS', ''))
|
||||
|
||||
LDAP_USE_TLS = os.getenv('LDAP_USE_TLS', 'True').lower() == 'true'
|
||||
|
||||
LDAP_CA_CERT_FILE = os.getenv('LDAP_CA_CERT_FILE', '')
|
||||
|
||||
LDAP_VALIDATE_CERT = os.getenv('LDAP_VALIDATE_CERT', 'True').lower() == 'true'
|
||||
|
||||
LDAP_CIPHERS = os.getenv('LDAP_CIPHERS', 'ALL')
|
||||
|
||||
ENABLE_LDAP_GROUP_MANAGEMENT = os.getenv('ENABLE_LDAP_GROUP_MANAGEMENT', 'False').lower() == 'true'
|
||||
|
||||
ENABLE_LDAP_GROUP_CREATION = os.getenv('ENABLE_LDAP_GROUP_CREATION', 'False').lower() == 'true'
|
||||
|
||||
LDAP_ATTRIBUTE_FOR_GROUPS = os.getenv('LDAP_ATTRIBUTE_FOR_GROUPS', 'memberOf')
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
'direct.enable': ENABLE_DIRECT_CONNECTIONS,
|
||||
'ollama.enable': ENABLE_OLLAMA_API,
|
||||
'ollama.base_urls': OLLAMA_BASE_URLS,
|
||||
'ollama.api_configs': OLLAMA_API_CONFIGS,
|
||||
'openai.enable': ENABLE_OPENAI_API,
|
||||
'openai.api_keys': OPENAI_API_KEYS,
|
||||
'openai.api_base_urls': OPENAI_API_BASE_URLS,
|
||||
'openai.api_configs': OPENAI_API_CONFIGS,
|
||||
'models.base_models_cache': ENABLE_BASE_MODELS_CACHE,
|
||||
'tool_server.connections': TOOL_SERVER_CONNECTIONS,
|
||||
'oauth.client.timeout': OAUTH_CLIENT_TIMEOUT,
|
||||
'terminal_server.connections': TERMINAL_SERVER_CONNECTIONS,
|
||||
'code_execution.enable': ENABLE_CODE_EXECUTION,
|
||||
'code_execution.engine': CODE_EXECUTION_ENGINE,
|
||||
'code_execution.jupyter.url': CODE_EXECUTION_JUPYTER_URL,
|
||||
'code_execution.jupyter.auth': CODE_EXECUTION_JUPYTER_AUTH,
|
||||
'code_execution.jupyter.auth_token': CODE_EXECUTION_JUPYTER_AUTH_TOKEN,
|
||||
'code_execution.jupyter.auth_password': CODE_EXECUTION_JUPYTER_AUTH_PASSWORD,
|
||||
'code_execution.jupyter.timeout': CODE_EXECUTION_JUPYTER_TIMEOUT,
|
||||
'code_interpreter.enable': ENABLE_CODE_INTERPRETER,
|
||||
'memories.enable': ENABLE_MEMORIES,
|
||||
'memories.system_context.enable': ENABLE_MEMORY_SYSTEM_CONTEXT,
|
||||
'memories.background_review.enable': ENABLE_MEMORY_BACKGROUND_REVIEW,
|
||||
'memories.review_interval_turns': MEMORIES_REVIEW_INTERVAL_TURNS,
|
||||
'memories.user_char_limit': MEMORIES_USER_CHAR_LIMIT,
|
||||
'memories.context_char_limit': MEMORIES_CONTEXT_CHAR_LIMIT,
|
||||
'code_interpreter.engine': CODE_INTERPRETER_ENGINE,
|
||||
'code_interpreter.prompt_template': CODE_INTERPRETER_PROMPT_TEMPLATE,
|
||||
'code_interpreter.jupyter.url': CODE_INTERPRETER_JUPYTER_URL,
|
||||
'code_interpreter.jupyter.auth': CODE_INTERPRETER_JUPYTER_AUTH,
|
||||
'code_interpreter.jupyter.auth_token': CODE_INTERPRETER_JUPYTER_AUTH_TOKEN,
|
||||
'code_interpreter.jupyter.auth_password': CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD,
|
||||
'code_interpreter.jupyter.timeout': CODE_INTERPRETER_JUPYTER_TIMEOUT,
|
||||
'google_drive.enable': ENABLE_GOOGLE_DRIVE_INTEGRATION,
|
||||
'google_drive.client_id': GOOGLE_DRIVE_CLIENT_ID,
|
||||
'google_drive.api_key': GOOGLE_DRIVE_API_KEY,
|
||||
'onedrive.enable': ENABLE_ONEDRIVE_INTEGRATION,
|
||||
'onedrive.sharepoint_url': ONEDRIVE_SHAREPOINT_URL,
|
||||
'onedrive.sharepoint_tenant_id': ONEDRIVE_SHAREPOINT_TENANT_ID,
|
||||
'rag.content_extraction_engine': CONTENT_EXTRACTION_ENGINE,
|
||||
'rag.datalab_marker_api_key': DATALAB_MARKER_API_KEY,
|
||||
'rag.datalab_marker_api_base_url': DATALAB_MARKER_API_BASE_URL,
|
||||
'rag.datalab_marker_additional_config': DATALAB_MARKER_ADDITIONAL_CONFIG,
|
||||
'rag.datalab_marker_use_llm': DATALAB_MARKER_USE_LLM,
|
||||
'rag.datalab_marker_skip_cache': DATALAB_MARKER_SKIP_CACHE,
|
||||
'rag.datalab_marker_force_ocr': DATALAB_MARKER_FORCE_OCR,
|
||||
'rag.datalab_marker_paginate': DATALAB_MARKER_PAGINATE,
|
||||
'rag.datalab_marker_strip_existing_ocr': DATALAB_MARKER_STRIP_EXISTING_OCR,
|
||||
'rag.datalab_marker_disable_image_extraction': DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION,
|
||||
'rag.datalab_marker_format_lines': DATALAB_MARKER_FORMAT_LINES,
|
||||
'rag.datalab_marker_output_format': DATALAB_MARKER_OUTPUT_FORMAT,
|
||||
'rag.mineru_api_mode': MINERU_API_MODE,
|
||||
'rag.mineru_api_url': MINERU_API_URL,
|
||||
'rag.mineru_api_timeout': MINERU_API_TIMEOUT,
|
||||
'rag.mineru_api_key': MINERU_API_KEY,
|
||||
'rag.mineru_params': MINERU_PARAMS,
|
||||
'rag.mineru_file_extensions': MINERU_FILE_EXTENSIONS,
|
||||
'rag.external_document_loader_url': EXTERNAL_DOCUMENT_LOADER_URL,
|
||||
'rag.external_document_loader_api_key': EXTERNAL_DOCUMENT_LOADER_API_KEY,
|
||||
'rag.external_document_loader_headers': EXTERNAL_DOCUMENT_LOADER_HEADERS,
|
||||
'rag.tika_server_url': TIKA_SERVER_URL,
|
||||
'rag.docling_server_url': DOCLING_SERVER_URL,
|
||||
'rag.docling_api_key': DOCLING_API_KEY,
|
||||
'rag.docling_params': DOCLING_PARAMS,
|
||||
'rag.document_intelligence_endpoint': DOCUMENT_INTELLIGENCE_ENDPOINT,
|
||||
'rag.document_intelligence_key': DOCUMENT_INTELLIGENCE_KEY,
|
||||
'rag.document_intelligence_model': DOCUMENT_INTELLIGENCE_MODEL,
|
||||
'rag.mistral_ocr_api_base_url': MISTRAL_OCR_API_BASE_URL,
|
||||
'rag.mistral_ocr_api_key': MISTRAL_OCR_API_KEY,
|
||||
'rag.mistral_ocr_use_base64': MISTRAL_OCR_USE_BASE64,
|
||||
'rag.paddleocr_vl_base_url': PADDLEOCR_VL_BASE_URL,
|
||||
'rag.paddleocr_vl_token': PADDLEOCR_VL_TOKEN,
|
||||
'rag.bypass_embedding_and_retrieval': BYPASS_EMBEDDING_AND_RETRIEVAL,
|
||||
'rag.top_k': RAG_TOP_K,
|
||||
'rag.top_k_reranker': RAG_TOP_K_RERANKER,
|
||||
'rag.relevance_threshold': RAG_RELEVANCE_THRESHOLD,
|
||||
'rag.hybrid_bm25_weight': RAG_HYBRID_BM25_WEIGHT,
|
||||
'rag.enable_hybrid_search': ENABLE_RAG_HYBRID_SEARCH,
|
||||
'rag.enable_hybrid_search_enriched_texts': ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS,
|
||||
'rag.full_context': RAG_FULL_CONTEXT,
|
||||
'rag.file.max_count': RAG_FILE_MAX_COUNT,
|
||||
'rag.file.max_size': RAG_FILE_MAX_SIZE,
|
||||
'file.image_compression_width': FILE_IMAGE_COMPRESSION_WIDTH,
|
||||
'file.image_compression_height': FILE_IMAGE_COMPRESSION_HEIGHT,
|
||||
'rag.file.allowed_extensions': RAG_ALLOWED_FILE_EXTENSIONS,
|
||||
'rag.embedding_engine': RAG_EMBEDDING_ENGINE,
|
||||
'rag.pdf_extract_images': PDF_EXTRACT_IMAGES,
|
||||
'rag.pdf_loader_mode': PDF_LOADER_MODE,
|
||||
'rag.embedding_model': RAG_EMBEDDING_MODEL,
|
||||
'rag.tokenizer_model': RAG_TOKENIZER_MODEL,
|
||||
'rag.embedding_batch_size': RAG_EMBEDDING_BATCH_SIZE,
|
||||
'rag.enable_async_embedding': ENABLE_ASYNC_EMBEDDING,
|
||||
'rag.embedding_concurrent_requests': RAG_EMBEDDING_CONCURRENT_REQUESTS,
|
||||
'rag.reranking_engine': RAG_RERANKING_ENGINE,
|
||||
'rag.reranking_model': RAG_RERANKING_MODEL,
|
||||
'rag.reranking_batch_size': RAG_RERANKING_BATCH_SIZE,
|
||||
'rag.external_reranker_url': RAG_EXTERNAL_RERANKER_URL,
|
||||
'rag.external_reranker_api_key': RAG_EXTERNAL_RERANKER_API_KEY,
|
||||
'rag.external_reranker_timeout': RAG_EXTERNAL_RERANKER_TIMEOUT,
|
||||
'rag.text_splitter': RAG_TEXT_SPLITTER,
|
||||
'rag.enable_markdown_header_text_splitter': ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER,
|
||||
'rag.tiktoken_encoding_name': TIKTOKEN_ENCODING_NAME,
|
||||
'rag.chunk_size': CHUNK_SIZE,
|
||||
'rag.chunk_min_size_target': CHUNK_MIN_SIZE_TARGET,
|
||||
'rag.chunk_overlap': CHUNK_OVERLAP,
|
||||
'rag.template': RAG_TEMPLATE,
|
||||
'rag.openai.api_base_url': RAG_OPENAI_API_BASE_URL,
|
||||
'rag.openai.api_key': RAG_OPENAI_API_KEY,
|
||||
'rag.azure_openai.base_url': RAG_AZURE_OPENAI_BASE_URL,
|
||||
'rag.azure_openai.api_key': RAG_AZURE_OPENAI_API_KEY,
|
||||
'rag.azure_openai.api_version': RAG_AZURE_OPENAI_API_VERSION,
|
||||
'rag.ollama.base_url': RAG_OLLAMA_BASE_URL,
|
||||
'rag.ollama.api_key': RAG_OLLAMA_API_KEY,
|
||||
'rag.youtube_loader_language': YOUTUBE_LOADER_LANGUAGE,
|
||||
'rag.youtube_loader_proxy_url': YOUTUBE_LOADER_PROXY_URL,
|
||||
'web.search.enable': ENABLE_WEB_SEARCH,
|
||||
'web.search.confirmation.enable': ENABLE_WEB_SEARCH_CONFIRMATION,
|
||||
'web.search.confirmation.content': WEB_SEARCH_CONFIRMATION_CONTENT,
|
||||
'web.search.engine': WEB_SEARCH_ENGINE,
|
||||
'web.search.bypass_embedding_and_retrieval': BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL,
|
||||
'web.search.bypass_web_loader': BYPASS_WEB_SEARCH_WEB_LOADER,
|
||||
'web.search.result_count': WEB_SEARCH_RESULT_COUNT,
|
||||
'web.search.domain.filter_list': WEB_SEARCH_DOMAIN_FILTER_LIST,
|
||||
'web.search.concurrent_requests': WEB_SEARCH_CONCURRENT_REQUESTS,
|
||||
'web.fetch.max_content_length': WEB_FETCH_MAX_CONTENT_LENGTH,
|
||||
'web.loader.engine': WEB_LOADER_ENGINE,
|
||||
'web.loader.concurrent_requests': WEB_LOADER_CONCURRENT_REQUESTS,
|
||||
'web.loader.timeout': WEB_LOADER_TIMEOUT,
|
||||
'web.loader.ssl_verification': ENABLE_WEB_LOADER_SSL_VERIFICATION,
|
||||
'web.search.trust_env': WEB_SEARCH_TRUST_ENV,
|
||||
'web.search.ollama_cloud_api_key': OLLAMA_CLOUD_WEB_SEARCH_API_KEY,
|
||||
'web.search.searxng_query_url': SEARXNG_QUERY_URL,
|
||||
'web.search.searxng_language': SEARXNG_LANGUAGE,
|
||||
'web.search.yacy_query_url': YACY_QUERY_URL,
|
||||
'web.search.yacy_username': YACY_USERNAME,
|
||||
'web.search.yacy_password': YACY_PASSWORD,
|
||||
'web.search.google_pse_api_key': GOOGLE_PSE_API_KEY,
|
||||
'web.search.google_pse_engine_id': GOOGLE_PSE_ENGINE_ID,
|
||||
'web.search.brave_search_api_key': BRAVE_SEARCH_API_KEY,
|
||||
'web.search.brave_search_context_tokens': BRAVE_SEARCH_CONTEXT_TOKENS,
|
||||
'web.search.kagi_search_api_key': KAGI_SEARCH_API_KEY,
|
||||
'web.search.mojeek_search_api_key': MOJEEK_SEARCH_API_KEY,
|
||||
'web.search.bocha_search_api_key': BOCHA_SEARCH_API_KEY,
|
||||
'web.search.serpstack_api_key': SERPSTACK_API_KEY,
|
||||
'web.search.serpstack_https': SERPSTACK_HTTPS,
|
||||
'web.search.serper_api_key': SERPER_API_KEY,
|
||||
'web.search.serply_api_key': SERPLY_API_KEY,
|
||||
'web.search.serphouse_api_key': SERPHOUSE_API_KEY,
|
||||
'web.search.serphouse_domain': SERPHOUSE_DOMAIN,
|
||||
'web.search.ddgs_backend': DDGS_BACKEND,
|
||||
'web.search.jina_api_key': JINA_API_KEY,
|
||||
'web.search.jina_api_base_url': JINA_API_BASE_URL,
|
||||
'web.search.searchapi_api_key': SEARCHAPI_API_KEY,
|
||||
'web.search.searchapi_engine': SEARCHAPI_ENGINE,
|
||||
'web.search.serpapi_api_key': SERPAPI_API_KEY,
|
||||
'web.search.serpapi_engine': SERPAPI_ENGINE,
|
||||
'web.search.bing_search_v7_endpoint': BING_SEARCH_V7_ENDPOINT,
|
||||
'web.search.bing_search_v7_subscription_key': BING_SEARCH_V7_SUBSCRIPTION_KEY,
|
||||
'web.search.azure_ai_search_api_key': AZURE_AI_SEARCH_API_KEY,
|
||||
'web.search.azure_ai_search_endpoint': AZURE_AI_SEARCH_ENDPOINT,
|
||||
'web.search.azure_ai_search_index_name': AZURE_AI_SEARCH_INDEX_NAME,
|
||||
'web.search.exa_api_key': EXA_API_KEY,
|
||||
'web.search.perplexity_api_key': PERPLEXITY_API_KEY,
|
||||
'web.search.perplexity_model': PERPLEXITY_MODEL,
|
||||
'web.search.perplexity_search_context_usage': PERPLEXITY_SEARCH_CONTEXT_USAGE,
|
||||
'web.search.perplexity_search_api_url': PERPLEXITY_SEARCH_API_URL,
|
||||
'web.search.microsoft_web_iq_api_base_url': MICROSOFT_WEB_IQ_API_BASE_URL,
|
||||
'web.search.microsoft_web_iq_api_key': MICROSOFT_WEB_IQ_API_KEY,
|
||||
'web.search.microsoft_web_iq_language': MICROSOFT_WEB_IQ_LANGUAGE,
|
||||
'web.search.sougou_api_sid': SOUGOU_API_SID,
|
||||
'web.search.sougou_api_sk': SOUGOU_API_SK,
|
||||
'web.search.tavily_api_key': TAVILY_API_KEY,
|
||||
'web.search.tavily_extract_depth': TAVILY_EXTRACT_DEPTH,
|
||||
'web.loader.playwright_ws_url': PLAYWRIGHT_WS_URL,
|
||||
'web.loader.playwright_timeout': PLAYWRIGHT_TIMEOUT,
|
||||
'web.loader.firecrawl_api_key': FIRECRAWL_API_KEY,
|
||||
'web.loader.firecrawl_api_url': FIRECRAWL_API_BASE_URL,
|
||||
'web.loader.firecrawl_timeout': FIRECRAWL_TIMEOUT,
|
||||
'web.search.external_web_search_url': EXTERNAL_WEB_SEARCH_URL,
|
||||
'web.search.external_web_search_api_key': EXTERNAL_WEB_SEARCH_API_KEY,
|
||||
'web.loader.external_web_loader_url': EXTERNAL_WEB_LOADER_URL,
|
||||
'web.loader.external_web_loader_api_key': EXTERNAL_WEB_LOADER_API_KEY,
|
||||
'web.search.yandex_web_search_url': YANDEX_WEB_SEARCH_URL,
|
||||
'web.search.yandex_web_search_api_key': YANDEX_WEB_SEARCH_API_KEY,
|
||||
'web.search.yandex_web_search_config': YANDEX_WEB_SEARCH_CONFIG,
|
||||
'web.search.youcom_api_key': YOUCOM_API_KEY,
|
||||
'web.search.linkup_api_key': LINKUP_API_KEY,
|
||||
'web.search.linkup_search_params': LINKUP_SEARCH_PARAMS,
|
||||
'image_generation.enable': ENABLE_IMAGE_GENERATION,
|
||||
'image_generation.engine': IMAGE_GENERATION_ENGINE,
|
||||
'image_generation.model': IMAGE_GENERATION_MODEL,
|
||||
'image_generation.size': IMAGE_SIZE,
|
||||
'image_generation.steps': IMAGE_STEPS,
|
||||
'image_generation.prompt.enable': ENABLE_IMAGE_PROMPT_GENERATION,
|
||||
'image_generation.automatic1111.base_url': AUTOMATIC1111_BASE_URL,
|
||||
'image_generation.automatic1111.api_auth': AUTOMATIC1111_API_AUTH,
|
||||
'image_generation.automatic1111.api_params': AUTOMATIC1111_PARAMS,
|
||||
'image_generation.comfyui.base_url': COMFYUI_BASE_URL,
|
||||
'image_generation.comfyui.api_key': COMFYUI_API_KEY,
|
||||
'image_generation.comfyui.workflow': COMFYUI_WORKFLOW,
|
||||
'image_generation.comfyui.nodes': COMFYUI_WORKFLOW_NODES,
|
||||
'image_generation.openai.api_base_url': IMAGES_OPENAI_API_BASE_URL,
|
||||
'image_generation.openai.api_version': IMAGES_OPENAI_API_VERSION,
|
||||
'image_generation.openai.api_key': IMAGES_OPENAI_API_KEY,
|
||||
'image_generation.openai.params': IMAGES_OPENAI_API_PARAMS,
|
||||
'image_generation.gemini.api_base_url': IMAGES_GEMINI_API_BASE_URL,
|
||||
'image_generation.gemini.api_key': IMAGES_GEMINI_API_KEY,
|
||||
'image_generation.gemini.endpoint_method': IMAGES_GEMINI_ENDPOINT_METHOD,
|
||||
'images.edit.enable': ENABLE_IMAGE_EDIT,
|
||||
'images.edit.engine': IMAGE_EDIT_ENGINE,
|
||||
'images.edit.model': IMAGE_EDIT_MODEL,
|
||||
'images.edit.size': IMAGE_EDIT_SIZE,
|
||||
'images.edit.openai.api_base_url': IMAGES_EDIT_OPENAI_API_BASE_URL,
|
||||
'images.edit.openai.api_version': IMAGES_EDIT_OPENAI_API_VERSION,
|
||||
'images.edit.openai.api_key': IMAGES_EDIT_OPENAI_API_KEY,
|
||||
'images.edit.gemini.api_base_url': IMAGES_EDIT_GEMINI_API_BASE_URL,
|
||||
'images.edit.gemini.api_key': IMAGES_EDIT_GEMINI_API_KEY,
|
||||
'images.edit.comfyui.base_url': IMAGES_EDIT_COMFYUI_BASE_URL,
|
||||
'images.edit.comfyui.api_key': IMAGES_EDIT_COMFYUI_API_KEY,
|
||||
'images.edit.comfyui.workflow': IMAGES_EDIT_COMFYUI_WORKFLOW,
|
||||
'images.edit.comfyui.nodes': IMAGES_EDIT_COMFYUI_WORKFLOW_NODES,
|
||||
'audio.stt.whisper_model': WHISPER_MODEL,
|
||||
'audio.stt.deepgram.api_key': DEEPGRAM_API_KEY,
|
||||
'audio.stt.openai.api_base_url': AUDIO_STT_OPENAI_API_BASE_URL,
|
||||
'audio.stt.openai.api_key': AUDIO_STT_OPENAI_API_KEY,
|
||||
'audio.stt.openai.api_request_format': AUDIO_STT_OPENAI_API_REQUEST_FORMAT,
|
||||
'audio.stt.engine': AUDIO_STT_ENGINE,
|
||||
'audio.stt.model': AUDIO_STT_MODEL,
|
||||
'audio.stt.supported_content_types': AUDIO_STT_SUPPORTED_CONTENT_TYPES,
|
||||
'audio.stt.allowed_extensions': AUDIO_STT_ALLOWED_EXTENSIONS,
|
||||
'audio.stt.azure.api_key': AUDIO_STT_AZURE_API_KEY,
|
||||
'audio.stt.azure.region': AUDIO_STT_AZURE_REGION,
|
||||
'audio.stt.azure.locales': AUDIO_STT_AZURE_LOCALES,
|
||||
'audio.stt.azure.base_url': AUDIO_STT_AZURE_BASE_URL,
|
||||
'audio.stt.azure.max_speakers': AUDIO_STT_AZURE_MAX_SPEAKERS,
|
||||
'audio.stt.mistral.api_key': AUDIO_STT_MISTRAL_API_KEY,
|
||||
'audio.stt.mistral.api_base_url': AUDIO_STT_MISTRAL_API_BASE_URL,
|
||||
'audio.stt.mistral.use_chat_completions': AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS,
|
||||
'audio.tts.openai.api_base_url': AUDIO_TTS_OPENAI_API_BASE_URL,
|
||||
'audio.tts.openai.api_key': AUDIO_TTS_OPENAI_API_KEY,
|
||||
'audio.tts.openai.params': AUDIO_TTS_OPENAI_PARAMS,
|
||||
'audio.tts.api_key': AUDIO_TTS_API_KEY,
|
||||
'audio.tts.engine': AUDIO_TTS_ENGINE,
|
||||
'audio.tts.model': AUDIO_TTS_MODEL,
|
||||
'audio.tts.voice': AUDIO_TTS_VOICE,
|
||||
'audio.tts.split_on': AUDIO_TTS_SPLIT_ON,
|
||||
'audio.tts.azure.speech_region': AUDIO_TTS_AZURE_SPEECH_REGION,
|
||||
'audio.tts.azure.speech_base_url': AUDIO_TTS_AZURE_SPEECH_BASE_URL,
|
||||
'audio.tts.azure.speech_output_format': AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT,
|
||||
'audio.tts.mistral.api_key': AUDIO_TTS_MISTRAL_API_KEY,
|
||||
'audio.tts.mistral.api_base_url': AUDIO_TTS_MISTRAL_API_BASE_URL,
|
||||
'webui.url': WEBUI_URL,
|
||||
'ui.enable_signup': ENABLE_SIGNUP,
|
||||
'ui.enable_login_form': ENABLE_LOGIN_FORM,
|
||||
'ui.enable_password_change_form': ENABLE_PASSWORD_CHANGE_FORM,
|
||||
'ui.default_locale': DEFAULT_LOCALE,
|
||||
'ui.default_models': DEFAULT_MODELS,
|
||||
'ui.default_pinned_models': DEFAULT_PINNED_MODELS,
|
||||
'ui.prompt_suggestions': DEFAULT_PROMPT_SUGGESTIONS,
|
||||
'ui.model_order_list': MODEL_ORDER_LIST,
|
||||
'models.default_metadata': DEFAULT_MODEL_METADATA,
|
||||
'models.default_params': DEFAULT_MODEL_PARAMS,
|
||||
'ui.default_user_role': DEFAULT_USER_ROLE,
|
||||
'ui.default_group_id': DEFAULT_GROUP_ID,
|
||||
'ui.pending_user_overlay_title': PENDING_USER_OVERLAY_TITLE,
|
||||
'ui.pending_user_overlay_content': PENDING_USER_OVERLAY_CONTENT,
|
||||
'ui.watermark': RESPONSE_WATERMARK,
|
||||
'user.permissions': USER_PERMISSIONS,
|
||||
'folders.enable': ENABLE_FOLDERS,
|
||||
'folders.max_file_count': FOLDER_MAX_FILE_COUNT,
|
||||
'channels.enable': ENABLE_CHANNELS,
|
||||
'calendar.enable': ENABLE_CALENDAR,
|
||||
'automations.enable': ENABLE_AUTOMATIONS,
|
||||
'automations.max_count': AUTOMATION_MAX_COUNT,
|
||||
'automations.min_interval': AUTOMATION_MIN_INTERVAL,
|
||||
'automations.auth_token_expires_in': AUTOMATION_AUTH_TOKEN_EXPIRES_IN,
|
||||
'notes.enable': ENABLE_NOTES,
|
||||
'users.enable_status': ENABLE_USER_STATUS,
|
||||
'evaluation.arena.enable': ENABLE_EVALUATION_ARENA_MODELS,
|
||||
'evaluation.arena.models': EVALUATION_ARENA_MODELS,
|
||||
'webhook_url': WEBHOOK_URL,
|
||||
'ui.enable_community_sharing': ENABLE_COMMUNITY_SHARING,
|
||||
'ui.enable_message_rating': ENABLE_MESSAGE_RATING,
|
||||
'ui.enable_user_webhooks': ENABLE_USER_WEBHOOKS,
|
||||
'ui.banners': WEBUI_BANNERS,
|
||||
'auth.admin.show': SHOW_ADMIN_DETAILS,
|
||||
'auth.admin.email': ADMIN_EMAIL,
|
||||
'task.model.default': TASK_MODEL,
|
||||
'task.model.external': TASK_MODEL_EXTERNAL,
|
||||
'chat.context_compaction.enable': ENABLE_CONTEXT_COMPACTION,
|
||||
'chat.context_compaction.token_threshold': CONTEXT_COMPACTION_TOKEN_THRESHOLD,
|
||||
'chat.context_compaction.prompt_template': CONTEXT_COMPACTION_PROMPT_TEMPLATE,
|
||||
'task.title.prompt_template': TITLE_GENERATION_PROMPT_TEMPLATE,
|
||||
'task.tags.prompt_template': TAGS_GENERATION_PROMPT_TEMPLATE,
|
||||
'task.image.prompt_template': IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE,
|
||||
'task.follow_up.prompt_template': FOLLOW_UP_GENERATION_PROMPT_TEMPLATE,
|
||||
'task.follow_up.enable': ENABLE_FOLLOW_UP_GENERATION,
|
||||
'task.tags.enable': ENABLE_TAGS_GENERATION,
|
||||
'task.title.enable': ENABLE_TITLE_GENERATION,
|
||||
'task.query.search.enable': ENABLE_SEARCH_QUERY_GENERATION,
|
||||
'task.query.retrieval.enable': ENABLE_RETRIEVAL_QUERY_GENERATION,
|
||||
'task.query.prompt_template': QUERY_GENERATION_PROMPT_TEMPLATE,
|
||||
'task.autocomplete.enable': ENABLE_AUTOCOMPLETE_GENERATION,
|
||||
'task.autocomplete.input_max_length': AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH,
|
||||
'task.autocomplete.prompt_template': AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE,
|
||||
'task.voice.prompt_template': VOICE_MODE_PROMPT_TEMPLATE,
|
||||
'task.voice.prompt.enable': ENABLE_VOICE_MODE_PROMPT,
|
||||
'task.tools.prompt_template': TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
|
||||
'auth.enable_api_keys': ENABLE_API_KEYS,
|
||||
'auth.api_key.endpoint_restrictions': ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS,
|
||||
'auth.api_key.allowed_endpoints': API_KEYS_ALLOWED_ENDPOINTS,
|
||||
'auth.jwt_expiry': JWT_EXPIRES_IN,
|
||||
'oauth.enable_signup': ENABLE_OAUTH_SIGNUP,
|
||||
'oauth.auto_redirect': OAUTH_AUTO_REDIRECT,
|
||||
'oauth.refresh_token.include_scope': OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE,
|
||||
'oauth.merge_accounts_by_email': OAUTH_MERGE_ACCOUNTS_BY_EMAIL,
|
||||
'oauth.google.client_id': GOOGLE_CLIENT_ID,
|
||||
'oauth.google.client_secret': GOOGLE_CLIENT_SECRET,
|
||||
'oauth.google.scope': GOOGLE_OAUTH_SCOPE,
|
||||
'oauth.google.redirect_uri': GOOGLE_REDIRECT_URI,
|
||||
'oauth.microsoft.client_id': MICROSOFT_CLIENT_ID,
|
||||
'oauth.microsoft.client_secret': MICROSOFT_CLIENT_SECRET,
|
||||
'oauth.microsoft.tenant_id': MICROSOFT_CLIENT_TENANT_ID,
|
||||
'oauth.microsoft.login_base_url': MICROSOFT_CLIENT_LOGIN_BASE_URL,
|
||||
'oauth.microsoft.picture_url': MICROSOFT_CLIENT_PICTURE_URL,
|
||||
'oauth.microsoft.scope': MICROSOFT_OAUTH_SCOPE,
|
||||
'oauth.microsoft.redirect_uri': MICROSOFT_REDIRECT_URI,
|
||||
'oauth.github.client_id': GITHUB_CLIENT_ID,
|
||||
'oauth.github.client_secret': GITHUB_CLIENT_SECRET,
|
||||
'oauth.github.scope': GITHUB_CLIENT_SCOPE,
|
||||
'oauth.github.redirect_uri': GITHUB_CLIENT_REDIRECT_URI,
|
||||
'oauth.client_id': OAUTH_CLIENT_ID,
|
||||
'oauth.client_secret': OAUTH_CLIENT_SECRET,
|
||||
'oauth.provider_url': OPENID_PROVIDER_URL,
|
||||
'oauth.end_session_endpoint': OPENID_END_SESSION_ENDPOINT,
|
||||
'oauth.redirect_uri': OPENID_REDIRECT_URI,
|
||||
'oauth.scopes': OAUTH_SCOPES,
|
||||
'oauth.timeout': OAUTH_TIMEOUT,
|
||||
'oauth.token_endpoint_auth_method': OAUTH_TOKEN_ENDPOINT_AUTH_METHOD,
|
||||
'oauth.code_challenge_method': OAUTH_CODE_CHALLENGE_METHOD,
|
||||
'oauth.provider_name': OAUTH_PROVIDER_NAME,
|
||||
'oauth.sub_claim': OAUTH_SUB_CLAIM,
|
||||
'oauth.username_claim': OAUTH_USERNAME_CLAIM,
|
||||
'oauth.picture_claim': OAUTH_PICTURE_CLAIM,
|
||||
'oauth.email_claim': OAUTH_EMAIL_CLAIM,
|
||||
'oauth.group_claim': OAUTH_GROUPS_CLAIM,
|
||||
'oauth.feishu.client_id': FEISHU_CLIENT_ID,
|
||||
'oauth.feishu.client_secret': FEISHU_CLIENT_SECRET,
|
||||
'oauth.feishu.scope': FEISHU_OAUTH_SCOPE,
|
||||
'oauth.feishu.redirect_uri': FEISHU_REDIRECT_URI,
|
||||
'oauth.enable_role_mapping': ENABLE_OAUTH_ROLE_MANAGEMENT,
|
||||
'oauth.enable_group_mapping': ENABLE_OAUTH_GROUP_MANAGEMENT,
|
||||
'oauth.enable_group_creation': ENABLE_OAUTH_GROUP_CREATION,
|
||||
'oauth.group_default_share': OAUTH_GROUP_DEFAULT_SHARE,
|
||||
'oauth.blocked_groups': OAUTH_BLOCKED_GROUPS,
|
||||
'oauth.roles_claim': OAUTH_ROLES_CLAIM,
|
||||
'oauth.allowed_roles': OAUTH_ALLOWED_ROLES,
|
||||
'oauth.admin_roles': OAUTH_ADMIN_ROLES,
|
||||
'oauth.allowed_domains': OAUTH_ALLOWED_DOMAINS,
|
||||
'oauth.update_picture_on_login': OAUTH_UPDATE_PICTURE_ON_LOGIN,
|
||||
'oauth.update_name_on_login': OAUTH_UPDATE_NAME_ON_LOGIN,
|
||||
'oauth.update_email_on_login': OAUTH_UPDATE_EMAIL_ON_LOGIN,
|
||||
'oauth.audience': OAUTH_AUDIENCE,
|
||||
'ldap.enable': ENABLE_LDAP,
|
||||
'ldap.server.label': LDAP_SERVER_LABEL,
|
||||
'ldap.server.host': LDAP_SERVER_HOST,
|
||||
'ldap.server.port': LDAP_SERVER_PORT,
|
||||
'ldap.server.attribute_for_mail': LDAP_ATTRIBUTE_FOR_MAIL,
|
||||
'ldap.server.attribute_for_username': LDAP_ATTRIBUTE_FOR_USERNAME,
|
||||
'ldap.server.app_dn': LDAP_APP_DN,
|
||||
'ldap.server.app_password': LDAP_APP_PASSWORD,
|
||||
'ldap.server.users_dn': LDAP_SEARCH_BASE,
|
||||
'ldap.server.search_filter': LDAP_SEARCH_FILTERS,
|
||||
'ldap.server.use_tls': LDAP_USE_TLS,
|
||||
'ldap.server.ca_cert_file': LDAP_CA_CERT_FILE,
|
||||
'ldap.server.validate_cert': LDAP_VALIDATE_CERT,
|
||||
'ldap.server.ciphers': LDAP_CIPHERS,
|
||||
'ldap.group.enable_management': ENABLE_LDAP_GROUP_MANAGEMENT,
|
||||
'ldap.group.enable_creation': ENABLE_LDAP_GROUP_CREATION,
|
||||
'ldap.server.attribute_for_groups': LDAP_ATTRIBUTE_FOR_GROUPS,
|
||||
}
|
||||
|
||||
|
||||
ENABLE_PERSISTENT_CONFIG = os.getenv('ENABLE_PERSISTENT_CONFIG', 'True').lower() == 'true'
|
||||
ENABLE_OAUTH_PERSISTENT_CONFIG = os.getenv('ENABLE_OAUTH_PERSISTENT_CONFIG', 'False').lower() == 'true'
|
||||
|
||||
Config.configure(
|
||||
defaults=DEFAULT_CONFIG,
|
||||
enable_persistent=ENABLE_PERSISTENT_CONFIG,
|
||||
enable_oauth_persistent=ENABLE_OAUTH_PERSISTENT_CONFIG,
|
||||
ENABLE_LDAP = PersistentConfig(
|
||||
'ENABLE_LDAP',
|
||||
'ldap.enable',
|
||||
os.environ.get('ENABLE_LDAP', 'false').lower() == 'true',
|
||||
)
|
||||
|
||||
LDAP_SERVER_LABEL = PersistentConfig(
|
||||
'LDAP_SERVER_LABEL',
|
||||
'ldap.server.label',
|
||||
os.environ.get('LDAP_SERVER_LABEL', 'LDAP Server'),
|
||||
)
|
||||
|
||||
LDAP_SERVER_HOST = PersistentConfig(
|
||||
'LDAP_SERVER_HOST',
|
||||
'ldap.server.host',
|
||||
os.environ.get('LDAP_SERVER_HOST', 'localhost'),
|
||||
)
|
||||
|
||||
LDAP_SERVER_PORT = PersistentConfig(
|
||||
'LDAP_SERVER_PORT',
|
||||
'ldap.server.port',
|
||||
int(os.environ.get('LDAP_SERVER_PORT', '389')),
|
||||
)
|
||||
|
||||
LDAP_ATTRIBUTE_FOR_MAIL = PersistentConfig(
|
||||
'LDAP_ATTRIBUTE_FOR_MAIL',
|
||||
'ldap.server.attribute_for_mail',
|
||||
os.environ.get('LDAP_ATTRIBUTE_FOR_MAIL', 'mail'),
|
||||
)
|
||||
|
||||
LDAP_ATTRIBUTE_FOR_USERNAME = PersistentConfig(
|
||||
'LDAP_ATTRIBUTE_FOR_USERNAME',
|
||||
'ldap.server.attribute_for_username',
|
||||
os.environ.get('LDAP_ATTRIBUTE_FOR_USERNAME', 'uid'),
|
||||
)
|
||||
|
||||
LDAP_APP_DN = PersistentConfig('LDAP_APP_DN', 'ldap.server.app_dn', os.environ.get('LDAP_APP_DN', ''))
|
||||
|
||||
LDAP_APP_PASSWORD = PersistentConfig(
|
||||
'LDAP_APP_PASSWORD',
|
||||
'ldap.server.app_password',
|
||||
os.environ.get('LDAP_APP_PASSWORD', ''),
|
||||
)
|
||||
|
||||
LDAP_SEARCH_BASE = PersistentConfig('LDAP_SEARCH_BASE', 'ldap.server.users_dn', os.environ.get('LDAP_SEARCH_BASE', ''))
|
||||
|
||||
LDAP_SEARCH_FILTERS = PersistentConfig(
|
||||
'LDAP_SEARCH_FILTER',
|
||||
'ldap.server.search_filter',
|
||||
os.environ.get('LDAP_SEARCH_FILTER', os.environ.get('LDAP_SEARCH_FILTERS', '')),
|
||||
)
|
||||
|
||||
LDAP_USE_TLS = PersistentConfig(
|
||||
'LDAP_USE_TLS',
|
||||
'ldap.server.use_tls',
|
||||
os.environ.get('LDAP_USE_TLS', 'True').lower() == 'true',
|
||||
)
|
||||
|
||||
LDAP_CA_CERT_FILE = PersistentConfig(
|
||||
'LDAP_CA_CERT_FILE',
|
||||
'ldap.server.ca_cert_file',
|
||||
os.environ.get('LDAP_CA_CERT_FILE', ''),
|
||||
)
|
||||
|
||||
LDAP_VALIDATE_CERT = PersistentConfig(
|
||||
'LDAP_VALIDATE_CERT',
|
||||
'ldap.server.validate_cert',
|
||||
os.environ.get('LDAP_VALIDATE_CERT', 'True').lower() == 'true',
|
||||
)
|
||||
|
||||
LDAP_CIPHERS = PersistentConfig('LDAP_CIPHERS', 'ldap.server.ciphers', os.environ.get('LDAP_CIPHERS', 'ALL'))
|
||||
|
||||
# For LDAP Group Management
|
||||
ENABLE_LDAP_GROUP_MANAGEMENT = PersistentConfig(
|
||||
'ENABLE_LDAP_GROUP_MANAGEMENT',
|
||||
'ldap.group.enable_management',
|
||||
os.environ.get('ENABLE_LDAP_GROUP_MANAGEMENT', 'False').lower() == 'true',
|
||||
)
|
||||
|
||||
ENABLE_LDAP_GROUP_CREATION = PersistentConfig(
|
||||
'ENABLE_LDAP_GROUP_CREATION',
|
||||
'ldap.group.enable_creation',
|
||||
os.environ.get('ENABLE_LDAP_GROUP_CREATION', 'False').lower() == 'true',
|
||||
)
|
||||
|
||||
LDAP_ATTRIBUTE_FOR_GROUPS = PersistentConfig(
|
||||
'LDAP_ATTRIBUTE_FOR_GROUPS',
|
||||
'ldap.server.attribute_for_groups',
|
||||
os.environ.get('LDAP_ATTRIBUTE_FOR_GROUPS', 'memberOf'),
|
||||
)
|
||||
|
||||
@@ -1,29 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import errno
|
||||
from enum import Enum
|
||||
|
||||
|
||||
_ERRNO_MESSAGES = {
|
||||
errno.ENAMETOOLONG: 'File name is too long.',
|
||||
errno.ENOSPC: 'The server is out of storage space.',
|
||||
errno.EDQUOT: 'Server storage quota exceeded.',
|
||||
errno.EACCES: 'Server storage is not writable.',
|
||||
errno.EPERM: 'Server storage is not writable.',
|
||||
errno.EROFS: 'Server storage is not writable.',
|
||||
}
|
||||
|
||||
|
||||
def _error_message(err='', fallback='') -> str:
|
||||
if not err:
|
||||
return 'Something went wrong :/'
|
||||
if isinstance(err, OSError) and err.errno in _ERRNO_MESSAGES:
|
||||
return f'[ERROR: {_ERRNO_MESSAGES[err.errno]}]'
|
||||
if isinstance(err, Exception):
|
||||
return f'[ERROR: {fallback}]' if fallback else 'Something went wrong :/'
|
||||
return f'[ERROR: {err}]'
|
||||
|
||||
|
||||
class MESSAGES(str, Enum):
|
||||
DEFAULT = lambda msg='': f'{msg if msg else ""}'
|
||||
MODEL_ADDED = lambda model='': f"The model '{model}' has been added successfully."
|
||||
@@ -39,7 +16,7 @@ class ERROR_MESSAGES(str, Enum):
|
||||
def __str__(self) -> str:
|
||||
return super().__str__()
|
||||
|
||||
DEFAULT = _error_message
|
||||
DEFAULT = lambda err='': f'{"Something went wrong :/" if err == "" else "[ERROR: " + str(err) + "]"}'
|
||||
ENV_VAR_NOT_FOUND = 'Required environment variable not found. Terminating now.'
|
||||
CREATE_USER_ERROR = 'Oops! Something went wrong while creating your account. Please try again later. If the issue persists, contact support for assistance.'
|
||||
DELETE_USER_ERROR = 'Oops! Something went wrong. We encountered an issue while trying to delete the user. Please give it another shot.'
|
||||
@@ -95,11 +72,11 @@ class ERROR_MESSAGES(str, Enum):
|
||||
|
||||
EMPTY_CONTENT = 'The content provided is empty. Please ensure that there is text or data present before proceeding.'
|
||||
|
||||
DB_NOT_SQLITE = 'This feature is only available with SQLite databases.'
|
||||
DB_NOT_SQLITE = 'This feature is only available when running with SQLite databases.'
|
||||
|
||||
INVALID_URL = 'The URL you provided is invalid. Please double-check and try again.'
|
||||
INVALID_URL = 'Oops! The URL you provided is invalid. Please double-check and try again.'
|
||||
|
||||
WEB_SEARCH_ERROR = lambda err='': err if err else 'Something went wrong while searching the web.'
|
||||
WEB_SEARCH_ERROR = lambda err='': f'{err if err else "Oops! Something went wrong while searching the web."}'
|
||||
|
||||
OLLAMA_API_DISABLED = 'The Ollama API is disabled. Please enable it to use this feature.'
|
||||
|
||||
|
||||
+458
-547
@@ -1,20 +1,22 @@
|
||||
import datetime as dt
|
||||
import importlib.metadata
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pkgutil
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import shutil
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
from pathlib import Path
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
import re
|
||||
|
||||
|
||||
import markdown
|
||||
from bs4 import BeautifulSoup
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from open_webui.constants import ERROR_MESSAGES
|
||||
|
||||
####################################
|
||||
# Load .env file
|
||||
@@ -39,27 +41,28 @@ try:
|
||||
except ImportError:
|
||||
print('dotenv not installed, skipping...')
|
||||
|
||||
DOCKER = os.getenv('DOCKER', 'False').lower() == 'true'
|
||||
DOCKER = os.environ.get('DOCKER', 'False').lower() == 'true'
|
||||
|
||||
USE_CUDA = os.getenv('USE_CUDA_DOCKER', 'false')
|
||||
DEVICE_TYPE = 'cpu'
|
||||
_cuda_error: Optional[str] = None
|
||||
# device type embedding models - "cpu" (default), "cuda" (nvidia gpu required) or "mps" (apple silicon) - choosing this right can lead to better performance
|
||||
USE_CUDA = os.environ.get('USE_CUDA_DOCKER', 'false')
|
||||
|
||||
if USE_CUDA.lower() == 'true':
|
||||
try:
|
||||
import torch # noqa: E402
|
||||
import torch
|
||||
|
||||
if not torch.cuda.is_available():
|
||||
raise RuntimeError('CUDA not available')
|
||||
assert torch.cuda.is_available(), 'CUDA not available'
|
||||
DEVICE_TYPE = 'cuda'
|
||||
except Exception as exc:
|
||||
_cuda_error = f'CUDA unavailable (USE_CUDA_DOCKER=true), falling back to CPU: {exc}'
|
||||
except Exception as e:
|
||||
cuda_error = f'Error when testing CUDA but USE_CUDA_DOCKER is true. Resetting USE_CUDA_DOCKER to false: {e}'
|
||||
os.environ['USE_CUDA_DOCKER'] = 'false'
|
||||
USE_CUDA = 'false'
|
||||
DEVICE_TYPE = 'cpu'
|
||||
else:
|
||||
DEVICE_TYPE = 'cpu'
|
||||
|
||||
if sys.platform == 'darwin' and DEVICE_TYPE == 'cpu':
|
||||
if sys.platform == 'darwin':
|
||||
try:
|
||||
import torch # noqa: E402
|
||||
import torch
|
||||
|
||||
if torch.backends.mps.is_available() and torch.backends.mps.is_built():
|
||||
DEVICE_TYPE = 'mps'
|
||||
@@ -84,7 +87,7 @@ class JSONFormatter(logging.Formatter):
|
||||
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
log_entry: dict[str, Any] = {
|
||||
'ts': dt.datetime.fromtimestamp(record.created, tz=dt.UTC).isoformat(timespec='milliseconds'),
|
||||
'ts': datetime.fromtimestamp(record.created, tz=timezone.utc).isoformat(timespec='milliseconds'),
|
||||
'level': _LEVEL_MAP.get(record.levelname, record.levelname.lower()),
|
||||
'msg': record.getMessage(),
|
||||
'caller': record.name,
|
||||
@@ -101,37 +104,43 @@ class JSONFormatter(logging.Formatter):
|
||||
return json.dumps(log_entry, ensure_ascii=False, default=str)
|
||||
|
||||
|
||||
LOG_FORMAT = os.getenv('LOG_FORMAT', '').lower()
|
||||
LOG_FORMAT = os.environ.get('LOG_FORMAT', '').lower()
|
||||
|
||||
GLOBAL_LOG_LEVEL = os.getenv('GLOBAL_LOG_LEVEL', '').upper()
|
||||
GLOBAL_LOG_LEVEL = os.environ.get('GLOBAL_LOG_LEVEL', '').upper()
|
||||
if GLOBAL_LOG_LEVEL in logging.getLevelNamesMapping():
|
||||
_log_cfg: dict[str, Any] = {'level': GLOBAL_LOG_LEVEL, 'force': True}
|
||||
if LOG_FORMAT == 'json':
|
||||
_json_handler = logging.StreamHandler(sys.stdout)
|
||||
_json_handler.setFormatter(JSONFormatter())
|
||||
_log_cfg['handlers'] = [_json_handler]
|
||||
_handler = logging.StreamHandler(sys.stdout)
|
||||
_handler.setFormatter(JSONFormatter())
|
||||
logging.basicConfig(handlers=[_handler], level=GLOBAL_LOG_LEVEL, force=True)
|
||||
else:
|
||||
_log_cfg['stream'] = sys.stdout
|
||||
logging.basicConfig(**_log_cfg)
|
||||
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL, force=True)
|
||||
else:
|
||||
GLOBAL_LOG_LEVEL = 'INFO'
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.info('GLOBAL_LOG_LEVEL: %s', GLOBAL_LOG_LEVEL)
|
||||
log.info(f'GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}')
|
||||
|
||||
if _cuda_error:
|
||||
log.error(_cuda_error)
|
||||
_cuda_error = None
|
||||
if 'cuda_error' in locals():
|
||||
log.exception(cuda_error)
|
||||
del cuda_error
|
||||
|
||||
SRC_LOG_LEVELS = {} # Legacy variable, do not remove
|
||||
|
||||
WEBUI_NAME = os.environ.get('WEBUI_NAME', 'Open WebUI')
|
||||
if WEBUI_NAME != 'Open WebUI':
|
||||
WEBUI_NAME += ' (Open WebUI)'
|
||||
|
||||
WEBUI_FAVICON_URL = 'https://openwebui.com/favicon.png'
|
||||
|
||||
TRUSTED_SIGNATURE_KEY = os.environ.get('TRUSTED_SIGNATURE_KEY', '')
|
||||
|
||||
####################################
|
||||
# ENV (dev,test,prod)
|
||||
####################################
|
||||
|
||||
ENV = os.getenv('ENV', 'dev')
|
||||
ENV = os.environ.get('ENV', 'dev')
|
||||
|
||||
FROM_INIT_PY = os.getenv('FROM_INIT_PY', 'False').lower() == 'true'
|
||||
FROM_INIT_PY = os.environ.get('FROM_INIT_PY', 'False').lower() == 'true'
|
||||
|
||||
if FROM_INIT_PY:
|
||||
PACKAGE_DATA = {'version': importlib.metadata.version('open-webui')}
|
||||
@@ -144,10 +153,10 @@ else:
|
||||
VERSION = PACKAGE_DATA['version']
|
||||
|
||||
|
||||
DEPLOYMENT_ID = os.getenv('DEPLOYMENT_ID', '')
|
||||
INSTANCE_ID = os.getenv('INSTANCE_ID', str(uuid4()))
|
||||
DEPLOYMENT_ID = os.environ.get('DEPLOYMENT_ID', '')
|
||||
INSTANCE_ID = os.environ.get('INSTANCE_ID', str(uuid4()))
|
||||
|
||||
ENABLE_DB_MIGRATIONS = os.getenv('ENABLE_DB_MIGRATIONS', 'True').lower() == 'true'
|
||||
ENABLE_DB_MIGRATIONS = os.environ.get('ENABLE_DB_MIGRATIONS', 'True').lower() == 'true'
|
||||
|
||||
|
||||
# Function to parse each section
|
||||
@@ -171,7 +180,7 @@ def parse_section(section):
|
||||
|
||||
try:
|
||||
changelog_path = BASE_DIR / 'CHANGELOG.md'
|
||||
with open(str(changelog_path.absolute()), encoding='utf8') as file:
|
||||
with open(str(changelog_path.absolute()), 'r', encoding='utf8') as file:
|
||||
changelog_content = file.read()
|
||||
|
||||
except Exception:
|
||||
@@ -209,6 +218,42 @@ for version in soup.find_all('h2'):
|
||||
|
||||
CHANGELOG = changelog_json
|
||||
|
||||
####################################
|
||||
# SAFE_MODE
|
||||
####################################
|
||||
|
||||
SAFE_MODE = os.environ.get('SAFE_MODE', 'false').lower() == 'true'
|
||||
|
||||
|
||||
####################################
|
||||
# ENABLE_FORWARD_USER_INFO_HEADERS
|
||||
####################################
|
||||
|
||||
ENABLE_FORWARD_USER_INFO_HEADERS = os.environ.get('ENABLE_FORWARD_USER_INFO_HEADERS', 'False').lower() == 'true'
|
||||
|
||||
# Header names for user info forwarding (customizable via environment variables)
|
||||
FORWARD_USER_INFO_HEADER_USER_NAME = os.environ.get('FORWARD_USER_INFO_HEADER_USER_NAME', 'X-OpenWebUI-User-Name')
|
||||
FORWARD_USER_INFO_HEADER_USER_ID = os.environ.get('FORWARD_USER_INFO_HEADER_USER_ID', 'X-OpenWebUI-User-Id')
|
||||
FORWARD_USER_INFO_HEADER_USER_EMAIL = os.environ.get('FORWARD_USER_INFO_HEADER_USER_EMAIL', 'X-OpenWebUI-User-Email')
|
||||
FORWARD_USER_INFO_HEADER_USER_ROLE = os.environ.get('FORWARD_USER_INFO_HEADER_USER_ROLE', 'X-OpenWebUI-User-Role')
|
||||
|
||||
# Header name for chat ID forwarding (customizable via environment variable)
|
||||
FORWARD_SESSION_INFO_HEADER_MESSAGE_ID = os.environ.get(
|
||||
'FORWARD_SESSION_INFO_HEADER_MESSAGE_ID', 'X-OpenWebUI-Message-Id'
|
||||
)
|
||||
FORWARD_SESSION_INFO_HEADER_CHAT_ID = os.environ.get('FORWARD_SESSION_INFO_HEADER_CHAT_ID', 'X-OpenWebUI-Chat-Id')
|
||||
|
||||
# Experimental feature, may be removed in future
|
||||
ENABLE_STAR_SESSIONS_MIDDLEWARE = os.environ.get('ENABLE_STAR_SESSIONS_MIDDLEWARE', 'False').lower() == 'true'
|
||||
|
||||
ENABLE_EASTER_EGGS = os.environ.get('ENABLE_EASTER_EGGS', 'True').lower() == 'true'
|
||||
|
||||
####################################
|
||||
# WEBUI_BUILD_HASH
|
||||
####################################
|
||||
|
||||
WEBUI_BUILD_HASH = os.environ.get('WEBUI_BUILD_HASH', 'dev-build')
|
||||
|
||||
####################################
|
||||
# DATA/FRONTEND BUILD DIR
|
||||
####################################
|
||||
@@ -258,11 +303,11 @@ if os.path.exists(f'{DATA_DIR}/ollama.db'):
|
||||
else:
|
||||
pass
|
||||
|
||||
DATABASE_URL = os.getenv('DATABASE_URL', f'sqlite:///{DATA_DIR}/webui.db')
|
||||
DATABASE_URL = os.environ.get('DATABASE_URL', f'sqlite:///{DATA_DIR}/webui.db')
|
||||
|
||||
DATABASE_TYPE = os.getenv('DATABASE_TYPE')
|
||||
DATABASE_USER = os.getenv('DATABASE_USER')
|
||||
DATABASE_PASSWORD = os.getenv('DATABASE_PASSWORD')
|
||||
DATABASE_TYPE = os.environ.get('DATABASE_TYPE')
|
||||
DATABASE_USER = os.environ.get('DATABASE_USER')
|
||||
DATABASE_PASSWORD = os.environ.get('DATABASE_PASSWORD')
|
||||
|
||||
DATABASE_CRED = ''
|
||||
if DATABASE_USER:
|
||||
@@ -273,16 +318,16 @@ if DATABASE_PASSWORD:
|
||||
DB_VARS = {
|
||||
'db_type': DATABASE_TYPE,
|
||||
'db_cred': DATABASE_CRED,
|
||||
'db_host': os.getenv('DATABASE_HOST'),
|
||||
'db_port': os.getenv('DATABASE_PORT'),
|
||||
'db_name': os.getenv('DATABASE_NAME'),
|
||||
'db_host': os.environ.get('DATABASE_HOST'),
|
||||
'db_port': os.environ.get('DATABASE_PORT'),
|
||||
'db_name': os.environ.get('DATABASE_NAME'),
|
||||
}
|
||||
|
||||
if all(DB_VARS.values()):
|
||||
DATABASE_URL = (
|
||||
f'{DB_VARS["db_type"]}://{DB_VARS["db_cred"]}@{DB_VARS["db_host"]}:{DB_VARS["db_port"]}/{DB_VARS["db_name"]}'
|
||||
)
|
||||
elif DATABASE_TYPE == 'sqlite+sqlcipher' and not os.getenv('DATABASE_URL'):
|
||||
elif DATABASE_TYPE == 'sqlite+sqlcipher' and not os.environ.get('DATABASE_URL'):
|
||||
# Handle SQLCipher with local file when DATABASE_URL wasn't explicitly set
|
||||
DATABASE_URL = f'sqlite+sqlcipher:///{DATA_DIR}/webui.db'
|
||||
|
||||
@@ -290,34 +335,47 @@ elif DATABASE_TYPE == 'sqlite+sqlcipher' and not os.getenv('DATABASE_URL'):
|
||||
if 'postgres://' in DATABASE_URL:
|
||||
DATABASE_URL = DATABASE_URL.replace('postgres://', 'postgresql://')
|
||||
|
||||
DATABASE_SCHEMA = os.getenv('DATABASE_SCHEMA', None)
|
||||
DATABASE_ENABLE_IAM_TOKEN_AUTH = os.getenv('DATABASE_ENABLE_IAM_TOKEN_AUTH', 'False').lower() == 'true'
|
||||
DATABASE_SCHEMA = os.environ.get('DATABASE_SCHEMA', None)
|
||||
|
||||
_pool_size_raw = os.getenv('DATABASE_POOL_SIZE')
|
||||
try:
|
||||
DATABASE_POOL_SIZE = int(_pool_size_raw) if _pool_size_raw else None
|
||||
except (ValueError, TypeError):
|
||||
DATABASE_POOL_SIZE = None
|
||||
DATABASE_POOL_SIZE = os.environ.get('DATABASE_POOL_SIZE', None)
|
||||
|
||||
_pool_overflow_raw = os.getenv('DATABASE_POOL_MAX_OVERFLOW', '0')
|
||||
try:
|
||||
DATABASE_POOL_MAX_OVERFLOW = int(_pool_overflow_raw) if _pool_overflow_raw else 0
|
||||
except (ValueError, TypeError):
|
||||
if DATABASE_POOL_SIZE != None:
|
||||
try:
|
||||
DATABASE_POOL_SIZE = int(DATABASE_POOL_SIZE)
|
||||
except Exception:
|
||||
DATABASE_POOL_SIZE = None
|
||||
|
||||
DATABASE_POOL_MAX_OVERFLOW = os.environ.get('DATABASE_POOL_MAX_OVERFLOW', 0)
|
||||
|
||||
if DATABASE_POOL_MAX_OVERFLOW == '':
|
||||
DATABASE_POOL_MAX_OVERFLOW = 0
|
||||
else:
|
||||
try:
|
||||
DATABASE_POOL_MAX_OVERFLOW = int(DATABASE_POOL_MAX_OVERFLOW)
|
||||
except Exception:
|
||||
DATABASE_POOL_MAX_OVERFLOW = 0
|
||||
|
||||
_pool_timeout_raw = os.getenv('DATABASE_POOL_TIMEOUT', '30')
|
||||
try:
|
||||
DATABASE_POOL_TIMEOUT = int(_pool_timeout_raw) if _pool_timeout_raw else 30
|
||||
except (ValueError, TypeError):
|
||||
DATABASE_POOL_TIMEOUT = os.environ.get('DATABASE_POOL_TIMEOUT', 30)
|
||||
|
||||
if DATABASE_POOL_TIMEOUT == '':
|
||||
DATABASE_POOL_TIMEOUT = 30
|
||||
else:
|
||||
try:
|
||||
DATABASE_POOL_TIMEOUT = int(DATABASE_POOL_TIMEOUT)
|
||||
except Exception:
|
||||
DATABASE_POOL_TIMEOUT = 30
|
||||
|
||||
_pool_recycle_raw = os.getenv('DATABASE_POOL_RECYCLE', '3600')
|
||||
try:
|
||||
DATABASE_POOL_RECYCLE = int(_pool_recycle_raw) if _pool_recycle_raw else 3600
|
||||
except (ValueError, TypeError):
|
||||
DATABASE_POOL_RECYCLE = os.environ.get('DATABASE_POOL_RECYCLE', 3600)
|
||||
|
||||
if DATABASE_POOL_RECYCLE == '':
|
||||
DATABASE_POOL_RECYCLE = 3600
|
||||
else:
|
||||
try:
|
||||
DATABASE_POOL_RECYCLE = int(DATABASE_POOL_RECYCLE)
|
||||
except Exception:
|
||||
DATABASE_POOL_RECYCLE = 3600
|
||||
|
||||
DATABASE_ENABLE_SQLITE_WAL = os.getenv('DATABASE_ENABLE_SQLITE_WAL', 'True').lower() == 'true'
|
||||
DATABASE_ENABLE_SQLITE_WAL = os.environ.get('DATABASE_ENABLE_SQLITE_WAL', 'True').lower() == 'true'
|
||||
|
||||
# SQLite PRAGMA tuning — these defaults are optimised for WAL-mode web-server
|
||||
# workloads. Each can be overridden via its environment variable.
|
||||
@@ -325,56 +383,63 @@ DATABASE_ENABLE_SQLITE_WAL = os.getenv('DATABASE_ENABLE_SQLITE_WAL', 'True').low
|
||||
|
||||
# PRAGMA synchronous: NORMAL (1) is safe with WAL and avoids an fsync per
|
||||
# transaction. Valid values: OFF (0), NORMAL (1), FULL (2), EXTRA (3).
|
||||
DATABASE_SQLITE_PRAGMA_SYNCHRONOUS = os.getenv('DATABASE_SQLITE_PRAGMA_SYNCHRONOUS', 'NORMAL')
|
||||
DATABASE_SQLITE_PRAGMA_SYNCHRONOUS = os.environ.get('DATABASE_SQLITE_PRAGMA_SYNCHRONOUS', 'NORMAL')
|
||||
|
||||
# PRAGMA busy_timeout (ms): how long a connection waits for a write lock
|
||||
# before raising SQLITE_BUSY.
|
||||
DATABASE_SQLITE_PRAGMA_BUSY_TIMEOUT = os.getenv('DATABASE_SQLITE_PRAGMA_BUSY_TIMEOUT', '5000')
|
||||
DATABASE_SQLITE_PRAGMA_BUSY_TIMEOUT = os.environ.get('DATABASE_SQLITE_PRAGMA_BUSY_TIMEOUT', '5000')
|
||||
|
||||
# PRAGMA cache_size: negative value = KiB. -65536 ≈ 64 MB page cache.
|
||||
DATABASE_SQLITE_PRAGMA_CACHE_SIZE = os.getenv('DATABASE_SQLITE_PRAGMA_CACHE_SIZE', '-65536')
|
||||
DATABASE_SQLITE_PRAGMA_CACHE_SIZE = os.environ.get('DATABASE_SQLITE_PRAGMA_CACHE_SIZE', '-65536')
|
||||
|
||||
# PRAGMA temp_store: MEMORY (2) keeps temp tables and indices in RAM.
|
||||
# Valid values: DEFAULT (0), FILE (1), MEMORY (2).
|
||||
DATABASE_SQLITE_PRAGMA_TEMP_STORE = os.getenv('DATABASE_SQLITE_PRAGMA_TEMP_STORE', 'MEMORY')
|
||||
DATABASE_SQLITE_PRAGMA_TEMP_STORE = os.environ.get('DATABASE_SQLITE_PRAGMA_TEMP_STORE', 'MEMORY')
|
||||
|
||||
# PRAGMA mmap_size (bytes): memory-mapped I/O size. 268435456 ≈ 256 MB.
|
||||
# Set to 0 to disable mmap.
|
||||
DATABASE_SQLITE_PRAGMA_MMAP_SIZE = os.getenv('DATABASE_SQLITE_PRAGMA_MMAP_SIZE', '268435456')
|
||||
DATABASE_SQLITE_PRAGMA_MMAP_SIZE = os.environ.get('DATABASE_SQLITE_PRAGMA_MMAP_SIZE', '268435456')
|
||||
|
||||
# PRAGMA journal_size_limit (bytes): caps the WAL file size after checkpoint.
|
||||
# Without this the WAL grows unbounded during write bursts and is never
|
||||
# truncated. 67108864 ≈ 64 MB. Set to -1 for no limit (SQLite default).
|
||||
DATABASE_SQLITE_PRAGMA_JOURNAL_SIZE_LIMIT = os.getenv('DATABASE_SQLITE_PRAGMA_JOURNAL_SIZE_LIMIT', '67108864')
|
||||
DATABASE_SQLITE_PRAGMA_JOURNAL_SIZE_LIMIT = os.environ.get('DATABASE_SQLITE_PRAGMA_JOURNAL_SIZE_LIMIT', '67108864')
|
||||
|
||||
DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL = os.getenv('DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL', None)
|
||||
DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL = os.environ.get('DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL', None)
|
||||
if DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL is not None:
|
||||
try:
|
||||
DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL = float(DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL)
|
||||
except Exception:
|
||||
DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL = 0.0
|
||||
|
||||
DATABASE_ENABLE_SESSION_SHARING = os.getenv('DATABASE_ENABLE_SESSION_SHARING', 'False').lower() == 'true'
|
||||
ENABLE_PUBLIC_ACTIVE_USERS_COUNT = os.getenv('ENABLE_PUBLIC_ACTIVE_USERS_COUNT', 'True').lower() == 'true'
|
||||
RESET_CONFIG_ON_START = os.getenv('RESET_CONFIG_ON_START', 'False').lower() == 'true'
|
||||
ENABLE_REALTIME_CHAT_SAVE = os.getenv('ENABLE_REALTIME_CHAT_SAVE', 'False').lower() == 'true'
|
||||
ENABLE_QUERIES_CACHE = os.getenv('ENABLE_QUERIES_CACHE', 'False').lower() == 'true'
|
||||
RAG_SYSTEM_CONTEXT = os.getenv('RAG_SYSTEM_CONTEXT', 'False').lower() == 'true'
|
||||
# When enabled, get_db_context reuses existing sessions; set to False to always create new sessions
|
||||
DATABASE_ENABLE_SESSION_SHARING = os.environ.get('DATABASE_ENABLE_SESSION_SHARING', 'False').lower() == 'true'
|
||||
|
||||
# Enable public visibility of active user count (when disabled, only admins can see it)
|
||||
ENABLE_PUBLIC_ACTIVE_USERS_COUNT = os.environ.get('ENABLE_PUBLIC_ACTIVE_USERS_COUNT', 'True').lower() == 'true'
|
||||
|
||||
RESET_CONFIG_ON_START = os.environ.get('RESET_CONFIG_ON_START', 'False').lower() == 'true'
|
||||
|
||||
ENABLE_REALTIME_CHAT_SAVE = os.environ.get('ENABLE_REALTIME_CHAT_SAVE', 'False').lower() == 'true'
|
||||
|
||||
ENABLE_QUERIES_CACHE = os.environ.get('ENABLE_QUERIES_CACHE', 'False').lower() == 'true'
|
||||
|
||||
RAG_SYSTEM_CONTEXT = os.environ.get('RAG_SYSTEM_CONTEXT', 'False').lower() == 'true'
|
||||
|
||||
####################################
|
||||
# REDIS
|
||||
####################################
|
||||
|
||||
REDIS_URL = os.getenv('REDIS_URL', '')
|
||||
REDIS_CLUSTER = os.getenv('REDIS_CLUSTER', 'False').lower() == 'true'
|
||||
REDIS_URL = os.environ.get('REDIS_URL', '')
|
||||
REDIS_CLUSTER = os.environ.get('REDIS_CLUSTER', 'False').lower() == 'true'
|
||||
|
||||
REDIS_KEY_PREFIX = os.getenv('REDIS_KEY_PREFIX', 'open-webui')
|
||||
REDIS_KEY_PREFIX = os.environ.get('REDIS_KEY_PREFIX', 'open-webui')
|
||||
|
||||
REDIS_SENTINEL_HOSTS = os.getenv('REDIS_SENTINEL_HOSTS', '')
|
||||
REDIS_SENTINEL_PORT = os.getenv('REDIS_SENTINEL_PORT', '26379')
|
||||
REDIS_SENTINEL_HOSTS = os.environ.get('REDIS_SENTINEL_HOSTS', '')
|
||||
REDIS_SENTINEL_PORT = os.environ.get('REDIS_SENTINEL_PORT', '26379')
|
||||
|
||||
# Maximum number of retries for Redis operations when using Sentinel fail-over
|
||||
REDIS_SENTINEL_MAX_RETRY_COUNT = os.getenv('REDIS_SENTINEL_MAX_RETRY_COUNT', '2')
|
||||
REDIS_SENTINEL_MAX_RETRY_COUNT = os.environ.get('REDIS_SENTINEL_MAX_RETRY_COUNT', '2')
|
||||
try:
|
||||
REDIS_SENTINEL_MAX_RETRY_COUNT = int(REDIS_SENTINEL_MAX_RETRY_COUNT)
|
||||
if REDIS_SENTINEL_MAX_RETRY_COUNT < 1:
|
||||
@@ -383,7 +448,7 @@ except ValueError:
|
||||
REDIS_SENTINEL_MAX_RETRY_COUNT = 2
|
||||
|
||||
|
||||
REDIS_SOCKET_CONNECT_TIMEOUT = os.getenv('REDIS_SOCKET_CONNECT_TIMEOUT', '')
|
||||
REDIS_SOCKET_CONNECT_TIMEOUT = os.environ.get('REDIS_SOCKET_CONNECT_TIMEOUT', '')
|
||||
try:
|
||||
REDIS_SOCKET_CONNECT_TIMEOUT = float(REDIS_SOCKET_CONNECT_TIMEOUT)
|
||||
except ValueError:
|
||||
@@ -394,7 +459,7 @@ except ValueError:
|
||||
# enabled, the kernel sends TCP keepalive probes on idle connections so
|
||||
# half-closed sockets (e.g. after a silent firewall/LB reset or a NIC
|
||||
# flap) are detected before the next command lands on them.
|
||||
REDIS_SOCKET_KEEPALIVE = os.getenv('REDIS_SOCKET_KEEPALIVE', 'False').lower() == 'true'
|
||||
REDIS_SOCKET_KEEPALIVE = os.environ.get('REDIS_SOCKET_KEEPALIVE', 'False').lower() == 'true'
|
||||
|
||||
# How often (in seconds) redis-py should PING an idle pooled connection
|
||||
# before reusing it. Opt-in: defaults to unset (empty string) so behavior
|
||||
@@ -402,7 +467,7 @@ REDIS_SOCKET_KEEPALIVE = os.getenv('REDIS_SOCKET_KEEPALIVE', 'False').lower() ==
|
||||
# the Redis server `timeout` setting and any firewall/LB idle timeout on
|
||||
# the path to Redis, so stale connections are detected before a real
|
||||
# command lands on them. Set to 0 or empty to disable.
|
||||
REDIS_HEALTH_CHECK_INTERVAL = os.getenv('REDIS_HEALTH_CHECK_INTERVAL', '')
|
||||
REDIS_HEALTH_CHECK_INTERVAL = os.environ.get('REDIS_HEALTH_CHECK_INTERVAL', '')
|
||||
try:
|
||||
REDIS_HEALTH_CHECK_INTERVAL = int(REDIS_HEALTH_CHECK_INTERVAL)
|
||||
if REDIS_HEALTH_CHECK_INTERVAL <= 0:
|
||||
@@ -410,7 +475,7 @@ try:
|
||||
except ValueError:
|
||||
REDIS_HEALTH_CHECK_INTERVAL = None
|
||||
|
||||
REDIS_RECONNECT_DELAY = os.getenv('REDIS_RECONNECT_DELAY', '')
|
||||
REDIS_RECONNECT_DELAY = os.environ.get('REDIS_RECONNECT_DELAY', '')
|
||||
|
||||
if REDIS_RECONNECT_DELAY == '':
|
||||
REDIS_RECONNECT_DELAY = None
|
||||
@@ -423,288 +488,27 @@ else:
|
||||
REDIS_RECONNECT_DELAY = None
|
||||
|
||||
####################################
|
||||
# Uvicorn
|
||||
# UVICORN WORKERS
|
||||
####################################
|
||||
|
||||
# Number of uvicorn worker processes for handling requests
|
||||
UVICORN_WORKERS = os.environ.get('UVICORN_WORKERS', '1')
|
||||
try:
|
||||
UVICORN_WORKERS = max(int(os.getenv('UVICORN_WORKERS', '1')), 1)
|
||||
except (ValueError, TypeError):
|
||||
UVICORN_WORKERS = int(UVICORN_WORKERS)
|
||||
if UVICORN_WORKERS < 1:
|
||||
UVICORN_WORKERS = 1
|
||||
except ValueError:
|
||||
UVICORN_WORKERS = 1
|
||||
log.info(f'Invalid UVICORN_WORKERS value, defaulting to {UVICORN_WORKERS}')
|
||||
|
||||
####################################
|
||||
# WEBSOCKET SUPPORT
|
||||
# WEBUI_AUTH (Required for security)
|
||||
####################################
|
||||
|
||||
ENABLE_WEBSOCKET_SUPPORT = os.getenv('ENABLE_WEBSOCKET_SUPPORT', 'True').lower() == 'true'
|
||||
WEBUI_AUTH = os.environ.get('WEBUI_AUTH', 'True').lower() == 'true'
|
||||
|
||||
|
||||
WEBSOCKET_MANAGER = os.getenv('WEBSOCKET_MANAGER', '')
|
||||
|
||||
WEBSOCKET_REDIS_OPTIONS = os.getenv('WEBSOCKET_REDIS_OPTIONS', '')
|
||||
|
||||
|
||||
if WEBSOCKET_REDIS_OPTIONS == '':
|
||||
WEBSOCKET_REDIS_OPTIONS = {'socket_timeout': None}
|
||||
if REDIS_SOCKET_CONNECT_TIMEOUT:
|
||||
WEBSOCKET_REDIS_OPTIONS['socket_connect_timeout'] = REDIS_SOCKET_CONNECT_TIMEOUT
|
||||
else:
|
||||
try:
|
||||
WEBSOCKET_REDIS_OPTIONS = json.loads(WEBSOCKET_REDIS_OPTIONS)
|
||||
WEBSOCKET_REDIS_OPTIONS.setdefault('socket_timeout', None)
|
||||
except Exception:
|
||||
log.warning('Invalid WEBSOCKET_REDIS_OPTIONS, defaulting to socket_timeout=None')
|
||||
WEBSOCKET_REDIS_OPTIONS = {'socket_timeout': None}
|
||||
|
||||
WEBSOCKET_REDIS_URL = os.getenv('WEBSOCKET_REDIS_URL', REDIS_URL)
|
||||
WEBSOCKET_REDIS_CLUSTER = os.getenv('WEBSOCKET_REDIS_CLUSTER', str(REDIS_CLUSTER)).lower() == 'true'
|
||||
|
||||
websocket_redis_lock_timeout = os.getenv('WEBSOCKET_REDIS_LOCK_TIMEOUT', '60')
|
||||
|
||||
try:
|
||||
WEBSOCKET_REDIS_LOCK_TIMEOUT = int(websocket_redis_lock_timeout)
|
||||
except ValueError:
|
||||
WEBSOCKET_REDIS_LOCK_TIMEOUT = 60
|
||||
|
||||
WEBSOCKET_SENTINEL_HOSTS = os.getenv('WEBSOCKET_SENTINEL_HOSTS', '')
|
||||
WEBSOCKET_SENTINEL_PORT = os.getenv('WEBSOCKET_SENTINEL_PORT', '26379')
|
||||
WEBSOCKET_SERVER_LOGGING = os.getenv('WEBSOCKET_SERVER_LOGGING', 'False').lower() == 'true'
|
||||
WEBSOCKET_SERVER_ENGINEIO_LOGGING = (
|
||||
os.getenv(
|
||||
'WEBSOCKET_SERVER_ENGINEIO_LOGGING',
|
||||
os.getenv('WEBSOCKET_SERVER_LOGGING', 'False'),
|
||||
).lower()
|
||||
== 'true'
|
||||
)
|
||||
WEBSOCKET_SERVER_PING_TIMEOUT = os.getenv('WEBSOCKET_SERVER_PING_TIMEOUT', '20')
|
||||
try:
|
||||
WEBSOCKET_SERVER_PING_TIMEOUT = int(WEBSOCKET_SERVER_PING_TIMEOUT)
|
||||
except ValueError:
|
||||
WEBSOCKET_SERVER_PING_TIMEOUT = 20
|
||||
|
||||
WEBSOCKET_SERVER_PING_INTERVAL = os.getenv('WEBSOCKET_SERVER_PING_INTERVAL', '25')
|
||||
try:
|
||||
WEBSOCKET_SERVER_PING_INTERVAL = int(WEBSOCKET_SERVER_PING_INTERVAL)
|
||||
except ValueError:
|
||||
WEBSOCKET_SERVER_PING_INTERVAL = 25
|
||||
|
||||
WEBSOCKET_EVENT_CALLER_TIMEOUT = os.getenv('WEBSOCKET_EVENT_CALLER_TIMEOUT', '')
|
||||
|
||||
if WEBSOCKET_EVENT_CALLER_TIMEOUT == '':
|
||||
WEBSOCKET_EVENT_CALLER_TIMEOUT = None
|
||||
else:
|
||||
try:
|
||||
WEBSOCKET_EVENT_CALLER_TIMEOUT = int(WEBSOCKET_EVENT_CALLER_TIMEOUT)
|
||||
except ValueError:
|
||||
WEBSOCKET_EVENT_CALLER_TIMEOUT = 300
|
||||
|
||||
|
||||
import ssl as _ssl
|
||||
|
||||
|
||||
# Dedicated env var for a custom CA bundle file path. When set, this is
|
||||
# used as the default CA bundle for all outbound HTTPS connections that
|
||||
# have SSL verification enabled (i.e. when their per-connection SSL env
|
||||
# var is ``"True"``). Per-connection overrides (setting the SSL env var
|
||||
# to a path directly) take precedence over this global fallback.
|
||||
#
|
||||
# This follows the industry convention of ``SSL_CERT_FILE`` / ``REQUESTS_CA_BUNDLE``
|
||||
# but is scoped to Open WebUI to avoid interfering with system-level settings.
|
||||
AIOHTTP_CLIENT_SSL_CERT_FILE = os.getenv('AIOHTTP_CLIENT_SSL_CERT_FILE', '').strip()
|
||||
|
||||
|
||||
def _build_ssl_context_from_file(path: str) -> '_ssl.SSLContext | None':
|
||||
"""Create an SSLContext from a CA bundle file, or None if invalid."""
|
||||
if not path:
|
||||
return None
|
||||
if not os.path.isfile(path):
|
||||
log.warning(
|
||||
'SSL CA bundle path does not exist: %r, ignoring',
|
||||
path,
|
||||
)
|
||||
return None
|
||||
ctx = _ssl.create_default_context(cafile=path)
|
||||
log.info('Using custom SSL CA bundle: %s', path)
|
||||
return ctx
|
||||
|
||||
|
||||
# Pre-built SSLContext from the dedicated env var (cached once at startup).
|
||||
_GLOBAL_SSL_CONTEXT = _build_ssl_context_from_file(AIOHTTP_CLIENT_SSL_CERT_FILE)
|
||||
|
||||
|
||||
def _parse_ssl_env(value: str) -> 'bool | _ssl.SSLContext':
|
||||
"""Parse an SSL env var into a bool or SSLContext.
|
||||
|
||||
- ``"true"`` → uses ``AIOHTTP_CLIENT_SSL_CERT_FILE`` context if set,
|
||||
otherwise ``True`` (default SSL verification via certifi)
|
||||
- ``"false"`` → ``False`` (no verification)
|
||||
- ``"/path/to/ca-bundle.crt"`` → ``SSLContext`` loading that CA file
|
||||
(takes precedence over ``AIOHTTP_CLIENT_SSL_CERT_FILE``)
|
||||
|
||||
This allows users with corporate or internal CAs to point Open WebUI
|
||||
at a custom CA bundle without disabling verification entirely.
|
||||
"""
|
||||
lower = value.strip().lower()
|
||||
if lower == 'true':
|
||||
# Use the global dedicated CA bundle if configured, otherwise default
|
||||
return _GLOBAL_SSL_CONTEXT if _GLOBAL_SSL_CONTEXT is not None else True
|
||||
if lower == 'false':
|
||||
return False
|
||||
# Treat as a file path to a CA bundle (per-connection override)
|
||||
ctx = _build_ssl_context_from_file(value.strip())
|
||||
if ctx is not None:
|
||||
return ctx
|
||||
# Path was invalid — fall back to default
|
||||
return _GLOBAL_SSL_CONTEXT if _GLOBAL_SSL_CONTEXT is not None else True
|
||||
|
||||
|
||||
REQUESTS_VERIFY = os.getenv('REQUESTS_VERIFY', 'True').lower() == 'true'
|
||||
|
||||
_aiohttp_timeout_raw = os.getenv('AIOHTTP_CLIENT_TIMEOUT', '')
|
||||
try:
|
||||
AIOHTTP_CLIENT_TIMEOUT = int(_aiohttp_timeout_raw) if _aiohttp_timeout_raw else None
|
||||
except (ValueError, TypeError):
|
||||
AIOHTTP_CLIENT_TIMEOUT = 300
|
||||
|
||||
|
||||
# SSL verification for general outbound requests (OpenAI, OAuth, etc.).
|
||||
# Accepts "True", "False", or a path to a CA bundle file.
|
||||
# When "True", falls back to AIOHTTP_CLIENT_SSL_CERT_FILE if set.
|
||||
AIOHTTP_CLIENT_SESSION_SSL = _parse_ssl_env(os.getenv('AIOHTTP_CLIENT_SESSION_SSL', 'True'))
|
||||
|
||||
# When False (default), outbound HTTP requests do not follow 3xx redirects.
|
||||
AIOHTTP_CLIENT_ALLOW_REDIRECTS = os.getenv('AIOHTTP_CLIENT_ALLOW_REDIRECTS', 'False').lower() == 'true'
|
||||
|
||||
# Optional User-Agent override for outbound web-loader fetches. When set,
|
||||
# SafeWebBaseLoader sends this value instead of the default python-requests UA
|
||||
# which is aggressively blocked by Cloudflare, Wikipedia, and similar services.
|
||||
USER_AGENT = os.getenv('USER_AGENT', '')
|
||||
|
||||
_model_list_timeout_raw = os.getenv(
|
||||
'AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST',
|
||||
os.getenv('AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST', '10'),
|
||||
)
|
||||
try:
|
||||
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = int(_model_list_timeout_raw) if _model_list_timeout_raw else None
|
||||
except (ValueError, TypeError):
|
||||
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = 10
|
||||
|
||||
_tool_data_timeout_raw = os.getenv('AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA', '10')
|
||||
try:
|
||||
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = int(_tool_data_timeout_raw) if _tool_data_timeout_raw else None
|
||||
except (ValueError, TypeError):
|
||||
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = 10
|
||||
|
||||
|
||||
# SSL verification for tool server connections specifically.
|
||||
# Accepts "True", "False", or a path to a CA bundle file.
|
||||
# When "True", falls back to AIOHTTP_CLIENT_SSL_CERT_FILE if set.
|
||||
AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL = _parse_ssl_env(os.getenv('AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL', 'True'))
|
||||
|
||||
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER = os.getenv('AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER', '')
|
||||
|
||||
if AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER == '':
|
||||
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER = AIOHTTP_CLIENT_TIMEOUT
|
||||
else:
|
||||
try:
|
||||
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER = int(AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER)
|
||||
except Exception:
|
||||
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER = AIOHTTP_CLIENT_TIMEOUT
|
||||
|
||||
# Timeout (in seconds) for the MCP session.initialize() handshake.
|
||||
# The handshake performs a list-tools round-trip and can take tens of
|
||||
# seconds on cold-start servers or servers exposing many tools.
|
||||
MCP_INITIALIZE_TIMEOUT = os.getenv('MCP_INITIALIZE_TIMEOUT', '10')
|
||||
try:
|
||||
MCP_INITIALIZE_TIMEOUT = int(MCP_INITIALIZE_TIMEOUT)
|
||||
except (ValueError, TypeError):
|
||||
MCP_INITIALIZE_TIMEOUT = 10
|
||||
|
||||
|
||||
####################################
|
||||
# AIOHTTP Connection Pool
|
||||
####################################
|
||||
|
||||
AIOHTTP_POOL_CONNECTIONS = os.getenv('AIOHTTP_POOL_CONNECTIONS', '')
|
||||
if AIOHTTP_POOL_CONNECTIONS == '':
|
||||
AIOHTTP_POOL_CONNECTIONS = None
|
||||
else:
|
||||
try:
|
||||
AIOHTTP_POOL_CONNECTIONS = int(AIOHTTP_POOL_CONNECTIONS)
|
||||
except ValueError:
|
||||
AIOHTTP_POOL_CONNECTIONS = None
|
||||
|
||||
AIOHTTP_POOL_CONNECTIONS_PER_HOST = os.getenv('AIOHTTP_POOL_CONNECTIONS_PER_HOST', '')
|
||||
if AIOHTTP_POOL_CONNECTIONS_PER_HOST == '':
|
||||
AIOHTTP_POOL_CONNECTIONS_PER_HOST = None
|
||||
else:
|
||||
try:
|
||||
AIOHTTP_POOL_CONNECTIONS_PER_HOST = int(AIOHTTP_POOL_CONNECTIONS_PER_HOST)
|
||||
except ValueError:
|
||||
AIOHTTP_POOL_CONNECTIONS_PER_HOST = None
|
||||
|
||||
AIOHTTP_POOL_DNS_TTL = os.getenv('AIOHTTP_POOL_DNS_TTL', '300')
|
||||
try:
|
||||
AIOHTTP_POOL_DNS_TTL = int(AIOHTTP_POOL_DNS_TTL)
|
||||
if AIOHTTP_POOL_DNS_TTL < 0:
|
||||
AIOHTTP_POOL_DNS_TTL = 300
|
||||
except ValueError:
|
||||
AIOHTTP_POOL_DNS_TTL = 300
|
||||
|
||||
RAG_EMBEDDING_TIMEOUT = os.getenv('RAG_EMBEDDING_TIMEOUT', '')
|
||||
|
||||
if RAG_EMBEDDING_TIMEOUT == '':
|
||||
RAG_EMBEDDING_TIMEOUT = None
|
||||
else:
|
||||
try:
|
||||
RAG_EMBEDDING_TIMEOUT = int(RAG_EMBEDDING_TIMEOUT)
|
||||
except Exception:
|
||||
RAG_EMBEDDING_TIMEOUT = None
|
||||
|
||||
|
||||
####################################
|
||||
# Auth
|
||||
####################################
|
||||
|
||||
WEBUI_AUTH = os.getenv('WEBUI_AUTH', 'True').lower() == 'true'
|
||||
|
||||
ENABLE_INITIAL_ADMIN_SIGNUP = os.getenv('ENABLE_INITIAL_ADMIN_SIGNUP', 'False').lower() == 'true'
|
||||
ENABLE_SIGNUP_PASSWORD_CONFIRMATION = os.getenv('ENABLE_SIGNUP_PASSWORD_CONFIRMATION', 'False').lower() == 'true'
|
||||
|
||||
####################################
|
||||
# Secret key & cookies
|
||||
####################################
|
||||
|
||||
# WEBUI_JWT_SECRET_KEY is deprecated; use WEBUI_SECRET_KEY instead.
|
||||
# No hardcoded fallback by design: the supported start scripts set/auto-generate it; unset is rejected below.
|
||||
WEBUI_SECRET_KEY = os.getenv(
|
||||
'WEBUI_SECRET_KEY',
|
||||
os.getenv('WEBUI_JWT_SECRET_KEY', ''),
|
||||
)
|
||||
|
||||
ENABLE_VALVE_ENCRYPTION = os.getenv('ENABLE_VALVE_ENCRYPTION', 'False').lower() == 'true'
|
||||
|
||||
WEBUI_SESSION_COOKIE_SAME_SITE = os.getenv('WEBUI_SESSION_COOKIE_SAME_SITE', 'lax')
|
||||
WEBUI_SESSION_COOKIE_SECURE = os.getenv('WEBUI_SESSION_COOKIE_SECURE', 'false').lower() == 'true'
|
||||
WEBUI_AUTH_COOKIE_SAME_SITE = os.getenv('WEBUI_AUTH_COOKIE_SAME_SITE', WEBUI_SESSION_COOKIE_SAME_SITE)
|
||||
WEBUI_AUTH_COOKIE_SECURE = (
|
||||
os.getenv(
|
||||
'WEBUI_AUTH_COOKIE_SECURE',
|
||||
os.getenv('WEBUI_SESSION_COOKIE_SECURE', 'false'),
|
||||
).lower()
|
||||
== 'true'
|
||||
)
|
||||
|
||||
if WEBUI_AUTH and WEBUI_SECRET_KEY == '':
|
||||
raise SystemExit(
|
||||
'WEBUI_SECRET_KEY is not set. It is a hard requirement when authentication is enabled.\n'
|
||||
'The supported start methods set or auto-generate it for you: use start.sh (Linux/macOS), '
|
||||
'start_windows.bat (Windows), or `open-webui serve`.\n'
|
||||
'If you start the backend another way (e.g. invoking uvicorn directly, which is unsupported), '
|
||||
'you must set WEBUI_SECRET_KEY yourself to a long random value.\n'
|
||||
'See https://docs.openwebui.com/reference/env-configuration#webui_secret_key'
|
||||
)
|
||||
|
||||
ENABLE_COMPRESSION_MIDDLEWARE = os.getenv('ENABLE_COMPRESSION_MIDDLEWARE', 'True').lower() == 'true'
|
||||
ENABLE_INITIAL_ADMIN_SIGNUP = os.environ.get('ENABLE_INITIAL_ADMIN_SIGNUP', 'False').lower() == 'true'
|
||||
ENABLE_SIGNUP_PASSWORD_CONFIRMATION = os.environ.get('ENABLE_SIGNUP_PASSWORD_CONFIRMATION', 'False').lower() == 'true'
|
||||
|
||||
####################################
|
||||
# Admin Account Runtime Creation
|
||||
@@ -712,25 +516,24 @@ ENABLE_COMPRESSION_MIDDLEWARE = os.getenv('ENABLE_COMPRESSION_MIDDLEWARE', 'True
|
||||
|
||||
# Optional env vars for creating an admin account on startup
|
||||
# Useful for headless/automated deployments
|
||||
WEBUI_ADMIN_EMAIL = os.getenv('WEBUI_ADMIN_EMAIL', '')
|
||||
WEBUI_ADMIN_PASSWORD = os.getenv('WEBUI_ADMIN_PASSWORD', '')
|
||||
WEBUI_ADMIN_NAME = os.getenv('WEBUI_ADMIN_NAME', 'Admin')
|
||||
WEBUI_ADMIN_EMAIL = os.environ.get('WEBUI_ADMIN_EMAIL', '')
|
||||
WEBUI_ADMIN_PASSWORD = os.environ.get('WEBUI_ADMIN_PASSWORD', '')
|
||||
WEBUI_ADMIN_NAME = os.environ.get('WEBUI_ADMIN_NAME', 'Admin')
|
||||
|
||||
WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.getenv('WEBUI_AUTH_TRUSTED_EMAIL_HEADER', None)
|
||||
WEBUI_AUTH_TRUSTED_NAME_HEADER = os.getenv('WEBUI_AUTH_TRUSTED_NAME_HEADER', None)
|
||||
WEBUI_AUTH_TRUSTED_GROUPS_HEADER = os.getenv('WEBUI_AUTH_TRUSTED_GROUPS_HEADER', None)
|
||||
WEBUI_AUTH_TRUSTED_ROLE_HEADER = os.getenv('WEBUI_AUTH_TRUSTED_ROLE_HEADER', None)
|
||||
WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get('WEBUI_AUTH_TRUSTED_EMAIL_HEADER', None)
|
||||
WEBUI_AUTH_TRUSTED_NAME_HEADER = os.environ.get('WEBUI_AUTH_TRUSTED_NAME_HEADER', None)
|
||||
WEBUI_AUTH_TRUSTED_GROUPS_HEADER = os.environ.get('WEBUI_AUTH_TRUSTED_GROUPS_HEADER', None)
|
||||
WEBUI_AUTH_TRUSTED_ROLE_HEADER = os.environ.get('WEBUI_AUTH_TRUSTED_ROLE_HEADER', None)
|
||||
|
||||
# Custom header name for API key authentication. Defaults to 'x-api-key'.
|
||||
# Useful when Open WebUI sits behind a reverse proxy / API gateway that
|
||||
# already uses the Authorization header for its own authentication — set
|
||||
# this to a unique header (e.g. 'X-OpenWebUI-Key') so the middleware
|
||||
# checks the custom header instead and avoids the 401 short-circuit.
|
||||
CUSTOM_API_KEY_HEADER = os.getenv('CUSTOM_API_KEY_HEADER', 'x-api-key')
|
||||
CUSTOM_API_KEY_HEADER = os.environ.get('CUSTOM_API_KEY_HEADER', 'x-api-key')
|
||||
|
||||
ENABLE_PASSWORD_VALIDATION = os.getenv('ENABLE_PASSWORD_VALIDATION', 'False').lower() == 'true'
|
||||
PASSWORD_HASH_ALGORITHM = os.getenv('PASSWORD_HASH_ALGORITHM', 'bcrypt').lower()
|
||||
PASSWORD_VALIDATION_REGEX_PATTERN = os.getenv(
|
||||
ENABLE_PASSWORD_VALIDATION = os.environ.get('ENABLE_PASSWORD_VALIDATION', 'False').lower() == 'true'
|
||||
PASSWORD_VALIDATION_REGEX_PATTERN = os.environ.get(
|
||||
'PASSWORD_VALIDATION_REGEX_PATTERN',
|
||||
r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$',
|
||||
)
|
||||
@@ -743,65 +546,83 @@ except Exception as e:
|
||||
log.error(f'Invalid PASSWORD_VALIDATION_REGEX_PATTERN: {e}')
|
||||
PASSWORD_VALIDATION_REGEX_PATTERN = re.compile(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$')
|
||||
|
||||
PASSWORD_VALIDATION_HINT = os.getenv('PASSWORD_VALIDATION_HINT', '')
|
||||
PASSWORD_VALIDATION_HINT = os.environ.get('PASSWORD_VALIDATION_HINT', '')
|
||||
|
||||
|
||||
BYPASS_MODEL_ACCESS_CONTROL = os.getenv('BYPASS_MODEL_ACCESS_CONTROL', 'False').lower() == 'true'
|
||||
BYPASS_RETRIEVAL_ACCESS_CONTROL = os.getenv('BYPASS_RETRIEVAL_ACCESS_CONTROL', 'False').lower() == 'true'
|
||||
|
||||
# When True, collection names that do not match any known file-*, user-memory-*,
|
||||
# web-search-*, or knowledge-base collection are allowed through access control
|
||||
# for non-admin users. When False (default), unknown collection names are
|
||||
# denied — closing the legacy unscoped namespace.
|
||||
ENABLE_RETRIEVAL_UNSCOPED_COLLECTIONS = os.getenv('ENABLE_RETRIEVAL_UNSCOPED_COLLECTIONS', 'False').lower() == 'true'
|
||||
MINERU_MAX_MARKDOWN_BYTES = (
|
||||
int(os.getenv('MINERU_MAX_MARKDOWN_BYTES')) if os.getenv('MINERU_MAX_MARKDOWN_BYTES') else None
|
||||
)
|
||||
BYPASS_MODEL_ACCESS_CONTROL = os.environ.get('BYPASS_MODEL_ACCESS_CONTROL', 'False').lower() == 'true'
|
||||
|
||||
# When enabled, skips pydub-based preprocessing (format conversion, compression,
|
||||
# and chunked splitting) before sending files to processing engines. Useful when
|
||||
# the upstream provider handles these steps or when ffmpeg is unavailable.
|
||||
BYPASS_PYDUB_PREPROCESSING = os.getenv('BYPASS_PYDUB_PREPROCESSING', 'False').lower() == 'true'
|
||||
BYPASS_PYDUB_PREPROCESSING = os.environ.get('BYPASS_PYDUB_PREPROCESSING', 'False').lower() == 'true'
|
||||
|
||||
# When disabled (default), the OpenAI catch-all proxy endpoint (/{path:path})
|
||||
# is blocked. Enable only if you need direct passthrough to upstream OpenAI-
|
||||
# compatible APIs for endpoints not natively handled by Open WebUI.
|
||||
ENABLE_OPENAI_API_PASSTHROUGH = os.getenv('ENABLE_OPENAI_API_PASSTHROUGH', 'False').lower() == 'true'
|
||||
ENABLE_OPENAI_API_PASSTHROUGH = os.environ.get('ENABLE_OPENAI_API_PASSTHROUGH', 'False').lower() == 'true'
|
||||
|
||||
WEBUI_AUTH_SIGNOUT_REDIRECT_URL = os.getenv('WEBUI_AUTH_SIGNOUT_REDIRECT_URL', None)
|
||||
WEBUI_AUTH_SIGNOUT_REDIRECT_URL = os.environ.get('WEBUI_AUTH_SIGNOUT_REDIRECT_URL', None)
|
||||
|
||||
####################################
|
||||
# WEBUI_SECRET_KEY
|
||||
####################################
|
||||
|
||||
WEBUI_SECRET_KEY = os.environ.get(
|
||||
'WEBUI_SECRET_KEY',
|
||||
os.environ.get('WEBUI_JWT_SECRET_KEY', 't0p-s3cr3t'), # DEPRECATED: remove at next major version
|
||||
)
|
||||
|
||||
WEBUI_SESSION_COOKIE_SAME_SITE = os.environ.get('WEBUI_SESSION_COOKIE_SAME_SITE', 'lax')
|
||||
|
||||
WEBUI_SESSION_COOKIE_SECURE = os.environ.get('WEBUI_SESSION_COOKIE_SECURE', 'false').lower() == 'true'
|
||||
|
||||
WEBUI_AUTH_COOKIE_SAME_SITE = os.environ.get('WEBUI_AUTH_COOKIE_SAME_SITE', WEBUI_SESSION_COOKIE_SAME_SITE)
|
||||
|
||||
WEBUI_AUTH_COOKIE_SECURE = (
|
||||
os.environ.get(
|
||||
'WEBUI_AUTH_COOKIE_SECURE',
|
||||
os.environ.get('WEBUI_SESSION_COOKIE_SECURE', 'false'),
|
||||
).lower()
|
||||
== 'true'
|
||||
)
|
||||
|
||||
if WEBUI_AUTH and WEBUI_SECRET_KEY == '':
|
||||
raise ValueError(ERROR_MESSAGES.ENV_VAR_NOT_FOUND)
|
||||
|
||||
ENABLE_COMPRESSION_MIDDLEWARE = os.environ.get('ENABLE_COMPRESSION_MIDDLEWARE', 'True').lower() == 'true'
|
||||
|
||||
####################################
|
||||
# OAUTH Configuration
|
||||
####################################
|
||||
ENABLE_OAUTH_EMAIL_FALLBACK = os.getenv('ENABLE_OAUTH_EMAIL_FALLBACK', 'False').lower() == 'true'
|
||||
ENABLE_OAUTH_EMAIL_FALLBACK = os.environ.get('ENABLE_OAUTH_EMAIL_FALLBACK', 'False').lower() == 'true'
|
||||
|
||||
ENABLE_OAUTH_ID_TOKEN_COOKIE = os.getenv('ENABLE_OAUTH_ID_TOKEN_COOKIE', 'True').lower() == 'true'
|
||||
ENABLE_OAUTH_ID_TOKEN_COOKIE = os.environ.get('ENABLE_OAUTH_ID_TOKEN_COOKIE', 'True').lower() == 'true'
|
||||
|
||||
OAUTH_CLIENT_INFO_ENCRYPTION_KEY = os.getenv('OAUTH_CLIENT_INFO_ENCRYPTION_KEY', WEBUI_SECRET_KEY)
|
||||
OAUTH_CLIENT_INFO_ENCRYPTION_KEY = os.environ.get('OAUTH_CLIENT_INFO_ENCRYPTION_KEY', WEBUI_SECRET_KEY)
|
||||
|
||||
OAUTH_SESSION_TOKEN_ENCRYPTION_KEY = os.getenv('OAUTH_SESSION_TOKEN_ENCRYPTION_KEY', WEBUI_SECRET_KEY)
|
||||
OAUTH_SESSION_TOKEN_ENCRYPTION_KEY = os.environ.get('OAUTH_SESSION_TOKEN_ENCRYPTION_KEY', WEBUI_SECRET_KEY)
|
||||
|
||||
# Maximum number of concurrent OAuth sessions per user per provider
|
||||
# This prevents unbounded session growth while allowing multi-device usage
|
||||
OAUTH_MAX_SESSIONS_PER_USER = int(os.getenv('OAUTH_MAX_SESSIONS_PER_USER', '10'))
|
||||
OAUTH_MAX_SESSIONS_PER_USER = int(os.environ.get('OAUTH_MAX_SESSIONS_PER_USER', '10'))
|
||||
|
||||
# Token Exchange Configuration
|
||||
# Allows external apps to exchange OAuth tokens for OpenWebUI tokens
|
||||
ENABLE_OAUTH_TOKEN_EXCHANGE = os.getenv('ENABLE_OAUTH_TOKEN_EXCHANGE', 'False').lower() == 'true'
|
||||
ENABLE_OAUTH_TOKEN_EXCHANGE = os.environ.get('ENABLE_OAUTH_TOKEN_EXCHANGE', 'False').lower() == 'true'
|
||||
|
||||
# Back-Channel Logout Configuration
|
||||
# When enabled, exposes POST /oauth/backchannel-logout for IdP-initiated logout
|
||||
# per OpenID Connect Back-Channel Logout 1.0 spec.
|
||||
# Requires Redis for JWT revocation.
|
||||
ENABLE_OAUTH_BACKCHANNEL_LOGOUT = os.getenv('ENABLE_OAUTH_BACKCHANNEL_LOGOUT', 'False').lower() == 'true'
|
||||
ENABLE_OAUTH_BACKCHANNEL_LOGOUT = os.environ.get('ENABLE_OAUTH_BACKCHANNEL_LOGOUT', 'False').lower() == 'true'
|
||||
|
||||
####################################
|
||||
# SCIM Configuration
|
||||
####################################
|
||||
|
||||
ENABLE_SCIM = os.getenv('ENABLE_SCIM', os.getenv('SCIM_ENABLED', 'False')).lower() == 'true'
|
||||
SCIM_TOKEN = os.getenv('SCIM_TOKEN', '')
|
||||
SCIM_AUTH_PROVIDER = os.getenv('SCIM_AUTH_PROVIDER', '')
|
||||
ENABLE_SCIM = os.environ.get('ENABLE_SCIM', os.environ.get('SCIM_ENABLED', 'False')).lower() == 'true'
|
||||
SCIM_TOKEN = os.environ.get('SCIM_TOKEN', '')
|
||||
SCIM_AUTH_PROVIDER = os.environ.get('SCIM_AUTH_PROVIDER', '')
|
||||
|
||||
if ENABLE_SCIM and not SCIM_AUTH_PROVIDER:
|
||||
log.warning(
|
||||
@@ -814,15 +635,15 @@ if ENABLE_SCIM and not SCIM_AUTH_PROVIDER:
|
||||
# LICENSE_KEY
|
||||
####################################
|
||||
|
||||
LICENSE_KEY = os.getenv('LICENSE_KEY', '')
|
||||
LICENSE_KEY = os.environ.get('LICENSE_KEY', '')
|
||||
|
||||
LICENSE_BLOB = None
|
||||
LICENSE_BLOB_PATH = os.getenv('LICENSE_BLOB_PATH', DATA_DIR / 'l.data')
|
||||
LICENSE_BLOB_PATH = os.environ.get('LICENSE_BLOB_PATH', DATA_DIR / 'l.data')
|
||||
if LICENSE_BLOB_PATH and os.path.exists(LICENSE_BLOB_PATH):
|
||||
with open(LICENSE_BLOB_PATH, 'rb') as f:
|
||||
LICENSE_BLOB = f.read()
|
||||
|
||||
LICENSE_PUBLIC_KEY = os.getenv('LICENSE_PUBLIC_KEY', '')
|
||||
LICENSE_PUBLIC_KEY = os.environ.get('LICENSE_PUBLIC_KEY', '')
|
||||
|
||||
pk = None
|
||||
if LICENSE_PUBLIC_KEY:
|
||||
@@ -831,92 +652,17 @@ if LICENSE_PUBLIC_KEY:
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
{LICENSE_PUBLIC_KEY}
|
||||
-----END PUBLIC KEY-----
|
||||
""".encode()
|
||||
""".encode('utf-8')
|
||||
)
|
||||
|
||||
|
||||
####################################
|
||||
# WEBUI Identity
|
||||
####################################
|
||||
|
||||
WEBUI_NAME = os.getenv('WEBUI_NAME', 'Open WebUI')
|
||||
if WEBUI_NAME != 'Open WebUI':
|
||||
WEBUI_NAME += ' (Open WebUI)'
|
||||
|
||||
WEBUI_FAVICON_URL = 'https://openwebui.com/favicon.png'
|
||||
WEBUI_BUILD_HASH = os.getenv('WEBUI_BUILD_HASH', 'dev-build')
|
||||
TRUSTED_SIGNATURE_KEY = os.getenv('TRUSTED_SIGNATURE_KEY', '')
|
||||
|
||||
####################################
|
||||
# Feature flags
|
||||
####################################
|
||||
|
||||
SAFE_MODE = os.getenv('SAFE_MODE', 'False').lower() == 'true'
|
||||
ENABLE_EASTER_EGGS = os.getenv('ENABLE_EASTER_EGGS', 'True').lower() == 'true'
|
||||
ENABLE_STAR_SESSIONS_MIDDLEWARE = os.getenv('ENABLE_STAR_SESSIONS_MIDDLEWARE', 'False').lower() == 'true'
|
||||
ENABLE_KB_EXEC = os.getenv('ENABLE_KB_EXEC', 'False').lower() == 'true'
|
||||
|
||||
ENABLE_PROFILE_IMAGE_URL_FORWARDING = os.getenv('ENABLE_PROFILE_IMAGE_URL_FORWARDING', 'True').lower() == 'true'
|
||||
PROFILE_IMAGE_ALLOWED_MIME_TYPES = frozenset(
|
||||
t.strip()
|
||||
for t in os.getenv(
|
||||
'PROFILE_IMAGE_ALLOWED_MIME_TYPES',
|
||||
'image/png,image/jpeg,image/gif,image/webp',
|
||||
).split(',')
|
||||
if t.strip()
|
||||
)
|
||||
|
||||
# Max stored length (bytes) of a data:image profile URI; bounds Postgres/Redis
|
||||
# bloat from inline avatars and model icons. Unset (default) disables the cap.
|
||||
_profile_image_max_data_uri_size = os.getenv('PROFILE_IMAGE_MAX_DATA_URI_SIZE', '').strip()
|
||||
PROFILE_IMAGE_MAX_DATA_URI_SIZE = int(_profile_image_max_data_uri_size) if _profile_image_max_data_uri_size else None
|
||||
|
||||
####################################
|
||||
# Forward Headers
|
||||
####################################
|
||||
|
||||
ENABLE_FORWARD_USER_INFO_HEADERS = os.getenv('ENABLE_FORWARD_USER_INFO_HEADERS', 'False').lower() == 'true'
|
||||
|
||||
FORWARD_USER_INFO_HEADER_USER_NAME = os.getenv('FORWARD_USER_INFO_HEADER_USER_NAME', 'X-OpenWebUI-User-Name')
|
||||
FORWARD_USER_INFO_HEADER_USER_ID = os.getenv('FORWARD_USER_INFO_HEADER_USER_ID', 'X-OpenWebUI-User-Id')
|
||||
FORWARD_USER_INFO_HEADER_USER_EMAIL = os.getenv('FORWARD_USER_INFO_HEADER_USER_EMAIL', 'X-OpenWebUI-User-Email')
|
||||
FORWARD_USER_INFO_HEADER_USER_ROLE = os.getenv('FORWARD_USER_INFO_HEADER_USER_ROLE', 'X-OpenWebUI-User-Role')
|
||||
FORWARD_SESSION_INFO_HEADER_MESSAGE_ID = os.getenv('FORWARD_SESSION_INFO_HEADER_MESSAGE_ID', 'X-OpenWebUI-Message-Id')
|
||||
FORWARD_SESSION_INFO_HEADER_CHAT_ID = os.getenv('FORWARD_SESSION_INFO_HEADER_CHAT_ID', 'X-OpenWebUI-Chat-Id')
|
||||
|
||||
# If set while ENABLE_FORWARD_USER_INFO_HEADERS is True, send one signed HS256 JWT
|
||||
# (FORWARD_USER_INFO_HEADER_JWT) instead of separate X-OpenWebUI-User-* headers.
|
||||
FORWARD_USER_INFO_HEADER_JWT_SECRET = (os.environ.get('FORWARD_USER_INFO_HEADER_JWT_SECRET') or '').strip() or None
|
||||
FORWARD_USER_INFO_HEADER_JWT = os.environ.get('FORWARD_USER_INFO_HEADER_JWT', 'X-OpenWebUI-User-Jwt')
|
||||
try:
|
||||
FORWARD_USER_INFO_HEADER_JWT_EXPIRES_SECONDS = int(
|
||||
os.environ.get('FORWARD_USER_INFO_HEADER_JWT_EXPIRES_SECONDS', '300')
|
||||
)
|
||||
except ValueError:
|
||||
FORWARD_USER_INFO_HEADER_JWT_EXPIRES_SECONDS = 300
|
||||
|
||||
####################################
|
||||
# Progressive Web App
|
||||
####################################
|
||||
|
||||
EXTERNAL_PWA_MANIFEST_URL = os.getenv('EXTERNAL_PWA_MANIFEST_URL', None)
|
||||
|
||||
####################################
|
||||
# GROUP DEFAULTS
|
||||
####################################
|
||||
|
||||
# Controls the default "Who can share to this group" setting for new groups.
|
||||
# Env var values: "true" (anyone), "false" (no one), "members" (only group members).
|
||||
_default_group_share = os.getenv('DEFAULT_GROUP_SHARE_PERMISSION', 'members').strip().lower()
|
||||
DEFAULT_GROUP_SHARE_PERMISSION = 'members' if _default_group_share == 'members' else _default_group_share == 'true'
|
||||
|
||||
####################################
|
||||
# MODELS
|
||||
####################################
|
||||
|
||||
ENABLE_CUSTOM_MODEL_FALLBACK = os.getenv('ENABLE_CUSTOM_MODEL_FALLBACK', 'False').lower() == 'true'
|
||||
ENABLE_CUSTOM_MODEL_FALLBACK = os.environ.get('ENABLE_CUSTOM_MODEL_FALLBACK', 'False').lower() == 'true'
|
||||
|
||||
MODELS_CACHE_TTL = os.getenv('MODELS_CACHE_TTL', '1')
|
||||
MODELS_CACHE_TTL = os.environ.get('MODELS_CACHE_TTL', '1')
|
||||
if MODELS_CACHE_TTL == '':
|
||||
MODELS_CACHE_TTL = None
|
||||
else:
|
||||
@@ -931,9 +677,8 @@ else:
|
||||
####################################
|
||||
|
||||
ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION = (
|
||||
os.getenv('ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION', 'False').lower() == 'true'
|
||||
os.environ.get('ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION', 'False').lower() == 'true'
|
||||
)
|
||||
ENABLE_API_OUTLET_FILTERS = os.getenv('ENABLE_API_OUTLET_FILTERS', 'True').lower() == 'true'
|
||||
|
||||
# When enabled, uses a hardcoded extension-to-MIME dictionary as a last-resort
|
||||
# fallback when both mimetypes.guess_type() and file.meta.content_type fail to
|
||||
@@ -941,10 +686,10 @@ ENABLE_API_OUTLET_FILTERS = os.getenv('ENABLE_API_OUTLET_FILTERS', 'True').lower
|
||||
# wolfi-base) that lack /etc/mime.types AND have legacy files without stored
|
||||
# content_type metadata.
|
||||
ENABLE_IMAGE_CONTENT_TYPE_EXTENSION_FALLBACK = (
|
||||
os.getenv('ENABLE_IMAGE_CONTENT_TYPE_EXTENSION_FALLBACK', 'False').lower() == 'true'
|
||||
os.environ.get('ENABLE_IMAGE_CONTENT_TYPE_EXTENSION_FALLBACK', 'False').lower() == 'true'
|
||||
)
|
||||
|
||||
CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = os.getenv('CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE', '1')
|
||||
CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = os.environ.get('CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE', '1')
|
||||
|
||||
if CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE == '':
|
||||
CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = 1
|
||||
@@ -955,34 +700,25 @@ else:
|
||||
CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = 1
|
||||
|
||||
|
||||
# Maximum tool-call iterations per chat response. Set to -1 for unlimited.
|
||||
# The old CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES name is accepted as a fallback.
|
||||
CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS = os.getenv(
|
||||
'CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS',
|
||||
os.getenv('CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES', '256'),
|
||||
)
|
||||
CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES = os.environ.get('CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES', '30')
|
||||
|
||||
if CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS == '':
|
||||
CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS = 256
|
||||
if CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES == '':
|
||||
CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES = 30
|
||||
else:
|
||||
try:
|
||||
CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS = int(CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS)
|
||||
CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES = int(CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES)
|
||||
except Exception:
|
||||
CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS = 256
|
||||
|
||||
# -1 means unlimited (no cap).
|
||||
if CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS == -1:
|
||||
CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS = None
|
||||
CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES = 30
|
||||
|
||||
|
||||
# WARNING: Experimental. Only enable if your upstream Responses API endpoint
|
||||
# supports stateful sessions (i.e. server-side response storage with
|
||||
# previous_response_id anchoring). Most proxies and third-party endpoints
|
||||
# are stateless and will break if this is enabled.
|
||||
ENABLE_RESPONSES_API_STATEFUL = os.getenv('ENABLE_RESPONSES_API_STATEFUL', 'False').lower() == 'true'
|
||||
ENABLE_RESPONSES_API_STATEFUL = os.environ.get('ENABLE_RESPONSES_API_STATEFUL', 'False').lower() == 'true'
|
||||
|
||||
|
||||
CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE = os.getenv('CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE', '')
|
||||
CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE = os.environ.get('CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE', '')
|
||||
|
||||
if CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE == '':
|
||||
CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE = None
|
||||
@@ -993,17 +729,181 @@ else:
|
||||
CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE = None
|
||||
|
||||
|
||||
####################################
|
||||
# WEBSOCKET SUPPORT
|
||||
####################################
|
||||
|
||||
ENABLE_WEBSOCKET_SUPPORT = os.environ.get('ENABLE_WEBSOCKET_SUPPORT', 'True').lower() == 'true'
|
||||
|
||||
|
||||
WEBSOCKET_MANAGER = os.environ.get('WEBSOCKET_MANAGER', '')
|
||||
|
||||
WEBSOCKET_REDIS_OPTIONS = os.environ.get('WEBSOCKET_REDIS_OPTIONS', '')
|
||||
|
||||
|
||||
if WEBSOCKET_REDIS_OPTIONS == '':
|
||||
if REDIS_SOCKET_CONNECT_TIMEOUT:
|
||||
WEBSOCKET_REDIS_OPTIONS = {'socket_connect_timeout': REDIS_SOCKET_CONNECT_TIMEOUT}
|
||||
else:
|
||||
log.debug('No WEBSOCKET_REDIS_OPTIONS provided, defaulting to None')
|
||||
WEBSOCKET_REDIS_OPTIONS = None
|
||||
else:
|
||||
try:
|
||||
WEBSOCKET_REDIS_OPTIONS = json.loads(WEBSOCKET_REDIS_OPTIONS)
|
||||
except Exception:
|
||||
log.warning('Invalid WEBSOCKET_REDIS_OPTIONS, defaulting to None')
|
||||
WEBSOCKET_REDIS_OPTIONS = None
|
||||
|
||||
WEBSOCKET_REDIS_URL = os.environ.get('WEBSOCKET_REDIS_URL', REDIS_URL)
|
||||
WEBSOCKET_REDIS_CLUSTER = os.environ.get('WEBSOCKET_REDIS_CLUSTER', str(REDIS_CLUSTER)).lower() == 'true'
|
||||
|
||||
websocket_redis_lock_timeout = os.environ.get('WEBSOCKET_REDIS_LOCK_TIMEOUT', '60')
|
||||
|
||||
try:
|
||||
WEBSOCKET_REDIS_LOCK_TIMEOUT = int(websocket_redis_lock_timeout)
|
||||
except ValueError:
|
||||
WEBSOCKET_REDIS_LOCK_TIMEOUT = 60
|
||||
|
||||
WEBSOCKET_SENTINEL_HOSTS = os.environ.get('WEBSOCKET_SENTINEL_HOSTS', '')
|
||||
WEBSOCKET_SENTINEL_PORT = os.environ.get('WEBSOCKET_SENTINEL_PORT', '26379')
|
||||
WEBSOCKET_SERVER_LOGGING = os.environ.get('WEBSOCKET_SERVER_LOGGING', 'False').lower() == 'true'
|
||||
WEBSOCKET_SERVER_ENGINEIO_LOGGING = (
|
||||
os.environ.get(
|
||||
'WEBSOCKET_SERVER_ENGINEIO_LOGGING',
|
||||
os.environ.get('WEBSOCKET_SERVER_LOGGING', 'False'),
|
||||
).lower()
|
||||
== 'true'
|
||||
)
|
||||
WEBSOCKET_SERVER_PING_TIMEOUT = os.environ.get('WEBSOCKET_SERVER_PING_TIMEOUT', '20')
|
||||
try:
|
||||
WEBSOCKET_SERVER_PING_TIMEOUT = int(WEBSOCKET_SERVER_PING_TIMEOUT)
|
||||
except ValueError:
|
||||
WEBSOCKET_SERVER_PING_TIMEOUT = 20
|
||||
|
||||
WEBSOCKET_SERVER_PING_INTERVAL = os.environ.get('WEBSOCKET_SERVER_PING_INTERVAL', '25')
|
||||
try:
|
||||
WEBSOCKET_SERVER_PING_INTERVAL = int(WEBSOCKET_SERVER_PING_INTERVAL)
|
||||
except ValueError:
|
||||
WEBSOCKET_SERVER_PING_INTERVAL = 25
|
||||
|
||||
WEBSOCKET_EVENT_CALLER_TIMEOUT = os.environ.get('WEBSOCKET_EVENT_CALLER_TIMEOUT', '')
|
||||
|
||||
if WEBSOCKET_EVENT_CALLER_TIMEOUT == '':
|
||||
WEBSOCKET_EVENT_CALLER_TIMEOUT = None
|
||||
else:
|
||||
try:
|
||||
WEBSOCKET_EVENT_CALLER_TIMEOUT = int(WEBSOCKET_EVENT_CALLER_TIMEOUT)
|
||||
except ValueError:
|
||||
WEBSOCKET_EVENT_CALLER_TIMEOUT = 300
|
||||
|
||||
|
||||
REQUESTS_VERIFY = os.environ.get('REQUESTS_VERIFY', 'True').lower() == 'true'
|
||||
|
||||
AIOHTTP_CLIENT_TIMEOUT = os.environ.get('AIOHTTP_CLIENT_TIMEOUT', '')
|
||||
|
||||
if AIOHTTP_CLIENT_TIMEOUT == '':
|
||||
AIOHTTP_CLIENT_TIMEOUT = None
|
||||
else:
|
||||
try:
|
||||
AIOHTTP_CLIENT_TIMEOUT = int(AIOHTTP_CLIENT_TIMEOUT)
|
||||
except Exception:
|
||||
AIOHTTP_CLIENT_TIMEOUT = 300
|
||||
|
||||
|
||||
AIOHTTP_CLIENT_SESSION_SSL = os.environ.get('AIOHTTP_CLIENT_SESSION_SSL', 'True').lower() == 'true'
|
||||
|
||||
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = os.environ.get(
|
||||
'AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST',
|
||||
os.environ.get('AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST', '10'),
|
||||
)
|
||||
|
||||
if AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST == '':
|
||||
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = None
|
||||
else:
|
||||
try:
|
||||
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = int(AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
|
||||
except Exception:
|
||||
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = 10
|
||||
|
||||
|
||||
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = os.environ.get('AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA', '10')
|
||||
|
||||
if AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA == '':
|
||||
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = None
|
||||
else:
|
||||
try:
|
||||
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = int(AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA)
|
||||
except Exception:
|
||||
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = 10
|
||||
|
||||
|
||||
AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL = (
|
||||
os.environ.get('AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL', 'True').lower() == 'true'
|
||||
)
|
||||
|
||||
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER = os.environ.get('AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER', '')
|
||||
|
||||
if AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER == '':
|
||||
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER = AIOHTTP_CLIENT_TIMEOUT
|
||||
else:
|
||||
try:
|
||||
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER = int(AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER)
|
||||
except Exception:
|
||||
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER = AIOHTTP_CLIENT_TIMEOUT
|
||||
|
||||
|
||||
####################################
|
||||
# AIOHTTP Connection Pool
|
||||
####################################
|
||||
|
||||
AIOHTTP_POOL_CONNECTIONS = os.environ.get('AIOHTTP_POOL_CONNECTIONS', '')
|
||||
if AIOHTTP_POOL_CONNECTIONS == '':
|
||||
AIOHTTP_POOL_CONNECTIONS = None
|
||||
else:
|
||||
try:
|
||||
AIOHTTP_POOL_CONNECTIONS = int(AIOHTTP_POOL_CONNECTIONS)
|
||||
except ValueError:
|
||||
AIOHTTP_POOL_CONNECTIONS = None
|
||||
|
||||
AIOHTTP_POOL_CONNECTIONS_PER_HOST = os.environ.get('AIOHTTP_POOL_CONNECTIONS_PER_HOST', '')
|
||||
if AIOHTTP_POOL_CONNECTIONS_PER_HOST == '':
|
||||
AIOHTTP_POOL_CONNECTIONS_PER_HOST = None
|
||||
else:
|
||||
try:
|
||||
AIOHTTP_POOL_CONNECTIONS_PER_HOST = int(AIOHTTP_POOL_CONNECTIONS_PER_HOST)
|
||||
except ValueError:
|
||||
AIOHTTP_POOL_CONNECTIONS_PER_HOST = None
|
||||
|
||||
AIOHTTP_POOL_DNS_TTL = os.environ.get('AIOHTTP_POOL_DNS_TTL', '300')
|
||||
try:
|
||||
AIOHTTP_POOL_DNS_TTL = int(AIOHTTP_POOL_DNS_TTL)
|
||||
if AIOHTTP_POOL_DNS_TTL < 0:
|
||||
AIOHTTP_POOL_DNS_TTL = 300
|
||||
except ValueError:
|
||||
AIOHTTP_POOL_DNS_TTL = 300
|
||||
|
||||
RAG_EMBEDDING_TIMEOUT = os.environ.get('RAG_EMBEDDING_TIMEOUT', '')
|
||||
|
||||
if RAG_EMBEDDING_TIMEOUT == '':
|
||||
RAG_EMBEDDING_TIMEOUT = None
|
||||
else:
|
||||
try:
|
||||
RAG_EMBEDDING_TIMEOUT = int(RAG_EMBEDDING_TIMEOUT)
|
||||
except Exception:
|
||||
RAG_EMBEDDING_TIMEOUT = None
|
||||
|
||||
|
||||
####################################
|
||||
# SENTENCE TRANSFORMERS
|
||||
####################################
|
||||
|
||||
|
||||
SENTENCE_TRANSFORMERS_BACKEND = os.getenv('SENTENCE_TRANSFORMERS_BACKEND', '')
|
||||
SENTENCE_TRANSFORMERS_BACKEND = os.environ.get('SENTENCE_TRANSFORMERS_BACKEND', '')
|
||||
if SENTENCE_TRANSFORMERS_BACKEND == '':
|
||||
SENTENCE_TRANSFORMERS_BACKEND = 'torch'
|
||||
|
||||
|
||||
SENTENCE_TRANSFORMERS_MODEL_KWARGS = os.getenv('SENTENCE_TRANSFORMERS_MODEL_KWARGS', '')
|
||||
SENTENCE_TRANSFORMERS_MODEL_KWARGS = os.environ.get('SENTENCE_TRANSFORMERS_MODEL_KWARGS', '')
|
||||
if SENTENCE_TRANSFORMERS_MODEL_KWARGS == '':
|
||||
SENTENCE_TRANSFORMERS_MODEL_KWARGS = None
|
||||
else:
|
||||
@@ -1013,12 +913,14 @@ else:
|
||||
SENTENCE_TRANSFORMERS_MODEL_KWARGS = None
|
||||
|
||||
|
||||
SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND = os.getenv('SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND', '')
|
||||
SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND = os.environ.get('SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND', '')
|
||||
if SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND == '':
|
||||
SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND = 'torch'
|
||||
|
||||
|
||||
SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = os.getenv('SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS', '')
|
||||
SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = os.environ.get(
|
||||
'SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS', ''
|
||||
)
|
||||
if SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS == '':
|
||||
SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = None
|
||||
else:
|
||||
@@ -1031,40 +933,22 @@ else:
|
||||
# When enabled (default), scores are normalized to 0-1 range for proper
|
||||
# relevance threshold behavior with MS MARCO models.
|
||||
SENTENCE_TRANSFORMERS_CROSS_ENCODER_SIGMOID_ACTIVATION_FUNCTION = (
|
||||
os.getenv('SENTENCE_TRANSFORMERS_CROSS_ENCODER_SIGMOID_ACTIVATION_FUNCTION', 'True').lower() == 'true'
|
||||
os.environ.get('SENTENCE_TRANSFORMERS_CROSS_ENCODER_SIGMOID_ACTIVATION_FUNCTION', 'True').lower() == 'true'
|
||||
)
|
||||
|
||||
####################################
|
||||
# TOOLS/FUNCTIONS PIP OPTIONS
|
||||
####################################
|
||||
|
||||
ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS = (
|
||||
os.getenv('ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS', 'True').lower() == 'true'
|
||||
)
|
||||
|
||||
PIP_OPTIONS = os.getenv('PIP_OPTIONS', '').split()
|
||||
PIP_PACKAGE_INDEX_OPTIONS = os.getenv('PIP_PACKAGE_INDEX_OPTIONS', '').split()
|
||||
|
||||
|
||||
####################################
|
||||
# OFFLINE_MODE
|
||||
####################################
|
||||
|
||||
ENABLE_VERSION_UPDATE_CHECK = os.getenv('ENABLE_VERSION_UPDATE_CHECK', 'true').lower() == 'true'
|
||||
OFFLINE_MODE = os.getenv('OFFLINE_MODE', 'false').lower() == 'true'
|
||||
ENABLE_VERSION_UPDATE_CHECK = os.environ.get('ENABLE_VERSION_UPDATE_CHECK', 'true').lower() == 'true'
|
||||
OFFLINE_MODE = os.environ.get('OFFLINE_MODE', 'false').lower() == 'true'
|
||||
|
||||
if OFFLINE_MODE:
|
||||
os.environ['HF_HUB_OFFLINE'] = '1'
|
||||
ENABLE_VERSION_UPDATE_CHECK = False
|
||||
|
||||
####################################
|
||||
# Pyodide file persistence
|
||||
####################################
|
||||
|
||||
ENABLE_PYODIDE_FILE_PERSISTENCE = os.getenv('ENABLE_PYODIDE_FILE_PERSISTENCE', 'false').lower() == 'true'
|
||||
|
||||
####################################
|
||||
# Audit logging
|
||||
# AUDIT LOGGING
|
||||
####################################
|
||||
|
||||
|
||||
@@ -1086,7 +970,7 @@ AUDIT_UVICORN_LOGGER_NAMES = os.getenv('AUDIT_UVICORN_LOGGER_NAMES', 'uvicorn.ac
|
||||
# METADATA | REQUEST | REQUEST_RESPONSE
|
||||
AUDIT_LOG_LEVEL = os.getenv('AUDIT_LOG_LEVEL', 'NONE').upper()
|
||||
try:
|
||||
MAX_BODY_LOG_SIZE = int(os.getenv('MAX_BODY_LOG_SIZE') or 2048)
|
||||
MAX_BODY_LOG_SIZE = int(os.environ.get('MAX_BODY_LOG_SIZE') or 2048)
|
||||
except ValueError:
|
||||
MAX_BODY_LOG_SIZE = 2048
|
||||
|
||||
@@ -1109,39 +993,66 @@ ENABLE_AUDIT_GET_REQUESTS = os.getenv('ENABLE_AUDIT_GET_REQUESTS', 'False').lowe
|
||||
# OPENTELEMETRY
|
||||
####################################
|
||||
|
||||
ENABLE_OTEL = os.getenv('ENABLE_OTEL', 'False').lower() == 'true'
|
||||
ENABLE_OTEL_TRACES = os.getenv('ENABLE_OTEL_TRACES', 'False').lower() == 'true'
|
||||
ENABLE_OTEL_METRICS = os.getenv('ENABLE_OTEL_METRICS', 'False').lower() == 'true'
|
||||
ENABLE_OTEL_LOGS = os.getenv('ENABLE_OTEL_LOGS', 'False').lower() == 'true'
|
||||
ENABLE_OTEL = os.environ.get('ENABLE_OTEL', 'False').lower() == 'true'
|
||||
ENABLE_OTEL_TRACES = os.environ.get('ENABLE_OTEL_TRACES', 'False').lower() == 'true'
|
||||
ENABLE_OTEL_METRICS = os.environ.get('ENABLE_OTEL_METRICS', 'False').lower() == 'true'
|
||||
ENABLE_OTEL_LOGS = os.environ.get('ENABLE_OTEL_LOGS', 'False').lower() == 'true'
|
||||
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT = os.getenv('OTEL_EXPORTER_OTLP_ENDPOINT', 'http://localhost:4317')
|
||||
OTEL_METRICS_EXPORTER_OTLP_ENDPOINT = os.getenv('OTEL_METRICS_EXPORTER_OTLP_ENDPOINT', OTEL_EXPORTER_OTLP_ENDPOINT)
|
||||
OTEL_LOGS_EXPORTER_OTLP_ENDPOINT = os.getenv('OTEL_LOGS_EXPORTER_OTLP_ENDPOINT', OTEL_EXPORTER_OTLP_ENDPOINT)
|
||||
OTEL_EXPORTER_OTLP_INSECURE = os.getenv('OTEL_EXPORTER_OTLP_INSECURE', 'False').lower() == 'true'
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT = os.environ.get('OTEL_EXPORTER_OTLP_ENDPOINT', 'http://localhost:4317')
|
||||
OTEL_METRICS_EXPORTER_OTLP_ENDPOINT = os.environ.get('OTEL_METRICS_EXPORTER_OTLP_ENDPOINT', OTEL_EXPORTER_OTLP_ENDPOINT)
|
||||
OTEL_LOGS_EXPORTER_OTLP_ENDPOINT = os.environ.get('OTEL_LOGS_EXPORTER_OTLP_ENDPOINT', OTEL_EXPORTER_OTLP_ENDPOINT)
|
||||
OTEL_EXPORTER_OTLP_INSECURE = os.environ.get('OTEL_EXPORTER_OTLP_INSECURE', 'False').lower() == 'true'
|
||||
OTEL_METRICS_EXPORTER_OTLP_INSECURE = (
|
||||
os.getenv('OTEL_METRICS_EXPORTER_OTLP_INSECURE', str(OTEL_EXPORTER_OTLP_INSECURE)).lower() == 'true'
|
||||
os.environ.get('OTEL_METRICS_EXPORTER_OTLP_INSECURE', str(OTEL_EXPORTER_OTLP_INSECURE)).lower() == 'true'
|
||||
)
|
||||
OTEL_LOGS_EXPORTER_OTLP_INSECURE = (
|
||||
os.getenv('OTEL_LOGS_EXPORTER_OTLP_INSECURE', str(OTEL_EXPORTER_OTLP_INSECURE)).lower() == 'true'
|
||||
os.environ.get('OTEL_LOGS_EXPORTER_OTLP_INSECURE', str(OTEL_EXPORTER_OTLP_INSECURE)).lower() == 'true'
|
||||
)
|
||||
OTEL_SERVICE_NAME = os.getenv('OTEL_SERVICE_NAME', 'open-webui')
|
||||
OTEL_RESOURCE_ATTRIBUTES = os.getenv('OTEL_RESOURCE_ATTRIBUTES', '') # e.g. key1=val1,key2=val2
|
||||
OTEL_TRACES_SAMPLER = os.getenv('OTEL_TRACES_SAMPLER', 'parentbased_always_on').lower()
|
||||
OTEL_BASIC_AUTH_USERNAME = os.getenv('OTEL_BASIC_AUTH_USERNAME', '')
|
||||
OTEL_BASIC_AUTH_PASSWORD = os.getenv('OTEL_BASIC_AUTH_PASSWORD', '')
|
||||
OTEL_METRICS_EXPORT_INTERVAL_MILLIS = int(os.getenv('OTEL_METRICS_EXPORT_INTERVAL_MILLIS', '10000'))
|
||||
OTEL_SERVICE_NAME = os.environ.get('OTEL_SERVICE_NAME', 'open-webui')
|
||||
OTEL_RESOURCE_ATTRIBUTES = os.environ.get('OTEL_RESOURCE_ATTRIBUTES', '') # e.g. key1=val1,key2=val2
|
||||
OTEL_TRACES_SAMPLER = os.environ.get('OTEL_TRACES_SAMPLER', 'parentbased_always_on').lower()
|
||||
OTEL_BASIC_AUTH_USERNAME = os.environ.get('OTEL_BASIC_AUTH_USERNAME', '')
|
||||
OTEL_BASIC_AUTH_PASSWORD = os.environ.get('OTEL_BASIC_AUTH_PASSWORD', '')
|
||||
OTEL_METRICS_EXPORT_INTERVAL_MILLIS = int(os.environ.get('OTEL_METRICS_EXPORT_INTERVAL_MILLIS', '10000'))
|
||||
|
||||
OTEL_METRICS_BASIC_AUTH_USERNAME = os.getenv('OTEL_METRICS_BASIC_AUTH_USERNAME', OTEL_BASIC_AUTH_USERNAME)
|
||||
OTEL_METRICS_BASIC_AUTH_PASSWORD = os.getenv('OTEL_METRICS_BASIC_AUTH_PASSWORD', OTEL_BASIC_AUTH_PASSWORD)
|
||||
OTEL_LOGS_BASIC_AUTH_USERNAME = os.getenv('OTEL_LOGS_BASIC_AUTH_USERNAME', OTEL_BASIC_AUTH_USERNAME)
|
||||
OTEL_LOGS_BASIC_AUTH_PASSWORD = os.getenv('OTEL_LOGS_BASIC_AUTH_PASSWORD', OTEL_BASIC_AUTH_PASSWORD)
|
||||
OTEL_METRICS_BASIC_AUTH_USERNAME = os.environ.get('OTEL_METRICS_BASIC_AUTH_USERNAME', OTEL_BASIC_AUTH_USERNAME)
|
||||
OTEL_METRICS_BASIC_AUTH_PASSWORD = os.environ.get('OTEL_METRICS_BASIC_AUTH_PASSWORD', OTEL_BASIC_AUTH_PASSWORD)
|
||||
OTEL_LOGS_BASIC_AUTH_USERNAME = os.environ.get('OTEL_LOGS_BASIC_AUTH_USERNAME', OTEL_BASIC_AUTH_USERNAME)
|
||||
OTEL_LOGS_BASIC_AUTH_PASSWORD = os.environ.get('OTEL_LOGS_BASIC_AUTH_PASSWORD', OTEL_BASIC_AUTH_PASSWORD)
|
||||
|
||||
OTEL_OTLP_SPAN_EXPORTER = os.getenv('OTEL_OTLP_SPAN_EXPORTER', 'grpc').lower() # grpc or http
|
||||
OTEL_OTLP_SPAN_EXPORTER = os.environ.get('OTEL_OTLP_SPAN_EXPORTER', 'grpc').lower() # grpc or http
|
||||
|
||||
OTEL_METRICS_OTLP_SPAN_EXPORTER = os.getenv(
|
||||
OTEL_METRICS_OTLP_SPAN_EXPORTER = os.environ.get(
|
||||
'OTEL_METRICS_OTLP_SPAN_EXPORTER', OTEL_OTLP_SPAN_EXPORTER
|
||||
).lower() # grpc or http
|
||||
|
||||
OTEL_LOGS_OTLP_SPAN_EXPORTER = os.getenv(
|
||||
OTEL_LOGS_OTLP_SPAN_EXPORTER = os.environ.get(
|
||||
'OTEL_LOGS_OTLP_SPAN_EXPORTER', OTEL_OTLP_SPAN_EXPORTER
|
||||
).lower() # grpc or http
|
||||
|
||||
####################################
|
||||
# TOOLS/FUNCTIONS PIP OPTIONS
|
||||
####################################
|
||||
|
||||
ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS = (
|
||||
os.environ.get('ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS', 'True').lower() == 'true'
|
||||
)
|
||||
|
||||
PIP_OPTIONS = os.getenv('PIP_OPTIONS', '').split()
|
||||
PIP_PACKAGE_INDEX_OPTIONS = os.getenv('PIP_PACKAGE_INDEX_OPTIONS', '').split()
|
||||
|
||||
|
||||
####################################
|
||||
# PROGRESSIVE WEB APP OPTIONS
|
||||
####################################
|
||||
|
||||
EXTERNAL_PWA_MANIFEST_URL = os.environ.get('EXTERNAL_PWA_MANIFEST_URL')
|
||||
|
||||
####################################
|
||||
# GROUP DEFAULTS
|
||||
####################################
|
||||
|
||||
# Controls the default "Who can share to this group" setting for new groups.
|
||||
# Env var values: "true" (anyone), "false" (no one), "members" (only group members).
|
||||
_default_group_share = os.environ.get('DEFAULT_GROUP_SHARE_PERMISSION', 'members').strip().lower()
|
||||
DEFAULT_GROUP_SHARE_PERMISSION = 'members' if _default_group_share == 'members' else _default_group_share == 'true'
|
||||
|
||||
@@ -1,1172 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import inspect
|
||||
import logging
|
||||
import time
|
||||
import uuid
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
from open_webui.env import VERSION
|
||||
from open_webui.models.config import Config
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
from open_webui.retrieval.web.utils import validate_url
|
||||
from open_webui.utils.webhook import post_webhook
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
MAX_STRING_LENGTH = 1000
|
||||
EVENT_WEBHOOKS_CONFIG_KEY = 'events.webhooks'
|
||||
LEGACY_WEBHOOK_CONFIG_KEY = 'webhook_url'
|
||||
DEFAULT_WEBHOOK_ID = 'default'
|
||||
|
||||
|
||||
class EventDefinition(BaseModel):
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
name: str
|
||||
description: str | None = None
|
||||
message: str | None = None
|
||||
|
||||
@model_validator(mode='after')
|
||||
def defaults(self) -> 'EventDefinition':
|
||||
title = self.name.replace('.', ' ').replace('_', ' ').title()
|
||||
if self.description is None:
|
||||
object.__setattr__(self, 'description', f'{title}.')
|
||||
if self.message is None:
|
||||
object.__setattr__(self, 'message', title)
|
||||
return self
|
||||
|
||||
|
||||
class EventDefinitions(BaseModel):
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
SYSTEM_STARTUP_STARTED: EventDefinition = EventDefinition(
|
||||
name='system.startup.started', description='Application startup began.', message='Startup started'
|
||||
)
|
||||
SYSTEM_STARTUP_COMPLETED: EventDefinition = EventDefinition(
|
||||
name='system.startup.completed', description='Application startup completed.', message='Startup completed'
|
||||
)
|
||||
SYSTEM_SHUTDOWN_STARTED: EventDefinition = EventDefinition(
|
||||
name='system.shutdown.started', description='Application shutdown began.', message='Shutdown started'
|
||||
)
|
||||
SYSTEM_SHUTDOWN_COMPLETED: EventDefinition = EventDefinition(
|
||||
name='system.shutdown.completed', description='Application shutdown completed.', message='Shutdown completed'
|
||||
)
|
||||
CONFIG_IMPORTED: EventDefinition = EventDefinition(
|
||||
name='config.imported', description='Configuration was imported.', message='Config imported'
|
||||
)
|
||||
CONFIG_UPDATED: EventDefinition = EventDefinition(
|
||||
name='config.updated', description='Configuration was updated.', message='Config updated'
|
||||
)
|
||||
CONFIG_WEBHOOK_UPDATED: EventDefinition = EventDefinition(
|
||||
name='config.webhook.updated',
|
||||
description='Event webhook configuration was updated.',
|
||||
message='Webhook configuration updated',
|
||||
)
|
||||
CONFIG_CONNECTIONS_UPDATED: EventDefinition = EventDefinition(
|
||||
name='config.connections.updated',
|
||||
description='Connection configuration was updated.',
|
||||
message='Config Connections updated',
|
||||
)
|
||||
CONFIG_TOOL_SERVERS_UPDATED: EventDefinition = EventDefinition(
|
||||
name='config.tool_servers.updated',
|
||||
description='Tool server configuration was updated.',
|
||||
message='Config Tool Servers updated',
|
||||
)
|
||||
CONFIG_TERMINAL_SERVERS_UPDATED: EventDefinition = EventDefinition(
|
||||
name='config.terminal_servers.updated',
|
||||
description='Terminal server configuration was updated.',
|
||||
message='Config Terminal Servers updated',
|
||||
)
|
||||
CONFIG_CODE_EXECUTION_UPDATED: EventDefinition = EventDefinition(
|
||||
name='config.code_execution.updated',
|
||||
description='Code execution configuration was updated.',
|
||||
message='Config Code Execution updated',
|
||||
)
|
||||
CONFIG_MODELS_UPDATED: EventDefinition = EventDefinition(
|
||||
name='config.models.updated', description='Model configuration was updated.', message='Config Models updated'
|
||||
)
|
||||
CONFIG_BANNERS_UPDATED: EventDefinition = EventDefinition(
|
||||
name='config.banners.updated', description='Banner configuration was updated.', message='Config Banners updated'
|
||||
)
|
||||
CONFIG_SUGGESTIONS_UPDATED: EventDefinition = EventDefinition(
|
||||
name='config.suggestions.updated',
|
||||
description='Suggestion configuration was updated.',
|
||||
message='Config Suggestions updated',
|
||||
)
|
||||
AUTH_SIGNUP: EventDefinition = EventDefinition(
|
||||
name='auth.signup', description='A user account was created through signup.', message='User signed up'
|
||||
)
|
||||
AUTH_LOGIN: EventDefinition = EventDefinition(
|
||||
name='auth.login', description='A user successfully logged in.', message='User logged in'
|
||||
)
|
||||
AUTH_LOGOUT: EventDefinition = EventDefinition(
|
||||
name='auth.logout', description='A user logged out.', message='User logged out'
|
||||
)
|
||||
AUTH_PASSWORD_CHANGED: EventDefinition = EventDefinition(
|
||||
name='auth.password_changed', description='A user password was changed.', message='Password changed'
|
||||
)
|
||||
AUTH_API_KEY_CREATED: EventDefinition = EventDefinition(
|
||||
name='auth.api_key.created', description='A user API key was created.', message='API key created'
|
||||
)
|
||||
AUTH_API_KEY_DELETED: EventDefinition = EventDefinition(
|
||||
name='auth.api_key.deleted', description='A user API key was deleted.', message='API key deleted'
|
||||
)
|
||||
AUTH_OAUTH_SESSION_DELETED: EventDefinition = EventDefinition(
|
||||
name='auth.oauth_session.deleted', description='An OAuth session was deleted.', message='OAuth session deleted'
|
||||
)
|
||||
USER_CREATED: EventDefinition = EventDefinition(
|
||||
name='user.created', description='A user account was created.', message='User created'
|
||||
)
|
||||
USER_UPDATED: EventDefinition = EventDefinition(
|
||||
name='user.updated', description='A user account was updated.', message='User updated'
|
||||
)
|
||||
USER_DELETED: EventDefinition = EventDefinition(
|
||||
name='user.deleted', description='A user account was deleted.', message='User deleted'
|
||||
)
|
||||
USER_ROLE_UPDATED: EventDefinition = EventDefinition(
|
||||
name='user.role_updated', description='A user role was updated.', message='User role updated'
|
||||
)
|
||||
USER_STATUS_UPDATED: EventDefinition = EventDefinition(
|
||||
name='user.status_updated', description='A user status was updated.', message='User status updated'
|
||||
)
|
||||
USER_SETTINGS_UPDATED: EventDefinition = EventDefinition(
|
||||
name='user.settings_updated', description='A user settings object was updated.', message='User settings updated'
|
||||
)
|
||||
USER_PROFILE_UPDATED: EventDefinition = EventDefinition(
|
||||
name='user.profile_updated', description='A user profile was updated.', message='User profile updated'
|
||||
)
|
||||
USER_PERMISSIONS_UPDATED: EventDefinition = EventDefinition(
|
||||
name='user.permissions_updated',
|
||||
description='A user permissions object was updated.',
|
||||
message='User permissions updated',
|
||||
)
|
||||
GROUP_CREATED: EventDefinition = EventDefinition(
|
||||
name='group.created', description='A group was created.', message='Group created'
|
||||
)
|
||||
GROUP_UPDATED: EventDefinition = EventDefinition(
|
||||
name='group.updated', description='A group was updated.', message='Group updated'
|
||||
)
|
||||
GROUP_DELETED: EventDefinition = EventDefinition(
|
||||
name='group.deleted', description='A group was deleted.', message='Group deleted'
|
||||
)
|
||||
GROUP_MEMBER_ADDED: EventDefinition = EventDefinition(
|
||||
name='group.member_added', description='A user was added to a group.', message='Group member added'
|
||||
)
|
||||
GROUP_MEMBER_REMOVED: EventDefinition = EventDefinition(
|
||||
name='group.member_removed', description='A user was removed from a group.', message='Group member removed'
|
||||
)
|
||||
CHAT_CREATED: EventDefinition = EventDefinition(
|
||||
name='chat.created', description='A chat was created.', message='Chat created'
|
||||
)
|
||||
CHAT_IMPORTED: EventDefinition = EventDefinition(
|
||||
name='chat.imported', description='A chat was imported.', message='Chat imported'
|
||||
)
|
||||
CHAT_UPDATED: EventDefinition = EventDefinition(
|
||||
name='chat.updated', description='A chat was updated.', message='Chat updated'
|
||||
)
|
||||
CHAT_DELETED: EventDefinition = EventDefinition(
|
||||
name='chat.deleted', description='A chat was deleted.', message='Chat deleted'
|
||||
)
|
||||
CHAT_DELETED_ALL: EventDefinition = EventDefinition(
|
||||
name='chat.deleted_all', description='All chats for a scope were deleted.', message='Chat deleted all'
|
||||
)
|
||||
CHAT_COMPACTED: EventDefinition = EventDefinition(
|
||||
name='chat.compacted', description='A chat was compacted.', message='Chat compacted'
|
||||
)
|
||||
CHAT_PINNED: EventDefinition = EventDefinition(
|
||||
name='chat.pinned', description='A chat was pinned.', message='Chat pinned'
|
||||
)
|
||||
CHAT_UNPINNED: EventDefinition = EventDefinition(
|
||||
name='chat.unpinned', description='A chat was unpinned.', message='Chat unpinned'
|
||||
)
|
||||
CHAT_CLONED: EventDefinition = EventDefinition(
|
||||
name='chat.cloned', description='A chat was cloned.', message='Chat cloned'
|
||||
)
|
||||
CHAT_ARCHIVED: EventDefinition = EventDefinition(
|
||||
name='chat.archived', description='A chat was archived.', message='Chat archived'
|
||||
)
|
||||
CHAT_UNARCHIVED: EventDefinition = EventDefinition(
|
||||
name='chat.unarchived', description='A chat was unarchived.', message='Chat unarchived'
|
||||
)
|
||||
CHAT_SHARED: EventDefinition = EventDefinition(
|
||||
name='chat.shared', description='A chat was shared.', message='Chat shared'
|
||||
)
|
||||
CHAT_UNSHARED: EventDefinition = EventDefinition(
|
||||
name='chat.unshared', description='A chat was unshared.', message='Chat unshared'
|
||||
)
|
||||
CHAT_FOLDER_UPDATED: EventDefinition = EventDefinition(
|
||||
name='chat.folder_updated', description='A chat folder assignment was updated.', message='Chat folder updated'
|
||||
)
|
||||
CHAT_TAG_ADDED: EventDefinition = EventDefinition(
|
||||
name='chat.tag_added', description='A tag was added to a chat.', message='Chat tag added'
|
||||
)
|
||||
CHAT_TAG_REMOVED: EventDefinition = EventDefinition(
|
||||
name='chat.tag_removed', description='A tag was removed from a chat.', message='Chat tag removed'
|
||||
)
|
||||
MESSAGE_CREATED: EventDefinition = EventDefinition(
|
||||
name='message.created', description='A message was created.', message='Message created'
|
||||
)
|
||||
MESSAGE_UPDATED: EventDefinition = EventDefinition(
|
||||
name='message.updated', description='A message was updated.', message='Message updated'
|
||||
)
|
||||
MESSAGE_DELETED: EventDefinition = EventDefinition(
|
||||
name='message.deleted', description='A message was deleted.', message='Message deleted'
|
||||
)
|
||||
MESSAGE_EVENT_RECEIVED: EventDefinition = EventDefinition(
|
||||
name='message.event_received',
|
||||
description='A message-level event was received.',
|
||||
message='Message event received',
|
||||
)
|
||||
MESSAGE_REACTION_ADDED: EventDefinition = EventDefinition(
|
||||
name='message.reaction_added',
|
||||
description='A reaction was added to a message.',
|
||||
message='Message reaction added',
|
||||
)
|
||||
MESSAGE_REACTION_REMOVED: EventDefinition = EventDefinition(
|
||||
name='message.reaction_removed',
|
||||
description='A reaction was removed from a message.',
|
||||
message='Message reaction removed',
|
||||
)
|
||||
MESSAGE_PINNED: EventDefinition = EventDefinition(
|
||||
name='message.pinned', description='A message was pinned.', message='Message pinned'
|
||||
)
|
||||
MESSAGE_UNPINNED: EventDefinition = EventDefinition(
|
||||
name='message.unpinned', description='A message was unpinned.', message='Message unpinned'
|
||||
)
|
||||
CHANNEL_CREATED: EventDefinition = EventDefinition(
|
||||
name='channel.created', description='A channel was created.', message='Channel created'
|
||||
)
|
||||
CHANNEL_UPDATED: EventDefinition = EventDefinition(
|
||||
name='channel.updated', description='A channel was updated.', message='Channel updated'
|
||||
)
|
||||
CHANNEL_DELETED: EventDefinition = EventDefinition(
|
||||
name='channel.deleted', description='A channel was deleted.', message='Channel deleted'
|
||||
)
|
||||
CHANNEL_MEMBER_ADDED: EventDefinition = EventDefinition(
|
||||
name='channel.member_added', description='A member was added to a channel.', message='Channel member added'
|
||||
)
|
||||
CHANNEL_MEMBER_REMOVED: EventDefinition = EventDefinition(
|
||||
name='channel.member_removed',
|
||||
description='A member was removed from a channel.',
|
||||
message='Channel member removed',
|
||||
)
|
||||
CHANNEL_MEMBER_ACTIVE_UPDATED: EventDefinition = EventDefinition(
|
||||
name='channel.member_active_updated',
|
||||
description='A channel member active state was updated.',
|
||||
message='Channel member active updated',
|
||||
)
|
||||
CHANNEL_WEBHOOK_CREATED: EventDefinition = EventDefinition(
|
||||
name='channel.webhook.created',
|
||||
description='A channel incoming webhook was created.',
|
||||
message='Channel Webhook created',
|
||||
)
|
||||
CHANNEL_WEBHOOK_UPDATED: EventDefinition = EventDefinition(
|
||||
name='channel.webhook.updated',
|
||||
description='A channel incoming webhook was updated.',
|
||||
message='Channel Webhook updated',
|
||||
)
|
||||
CHANNEL_WEBHOOK_DELETED: EventDefinition = EventDefinition(
|
||||
name='channel.webhook.deleted',
|
||||
description='A channel incoming webhook was deleted.',
|
||||
message='Channel Webhook deleted',
|
||||
)
|
||||
FILE_UPLOADED: EventDefinition = EventDefinition(
|
||||
name='file.uploaded', description='A file was uploaded.', message='File uploaded'
|
||||
)
|
||||
FILE_CONTENT_UPDATED: EventDefinition = EventDefinition(
|
||||
name='file.content_updated', description='File content was updated.', message='File content updated'
|
||||
)
|
||||
FILE_RENAMED: EventDefinition = EventDefinition(
|
||||
name='file.renamed', description='A file was renamed.', message='File renamed'
|
||||
)
|
||||
FILE_DELETED: EventDefinition = EventDefinition(
|
||||
name='file.deleted', description='A file was deleted.', message='File deleted'
|
||||
)
|
||||
FILE_DELETED_ALL: EventDefinition = EventDefinition(
|
||||
name='file.deleted_all', description='All files for a scope were deleted.', message='File deleted all'
|
||||
)
|
||||
FOLDER_CREATED: EventDefinition = EventDefinition(
|
||||
name='folder.created', description='A folder was created.', message='Folder created'
|
||||
)
|
||||
FOLDER_UPDATED: EventDefinition = EventDefinition(
|
||||
name='folder.updated', description='A folder was updated.', message='Folder updated'
|
||||
)
|
||||
FOLDER_PARENT_UPDATED: EventDefinition = EventDefinition(
|
||||
name='folder.parent_updated', description='A folder parent was updated.', message='Folder parent updated'
|
||||
)
|
||||
FOLDER_ACCESS_UPDATED: EventDefinition = EventDefinition(
|
||||
name='folder.access_updated', description='Folder access was updated.', message='Folder access updated'
|
||||
)
|
||||
FOLDER_DELETED: EventDefinition = EventDefinition(
|
||||
name='folder.deleted', description='A folder was deleted.', message='Folder deleted'
|
||||
)
|
||||
NOTE_CREATED: EventDefinition = EventDefinition(
|
||||
name='note.created', description='A note was created.', message='Note created'
|
||||
)
|
||||
NOTE_UPDATED: EventDefinition = EventDefinition(
|
||||
name='note.updated', description='A note was updated.', message='Note updated'
|
||||
)
|
||||
NOTE_ACCESS_UPDATED: EventDefinition = EventDefinition(
|
||||
name='note.access_updated', description='Note access was updated.', message='Note access updated'
|
||||
)
|
||||
NOTE_PINNED: EventDefinition = EventDefinition(
|
||||
name='note.pinned', description='A note was pinned.', message='Note pinned'
|
||||
)
|
||||
NOTE_UNPINNED: EventDefinition = EventDefinition(
|
||||
name='note.unpinned', description='A note was unpinned.', message='Note unpinned'
|
||||
)
|
||||
NOTE_DELETED: EventDefinition = EventDefinition(
|
||||
name='note.deleted', description='A note was deleted.', message='Note deleted'
|
||||
)
|
||||
MEMORY_CREATED: EventDefinition = EventDefinition(
|
||||
name='memory.created', description='A memory was created.', message='Memory created'
|
||||
)
|
||||
MEMORY_UPDATED: EventDefinition = EventDefinition(
|
||||
name='memory.updated', description='A memory was updated.', message='Memory updated'
|
||||
)
|
||||
MEMORY_DELETED: EventDefinition = EventDefinition(
|
||||
name='memory.deleted', description='A memory was deleted.', message='Memory deleted'
|
||||
)
|
||||
MEMORY_RESET: EventDefinition = EventDefinition(
|
||||
name='memory.reset', description='A memory was reset.', message='Memory reset'
|
||||
)
|
||||
KNOWLEDGE_CREATED: EventDefinition = EventDefinition(
|
||||
name='knowledge.created', description='A knowledge was created.', message='Knowledge created'
|
||||
)
|
||||
KNOWLEDGE_UPDATED: EventDefinition = EventDefinition(
|
||||
name='knowledge.updated', description='A knowledge was updated.', message='Knowledge updated'
|
||||
)
|
||||
KNOWLEDGE_DELETED: EventDefinition = EventDefinition(
|
||||
name='knowledge.deleted', description='A knowledge was deleted.', message='Knowledge deleted'
|
||||
)
|
||||
KNOWLEDGE_RESET: EventDefinition = EventDefinition(
|
||||
name='knowledge.reset', description='A knowledge was reset.', message='Knowledge reset'
|
||||
)
|
||||
KNOWLEDGE_REINDEXED: EventDefinition = EventDefinition(
|
||||
name='knowledge.reindexed', description='A knowledge was reindexed.', message='Knowledge reindexed'
|
||||
)
|
||||
KNOWLEDGE_ACCESS_UPDATED: EventDefinition = EventDefinition(
|
||||
name='knowledge.access_updated', description='Knowledge access was updated.', message='Knowledge access updated'
|
||||
)
|
||||
KNOWLEDGE_FILE_ADDED: EventDefinition = EventDefinition(
|
||||
name='knowledge.file.added', description='A file was added to a knowledge base.', message='Knowledge File added'
|
||||
)
|
||||
KNOWLEDGE_FILE_UPDATED: EventDefinition = EventDefinition(
|
||||
name='knowledge.file.updated', description='A knowledge file was updated.', message='Knowledge File updated'
|
||||
)
|
||||
KNOWLEDGE_FILE_REMOVED: EventDefinition = EventDefinition(
|
||||
name='knowledge.file.removed',
|
||||
description='A file was removed from a knowledge base.',
|
||||
message='Knowledge File removed',
|
||||
)
|
||||
KNOWLEDGE_FILE_MOVED: EventDefinition = EventDefinition(
|
||||
name='knowledge.file.moved', description='A knowledge file was moved.', message='Knowledge File moved'
|
||||
)
|
||||
KNOWLEDGE_DIRECTORY_CREATED: EventDefinition = EventDefinition(
|
||||
name='knowledge.directory.created',
|
||||
description='A knowledge directory was created.',
|
||||
message='Knowledge Directory created',
|
||||
)
|
||||
KNOWLEDGE_DIRECTORY_UPDATED: EventDefinition = EventDefinition(
|
||||
name='knowledge.directory.updated',
|
||||
description='A knowledge directory was updated.',
|
||||
message='Knowledge Directory updated',
|
||||
)
|
||||
KNOWLEDGE_DIRECTORY_DELETED: EventDefinition = EventDefinition(
|
||||
name='knowledge.directory.deleted',
|
||||
description='A knowledge directory was deleted.',
|
||||
message='Knowledge Directory deleted',
|
||||
)
|
||||
KNOWLEDGE_EXTERNAL_CONNECTION_CREATED: EventDefinition = EventDefinition(
|
||||
name='knowledge.external_connection.created',
|
||||
description='A knowledge external connection was created.',
|
||||
message='Knowledge External Connection created',
|
||||
)
|
||||
KNOWLEDGE_EXTERNAL_CONNECTION_UPDATED: EventDefinition = EventDefinition(
|
||||
name='knowledge.external_connection.updated',
|
||||
description='A knowledge external connection was updated.',
|
||||
message='Knowledge External Connection updated',
|
||||
)
|
||||
KNOWLEDGE_EXTERNAL_CONNECTION_DELETED: EventDefinition = EventDefinition(
|
||||
name='knowledge.external_connection.deleted',
|
||||
description='A knowledge external connection was deleted.',
|
||||
message='Knowledge External Connection deleted',
|
||||
)
|
||||
RETRIEVAL_CONTENT_PROCESSED: EventDefinition = EventDefinition(
|
||||
name='retrieval.content.processed',
|
||||
description='Retrieval content was processed.',
|
||||
message='Retrieval Content processed',
|
||||
)
|
||||
RETRIEVAL_COLLECTION_DELETED: EventDefinition = EventDefinition(
|
||||
name='retrieval.collection.deleted',
|
||||
description='A retrieval collection was deleted.',
|
||||
message='Retrieval Collection deleted',
|
||||
)
|
||||
RETRIEVAL_VECTOR_DB_RESET: EventDefinition = EventDefinition(
|
||||
name='retrieval.vector_db.reset',
|
||||
description='The retrieval vector database was reset.',
|
||||
message='Retrieval Vector Db reset',
|
||||
)
|
||||
RETRIEVAL_UPLOADS_RESET: EventDefinition = EventDefinition(
|
||||
name='retrieval.uploads.reset', description='Retrieval uploads were reset.', message='Retrieval Uploads reset'
|
||||
)
|
||||
MODEL_CREATED: EventDefinition = EventDefinition(
|
||||
name='model.created', description='A model was created.', message='Model created'
|
||||
)
|
||||
MODEL_IMPORTED: EventDefinition = EventDefinition(
|
||||
name='model.imported', description='A model was imported.', message='Model imported'
|
||||
)
|
||||
MODEL_SYNCED: EventDefinition = EventDefinition(
|
||||
name='model.synced', description='A model was synced.', message='Model synced'
|
||||
)
|
||||
MODEL_UPDATED: EventDefinition = EventDefinition(
|
||||
name='model.updated', description='A model was updated.', message='Model updated'
|
||||
)
|
||||
MODEL_DELETED: EventDefinition = EventDefinition(
|
||||
name='model.deleted', description='A model was deleted.', message='Model deleted'
|
||||
)
|
||||
MODEL_ENABLED: EventDefinition = EventDefinition(
|
||||
name='model.enabled', description='A model was enabled.', message='Model enabled'
|
||||
)
|
||||
MODEL_DISABLED: EventDefinition = EventDefinition(
|
||||
name='model.disabled', description='A model was disabled.', message='Model disabled'
|
||||
)
|
||||
MODEL_ACCESS_UPDATED: EventDefinition = EventDefinition(
|
||||
name='model.access_updated', description='Model access was updated.', message='Model access updated'
|
||||
)
|
||||
MODEL_PROVIDER_CONFIG_UPDATED: EventDefinition = EventDefinition(
|
||||
name='model.provider_config.updated',
|
||||
description='Model provider configuration was updated.',
|
||||
message='Model Provider Config updated',
|
||||
)
|
||||
MODEL_PROVIDER_REQUEST_FAILED: EventDefinition = EventDefinition(
|
||||
name='model.provider_request.failed',
|
||||
description='A model provider request failed.',
|
||||
message='Model provider request failed',
|
||||
)
|
||||
MODEL_PROVIDER_MODEL_CREATED: EventDefinition = EventDefinition(
|
||||
name='model.provider_model.created',
|
||||
description='A provider model was created.',
|
||||
message='Provider model created',
|
||||
)
|
||||
MODEL_PROVIDER_MODEL_DELETED: EventDefinition = EventDefinition(
|
||||
name='model.provider_model.deleted',
|
||||
description='A provider model was deleted.',
|
||||
message='Provider model deleted',
|
||||
)
|
||||
FUNCTION_CREATED: EventDefinition = EventDefinition(
|
||||
name='function.created', description='A function was created.', message='Function created'
|
||||
)
|
||||
FUNCTION_UPDATED: EventDefinition = EventDefinition(
|
||||
name='function.updated', description='A function was updated.', message='Function updated'
|
||||
)
|
||||
FUNCTION_DELETED: EventDefinition = EventDefinition(
|
||||
name='function.deleted', description='A function was deleted.', message='Function deleted'
|
||||
)
|
||||
FUNCTION_ENABLED: EventDefinition = EventDefinition(
|
||||
name='function.enabled', description='A function was enabled.', message='Function enabled'
|
||||
)
|
||||
FUNCTION_DISABLED: EventDefinition = EventDefinition(
|
||||
name='function.disabled', description='A function was disabled.', message='Function disabled'
|
||||
)
|
||||
FUNCTION_VALVES_UPDATED: EventDefinition = EventDefinition(
|
||||
name='function.valves_updated', description='Function valves were updated.', message='Function valves updated'
|
||||
)
|
||||
TOOL_CREATED: EventDefinition = EventDefinition(
|
||||
name='tool.created', description='A tool was created.', message='Tool created'
|
||||
)
|
||||
TOOL_UPDATED: EventDefinition = EventDefinition(
|
||||
name='tool.updated', description='A tool was updated.', message='Tool updated'
|
||||
)
|
||||
TOOL_DELETED: EventDefinition = EventDefinition(
|
||||
name='tool.deleted', description='A tool was deleted.', message='Tool deleted'
|
||||
)
|
||||
TOOL_ACCESS_UPDATED: EventDefinition = EventDefinition(
|
||||
name='tool.access_updated', description='Tool access was updated.', message='Tool access updated'
|
||||
)
|
||||
TOOL_VALVES_UPDATED: EventDefinition = EventDefinition(
|
||||
name='tool.valves_updated', description='Tool valves were updated.', message='Tool valves updated'
|
||||
)
|
||||
SKILL_CREATED: EventDefinition = EventDefinition(
|
||||
name='skill.created', description='A skill was created.', message='Skill created'
|
||||
)
|
||||
SKILL_UPDATED: EventDefinition = EventDefinition(
|
||||
name='skill.updated', description='A skill was updated.', message='Skill updated'
|
||||
)
|
||||
SKILL_DELETED: EventDefinition = EventDefinition(
|
||||
name='skill.deleted', description='A skill was deleted.', message='Skill deleted'
|
||||
)
|
||||
SKILL_ENABLED: EventDefinition = EventDefinition(
|
||||
name='skill.enabled', description='A skill was enabled.', message='Skill enabled'
|
||||
)
|
||||
SKILL_DISABLED: EventDefinition = EventDefinition(
|
||||
name='skill.disabled', description='A skill was disabled.', message='Skill disabled'
|
||||
)
|
||||
PROMPT_CREATED: EventDefinition = EventDefinition(
|
||||
name='prompt.created', description='A prompt was created.', message='Prompt created'
|
||||
)
|
||||
PROMPT_UPDATED: EventDefinition = EventDefinition(
|
||||
name='prompt.updated', description='A prompt was updated.', message='Prompt updated'
|
||||
)
|
||||
PROMPT_DELETED: EventDefinition = EventDefinition(
|
||||
name='prompt.deleted', description='A prompt was deleted.', message='Prompt deleted'
|
||||
)
|
||||
PROMPT_ENABLED: EventDefinition = EventDefinition(
|
||||
name='prompt.enabled', description='A prompt was enabled.', message='Prompt enabled'
|
||||
)
|
||||
PROMPT_DISABLED: EventDefinition = EventDefinition(
|
||||
name='prompt.disabled', description='A prompt was disabled.', message='Prompt disabled'
|
||||
)
|
||||
PROMPT_VERSION_UPDATED: EventDefinition = EventDefinition(
|
||||
name='prompt.version_updated', description='A prompt version was updated.', message='Prompt version updated'
|
||||
)
|
||||
PROMPT_ACCESS_UPDATED: EventDefinition = EventDefinition(
|
||||
name='prompt.access_updated', description='Prompt access was updated.', message='Prompt access updated'
|
||||
)
|
||||
PIPELINE_UPLOADED: EventDefinition = EventDefinition(
|
||||
name='pipeline.uploaded', description='A pipeline was uploaded.', message='Pipeline uploaded'
|
||||
)
|
||||
PIPELINE_ADDED: EventDefinition = EventDefinition(
|
||||
name='pipeline.added', description='A pipeline was added.', message='Pipeline added'
|
||||
)
|
||||
PIPELINE_DELETED: EventDefinition = EventDefinition(
|
||||
name='pipeline.deleted', description='A pipeline was deleted.', message='Pipeline deleted'
|
||||
)
|
||||
PIPELINE_VALVES_UPDATED: EventDefinition = EventDefinition(
|
||||
name='pipeline.valves_updated', description='Pipeline valves were updated.', message='Pipeline valves updated'
|
||||
)
|
||||
CALENDAR_CREATED: EventDefinition = EventDefinition(
|
||||
name='calendar.created', description='A calendar was created.', message='Calendar created'
|
||||
)
|
||||
CALENDAR_UPDATED: EventDefinition = EventDefinition(
|
||||
name='calendar.updated', description='A calendar was updated.', message='Calendar updated'
|
||||
)
|
||||
CALENDAR_DELETED: EventDefinition = EventDefinition(
|
||||
name='calendar.deleted', description='A calendar was deleted.', message='Calendar deleted'
|
||||
)
|
||||
CALENDAR_DEFAULT_UPDATED: EventDefinition = EventDefinition(
|
||||
name='calendar.default_updated',
|
||||
description='The default calendar was updated.',
|
||||
message='Calendar default updated',
|
||||
)
|
||||
CALENDAR_EVENT_CREATED: EventDefinition = EventDefinition(
|
||||
name='calendar.event.created', description='A calendar event was created.', message='Calendar Event created'
|
||||
)
|
||||
CALENDAR_EVENT_UPDATED: EventDefinition = EventDefinition(
|
||||
name='calendar.event.updated', description='A calendar event was updated.', message='Calendar Event updated'
|
||||
)
|
||||
CALENDAR_EVENT_DELETED: EventDefinition = EventDefinition(
|
||||
name='calendar.event.deleted', description='A calendar event was deleted.', message='Calendar Event deleted'
|
||||
)
|
||||
CALENDAR_EVENT_RSVP_UPDATED: EventDefinition = EventDefinition(
|
||||
name='calendar.event.rsvp_updated',
|
||||
description='A calendar event RSVP was updated.',
|
||||
message='Calendar Event rsvp updated',
|
||||
)
|
||||
AUTOMATION_CREATED: EventDefinition = EventDefinition(
|
||||
name='automation.created', description='An automation was created.', message='Automation created'
|
||||
)
|
||||
AUTOMATION_UPDATED: EventDefinition = EventDefinition(
|
||||
name='automation.updated', description='An automation was updated.', message='Automation updated'
|
||||
)
|
||||
AUTOMATION_ENABLED: EventDefinition = EventDefinition(
|
||||
name='automation.enabled', description='An automation was enabled.', message='Automation enabled'
|
||||
)
|
||||
AUTOMATION_DISABLED: EventDefinition = EventDefinition(
|
||||
name='automation.disabled', description='An automation was disabled.', message='Automation disabled'
|
||||
)
|
||||
AUTOMATION_DELETED: EventDefinition = EventDefinition(
|
||||
name='automation.deleted', description='An automation was deleted.', message='Automation deleted'
|
||||
)
|
||||
AUTOMATION_RUN_STARTED: EventDefinition = EventDefinition(
|
||||
name='automation.run_started', description='An automation run started.', message='Automation run started'
|
||||
)
|
||||
AUTOMATION_RUN_COMPLETED: EventDefinition = EventDefinition(
|
||||
name='automation.run_completed', description='An automation run completed.', message='Automation run completed'
|
||||
)
|
||||
AUTOMATION_RUN_FAILED: EventDefinition = EventDefinition(
|
||||
name='automation.run_failed', description='An automation run failed.', message='Automation run failed'
|
||||
)
|
||||
FEEDBACK_CREATED: EventDefinition = EventDefinition(
|
||||
name='feedback.created', description='A feedback was created.', message='Feedback created'
|
||||
)
|
||||
FEEDBACK_UPDATED: EventDefinition = EventDefinition(
|
||||
name='feedback.updated', description='A feedback was updated.', message='Feedback updated'
|
||||
)
|
||||
FEEDBACK_DELETED: EventDefinition = EventDefinition(
|
||||
name='feedback.deleted', description='A feedback was deleted.', message='Feedback deleted'
|
||||
)
|
||||
FEEDBACK_DELETED_ALL: EventDefinition = EventDefinition(
|
||||
name='feedback.deleted_all', description='All feedback for a scope was deleted.', message='Feedback deleted all'
|
||||
)
|
||||
IMAGE_GENERATED: EventDefinition = EventDefinition(
|
||||
name='image.generated', description='An image was generated.', message='Image generated'
|
||||
)
|
||||
IMAGE_EDITED: EventDefinition = EventDefinition(
|
||||
name='image.edited', description='An image was edited.', message='Image edited'
|
||||
)
|
||||
AUDIO_SPEECH_REQUESTED: EventDefinition = EventDefinition(
|
||||
name='audio.speech_requested', description='Speech generation was requested.', message='Speech requested'
|
||||
)
|
||||
AUDIO_TRANSCRIPTION_REQUESTED: EventDefinition = EventDefinition(
|
||||
name='audio.transcription_requested',
|
||||
description='Audio transcription was requested.',
|
||||
message='Transcription requested',
|
||||
)
|
||||
TERMINAL_SESSION_OPENED: EventDefinition = EventDefinition(
|
||||
name='terminal.session.opened', description='A terminal session was opened.', message='Terminal Session opened'
|
||||
)
|
||||
TERMINAL_SESSION_CLOSED: EventDefinition = EventDefinition(
|
||||
name='terminal.session.closed', description='A terminal session was closed.', message='Terminal Session closed'
|
||||
)
|
||||
|
||||
|
||||
EVENTS = EventDefinitions()
|
||||
EVENT_DEFINITIONS = tuple(getattr(EVENTS, field_name) for field_name in EventDefinitions.model_fields)
|
||||
EVENT_DEFINITIONS_BY_NAME = {definition.name: definition for definition in EVENT_DEFINITIONS}
|
||||
EVENT_CATALOG = tuple(definition.name for definition in EVENT_DEFINITIONS)
|
||||
EVENT_CATALOG_SET = set(EVENT_CATALOG)
|
||||
|
||||
|
||||
def get_event_catalog() -> list[dict[str, str]]:
|
||||
return [
|
||||
{
|
||||
'event': definition.name,
|
||||
'description': definition.description,
|
||||
'message': definition.message,
|
||||
}
|
||||
for definition in EVENT_DEFINITIONS
|
||||
]
|
||||
|
||||
|
||||
SENSITIVE_KEYS = {
|
||||
'password',
|
||||
'hashed_password',
|
||||
'token',
|
||||
'access_token',
|
||||
'refresh_token',
|
||||
'id_token',
|
||||
'api_key',
|
||||
'secret',
|
||||
'key',
|
||||
'authorization',
|
||||
'cookie',
|
||||
'webhook_token',
|
||||
}
|
||||
|
||||
SAFE_ACTOR_FIELDS = ('id', 'name', 'email', 'role', 'created_at', 'updated_at')
|
||||
|
||||
|
||||
def normalize_event_webhook(webhook: dict[str, Any], *, create: bool = False) -> dict[str, Any]:
|
||||
now = int(time.time())
|
||||
webhook_id = str(webhook.get('id') or uuid.uuid4())
|
||||
url = str(webhook.get('url') or '').strip()
|
||||
|
||||
events = [str(event).strip() for event in (webhook.get('events') or ['*']) if str(event).strip()]
|
||||
events = events or ['*']
|
||||
for event_filter in events:
|
||||
if event_filter == '*':
|
||||
continue
|
||||
if event_filter.endswith('.*'):
|
||||
prefix = event_filter[:-2]
|
||||
if prefix and any(event.startswith(f'{prefix}.') for event in EVENT_CATALOG):
|
||||
continue
|
||||
raise ValueError(f'Invalid event pattern: {event_filter}')
|
||||
if event_filter not in EVENT_CATALOG_SET:
|
||||
raise ValueError(f'Invalid event: {event_filter}')
|
||||
|
||||
targets = normalize_event_targets(webhook.get('targets'))
|
||||
|
||||
return {
|
||||
'id': webhook_id,
|
||||
'name': str(webhook.get('name') or ('Default webhook' if webhook_id == DEFAULT_WEBHOOK_ID else 'Webhook')),
|
||||
'url': url,
|
||||
'enabled': bool(webhook.get('enabled', True)),
|
||||
'events': events,
|
||||
'targets': targets,
|
||||
'created_at': int(webhook.get('created_at') or now),
|
||||
'updated_at': now if create or webhook.get('updated_at') is None else int(webhook.get('updated_at') or now),
|
||||
}
|
||||
|
||||
|
||||
def normalize_event_targets(targets: Any) -> list[dict[str, str]] | None:
|
||||
if targets is None:
|
||||
return None
|
||||
if not isinstance(targets, list):
|
||||
raise ValueError('Invalid targets')
|
||||
|
||||
normalized = []
|
||||
seen = set()
|
||||
for target in targets:
|
||||
if not isinstance(target, dict):
|
||||
raise ValueError('Invalid target')
|
||||
|
||||
target_type = str(target.get('type') or '').strip()
|
||||
target_id = str(target.get('id') or '').strip()
|
||||
if target_type not in {'user', 'group'} or not target_id:
|
||||
raise ValueError('Invalid target')
|
||||
|
||||
key = (target_type, target_id)
|
||||
if key in seen:
|
||||
continue
|
||||
|
||||
normalized.append({'type': target_type, 'id': target_id})
|
||||
seen.add(key)
|
||||
|
||||
return normalized
|
||||
|
||||
|
||||
def event_filter_matches(webhook: dict[str, Any], event_name: str) -> bool:
|
||||
if not webhook.get('enabled', True):
|
||||
return False
|
||||
|
||||
for event_filter in webhook.get('events') or ['*']:
|
||||
if event_filter == '*':
|
||||
return True
|
||||
if event_filter.endswith('.*') and event_name.startswith(f'{event_filter[:-2]}.'):
|
||||
return True
|
||||
if event_name == event_filter:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def event_user_ids(event: 'Event') -> set[str]:
|
||||
user_ids = set()
|
||||
actor = event.actor or {}
|
||||
subject = event.subject or {}
|
||||
data = event.data or {}
|
||||
|
||||
if actor.get('id'):
|
||||
user_ids.add(str(actor['id']))
|
||||
|
||||
if subject.get('type') == 'user' and subject.get('id'):
|
||||
user_ids.add(str(subject['id']))
|
||||
|
||||
if data.get('user_id'):
|
||||
user_ids.add(str(data['user_id']))
|
||||
|
||||
for user_id in data.get('user_ids') or []:
|
||||
if user_id:
|
||||
user_ids.add(str(user_id))
|
||||
|
||||
return user_ids
|
||||
|
||||
|
||||
async def event_target_matches(
|
||||
targets: list[dict[str, str]] | None,
|
||||
user_ids: set[str],
|
||||
user_group_ids: dict[str, set[str]] | None = None,
|
||||
) -> bool:
|
||||
if targets is None:
|
||||
return True
|
||||
if not targets:
|
||||
return not user_ids
|
||||
if not user_ids:
|
||||
return False
|
||||
|
||||
target_user_ids = {target['id'] for target in targets if target.get('type') == 'user'}
|
||||
if target_user_ids.intersection(user_ids):
|
||||
return True
|
||||
|
||||
target_group_ids = {target['id'] for target in targets if target.get('type') == 'group'}
|
||||
if not target_group_ids:
|
||||
return False
|
||||
|
||||
if user_group_ids is None:
|
||||
from open_webui.models.groups import Groups
|
||||
|
||||
groups_by_user = await Groups.get_groups_by_member_ids(list(user_ids))
|
||||
user_group_ids = {user_id: {group.id for group in groups} for user_id, groups in groups_by_user.items()}
|
||||
|
||||
return any(group_ids.intersection(target_group_ids) for group_ids in user_group_ids.values())
|
||||
|
||||
|
||||
async def event_webhook_matches(webhook: dict[str, Any], event: 'Event') -> bool:
|
||||
if not event_filter_matches(webhook, event.event):
|
||||
return False
|
||||
|
||||
return await event_target_matches(webhook.get('targets'), event_user_ids(event))
|
||||
|
||||
|
||||
async def get_event_webhooks() -> list[dict[str, Any]]:
|
||||
webhooks = await Config.get(EVENT_WEBHOOKS_CONFIG_KEY, []) or []
|
||||
if not isinstance(webhooks, list):
|
||||
return []
|
||||
|
||||
normalized = []
|
||||
for webhook in webhooks:
|
||||
if not isinstance(webhook, dict):
|
||||
continue
|
||||
try:
|
||||
normalized.append(normalize_event_webhook(webhook))
|
||||
except ValueError:
|
||||
log.exception('Invalid event webhook config skipped')
|
||||
return normalized
|
||||
|
||||
|
||||
async def migrate_legacy_webhook_config() -> list[dict[str, Any]]:
|
||||
webhooks = await get_event_webhooks()
|
||||
if any(webhook.get('id') == DEFAULT_WEBHOOK_ID for webhook in webhooks):
|
||||
return webhooks
|
||||
|
||||
now = int(time.time())
|
||||
legacy_url = await Config.get(LEGACY_WEBHOOK_CONFIG_KEY) or ''
|
||||
if not legacy_url:
|
||||
return webhooks
|
||||
|
||||
webhooks = [
|
||||
{
|
||||
'id': DEFAULT_WEBHOOK_ID,
|
||||
'name': 'Default webhook',
|
||||
'url': legacy_url,
|
||||
'enabled': True,
|
||||
'events': ['*'],
|
||||
'targets': None,
|
||||
'created_at': now,
|
||||
'updated_at': now,
|
||||
},
|
||||
*webhooks,
|
||||
]
|
||||
await Config.upsert({EVENT_WEBHOOKS_CONFIG_KEY: webhooks})
|
||||
return webhooks
|
||||
|
||||
|
||||
async def upsert_event_webhook(webhook: dict[str, Any]) -> dict[str, Any]:
|
||||
webhooks = await get_event_webhooks()
|
||||
url = str(webhook.get('url') or '').strip()
|
||||
if url:
|
||||
validate_url(url)
|
||||
|
||||
normalized = normalize_event_webhook(webhook, create=True)
|
||||
replaced = False
|
||||
next_webhooks = []
|
||||
|
||||
for existing in webhooks:
|
||||
if existing.get('id') == normalized['id']:
|
||||
next_webhooks.append(
|
||||
{
|
||||
**existing,
|
||||
**normalized,
|
||||
'created_at': existing.get('created_at') or normalized['created_at'],
|
||||
}
|
||||
)
|
||||
replaced = True
|
||||
else:
|
||||
next_webhooks.append(existing)
|
||||
|
||||
if not replaced:
|
||||
next_webhooks.append(normalized)
|
||||
|
||||
await Config.upsert({EVENT_WEBHOOKS_CONFIG_KEY: next_webhooks})
|
||||
return next(webhook for webhook in next_webhooks if webhook.get('id') == normalized['id'])
|
||||
|
||||
|
||||
async def delete_event_webhook(webhook_id: str) -> bool:
|
||||
webhooks = await get_event_webhooks()
|
||||
next_webhooks = [webhook for webhook in webhooks if webhook.get('id') != webhook_id]
|
||||
if len(next_webhooks) == len(webhooks):
|
||||
return False
|
||||
|
||||
values = {EVENT_WEBHOOKS_CONFIG_KEY: next_webhooks}
|
||||
if webhook_id == DEFAULT_WEBHOOK_ID:
|
||||
values[LEGACY_WEBHOOK_CONFIG_KEY] = ''
|
||||
|
||||
await Config.upsert(values)
|
||||
return True
|
||||
|
||||
|
||||
class Event(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
schema_: str = Field(alias='schema')
|
||||
id: str
|
||||
event: str
|
||||
resource: str
|
||||
operation: str
|
||||
created_at: int
|
||||
instance_id: str | None
|
||||
version: str
|
||||
source: str
|
||||
actor: dict[str, Any] | None = None
|
||||
subject: dict[str, Any] | None = None
|
||||
data: dict[str, Any] = Field(default_factory=dict)
|
||||
message: str | None = None
|
||||
|
||||
def model_dump(self, *args, **kwargs) -> dict[str, Any]:
|
||||
kwargs.setdefault('by_alias', True)
|
||||
return super().model_dump(*args, **kwargs)
|
||||
|
||||
|
||||
def _sensitive(key: Any) -> bool:
|
||||
normalized = str(key).lower().replace('-', '_')
|
||||
return (
|
||||
normalized in SENSITIVE_KEYS
|
||||
or normalized.endswith('_token')
|
||||
or normalized.endswith('_secret')
|
||||
or normalized.endswith('_api_key')
|
||||
or normalized.endswith('_key')
|
||||
)
|
||||
|
||||
|
||||
def _sanitize(value: Any) -> Any:
|
||||
if hasattr(value, 'model_dump'):
|
||||
value = value.model_dump()
|
||||
|
||||
if isinstance(value, dict):
|
||||
return {key: _sanitize(item) for key, item in value.items() if not _sensitive(key)}
|
||||
|
||||
if isinstance(value, (list, tuple, set)):
|
||||
return [_sanitize(item) for item in value]
|
||||
|
||||
if isinstance(value, str) and len(value) > MAX_STRING_LENGTH:
|
||||
return f'{value[:MAX_STRING_LENGTH]}...'
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def _actor(actor: Any | None) -> dict[str, Any] | None:
|
||||
actor = _sanitize(actor)
|
||||
if not actor:
|
||||
return None
|
||||
|
||||
get = actor.get if isinstance(actor, dict) else lambda key: getattr(actor, key, None)
|
||||
data = {field: get(field) for field in SAFE_ACTOR_FIELDS if get(field) is not None}
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data['type'] = get('type') or 'user'
|
||||
return data
|
||||
|
||||
|
||||
def event_name(event: EventDefinition | str) -> str:
|
||||
name = event.name if isinstance(event, EventDefinition) else str(event)
|
||||
if name not in EVENT_CATALOG_SET:
|
||||
raise ValueError(f'Unknown event: {name}')
|
||||
return name
|
||||
|
||||
|
||||
def build_event(
|
||||
request_or_app: Any,
|
||||
event: EventDefinition | str,
|
||||
*,
|
||||
actor: Any | None = None,
|
||||
subject_id: Any | None = None,
|
||||
subject_type: str | None = None,
|
||||
source: str = 'api',
|
||||
data: dict | None = None,
|
||||
message: str | None = None,
|
||||
) -> Event:
|
||||
event_name_value = event_name(event)
|
||||
app = getattr(request_or_app, 'app', request_or_app)
|
||||
parts = event_name_value.split('.')
|
||||
resource = '.'.join(parts[:-1])
|
||||
instance_id = getattr(getattr(app, 'state', None), 'instance_id', None)
|
||||
subject = (
|
||||
{'type': subject_type or resource, 'id': subject_id}
|
||||
if subject_id is not None or subject_type is not None
|
||||
else None
|
||||
)
|
||||
|
||||
return Event(
|
||||
schema=VERSION,
|
||||
id=str(uuid.uuid4()),
|
||||
event=event_name_value,
|
||||
resource=resource,
|
||||
operation=parts[-1],
|
||||
created_at=int(time.time()),
|
||||
instance_id=instance_id,
|
||||
version=VERSION,
|
||||
source=source,
|
||||
actor=_actor(actor),
|
||||
subject=_sanitize(subject) if subject else None,
|
||||
data=_sanitize(data or {}),
|
||||
message=message,
|
||||
)
|
||||
|
||||
|
||||
async def dispatch_webhook_event(app: Any, event: Event) -> None:
|
||||
name = getattr(getattr(app, 'state', None), 'WEBUI_NAME', 'Open WebUI')
|
||||
subject = event.subject or {}
|
||||
subject_id = subject.get('id')
|
||||
definition = EVENT_DEFINITIONS_BY_NAME.get(event.event)
|
||||
message = event.message or (definition.message if definition else event.event)
|
||||
if subject_id:
|
||||
message = f'{message} ({subject_id})'
|
||||
|
||||
for webhook in await get_event_webhooks():
|
||||
if not webhook.get('url') or not await event_webhook_matches(webhook, event):
|
||||
continue
|
||||
|
||||
try:
|
||||
await post_webhook(
|
||||
name,
|
||||
webhook['url'],
|
||||
message,
|
||||
event.model_dump(),
|
||||
description=definition.description if definition else None,
|
||||
)
|
||||
except Exception:
|
||||
log.exception('Event webhook failed for %s', webhook.get('id'))
|
||||
|
||||
|
||||
def schedule_webhook_dispatch(app: Any, event: Event) -> None:
|
||||
try:
|
||||
asyncio.create_task(dispatch_webhook_event(app, event))
|
||||
except RuntimeError:
|
||||
log.exception('Event webhook delivery could not be scheduled for %s', event.event)
|
||||
|
||||
|
||||
class WebhookEventSink:
|
||||
async def handle_event(self, app: Any, event: Event, request: Any | None = None) -> None:
|
||||
schedule_webhook_dispatch(app, event)
|
||||
|
||||
|
||||
async def dispatch_event_functions(app: Any, event: Event, request: Any | None = None) -> None:
|
||||
from open_webui.models.functions import Functions
|
||||
from open_webui.utils.plugin import get_function_module_from_cache
|
||||
|
||||
context = request or SimpleNamespace(app=app)
|
||||
event_payload = event.model_dump()
|
||||
|
||||
try:
|
||||
event_functions = await Functions.get_functions_by_type('event', active_only=True)
|
||||
except Exception:
|
||||
log.exception('Event functions could not be loaded for %s', event.event)
|
||||
return
|
||||
|
||||
for function in event_functions:
|
||||
try:
|
||||
function_module, _, _ = await get_function_module_from_cache(context, function.id, function=function)
|
||||
handler = getattr(function_module, 'event', None)
|
||||
if not handler:
|
||||
continue
|
||||
|
||||
if hasattr(function_module, 'valves') and hasattr(function_module, 'Valves'):
|
||||
valves = await Functions.get_function_valves_by_id(function.id)
|
||||
function_module.valves = function_module.Valves(**(valves if valves else {}))
|
||||
|
||||
sig = inspect.signature(handler)
|
||||
accepts_kwargs = any(param.kind == inspect.Parameter.VAR_KEYWORD for param in sig.parameters.values())
|
||||
extra_params = {
|
||||
'event': event_payload,
|
||||
'__id__': function.id,
|
||||
'__event__': event,
|
||||
'__event_id__': event.id,
|
||||
'__event_name__': event.event,
|
||||
'__app__': app,
|
||||
'__request__': request,
|
||||
}
|
||||
params = {key: value for key, value in extra_params.items() if accepts_kwargs or key in sig.parameters}
|
||||
|
||||
if inspect.iscoroutinefunction(handler):
|
||||
await handler(**params)
|
||||
else:
|
||||
handler(**params)
|
||||
except Exception:
|
||||
log.exception('Event function failed for %s', function.id)
|
||||
|
||||
|
||||
def schedule_event_function_dispatch(app: Any, event: Event, request: Any | None = None) -> None:
|
||||
try:
|
||||
asyncio.create_task(dispatch_event_functions(app, event, request))
|
||||
except RuntimeError:
|
||||
log.exception('Event functions could not be scheduled for %s', event.event)
|
||||
|
||||
|
||||
class EventFunctionSink:
|
||||
async def handle_event(self, app: Any, event: Event, request: Any | None = None) -> None:
|
||||
schedule_event_function_dispatch(app, event, request)
|
||||
|
||||
|
||||
EVENT_SINKS = [EventFunctionSink(), WebhookEventSink()]
|
||||
|
||||
|
||||
async def publish_event(
|
||||
request_or_app: Any,
|
||||
event: EventDefinition | str,
|
||||
*,
|
||||
actor: Any | None = None,
|
||||
subject_id: Any | None = None,
|
||||
subject_type: str | None = None,
|
||||
source: str = 'api',
|
||||
data: dict | None = None,
|
||||
message: str | None = None,
|
||||
) -> None:
|
||||
app = getattr(request_or_app, 'app', request_or_app)
|
||||
request = request_or_app if hasattr(request_or_app, 'app') else None
|
||||
event_payload = build_event(
|
||||
request_or_app,
|
||||
event,
|
||||
actor=actor,
|
||||
subject_id=subject_id,
|
||||
subject_type=subject_type,
|
||||
source=source,
|
||||
data=data,
|
||||
message=message,
|
||||
)
|
||||
|
||||
for sink in EVENT_SINKS:
|
||||
try:
|
||||
await sink.handle_event(app, event_payload, request=request)
|
||||
except Exception:
|
||||
log.exception('Event sink failed for %s', event_payload.event)
|
||||
|
||||
|
||||
async def publish_model_provider_request_failed(
|
||||
request_or_app: Any,
|
||||
*,
|
||||
actor: Any | None,
|
||||
provider: str,
|
||||
base_url: str,
|
||||
status: int,
|
||||
requested_model: str | None = None,
|
||||
api_key: str | None = None,
|
||||
upstream_error: Any = None,
|
||||
) -> None:
|
||||
error = upstream_error.get('error') if isinstance(upstream_error, dict) else upstream_error
|
||||
error_code = None
|
||||
if isinstance(error, dict):
|
||||
error_code = error.get('code') or error.get('type') or error.get('error_code')
|
||||
error = error.get('message') or error.get('detail') or error
|
||||
|
||||
error_text = str(error or '')
|
||||
marker = f'{error_code or ""} {error_text}'.lower()
|
||||
error_type = (
|
||||
'model_not_found'
|
||||
if status == 404
|
||||
and any(value in marker for value in ('model_not_found', 'model not found', 'does not exist', 'no such model'))
|
||||
else 'authentication_failed'
|
||||
if status in (401, 403)
|
||||
else 'rate_limited'
|
||||
if status == 429
|
||||
else 'server_failed'
|
||||
if status >= 500
|
||||
else 'upstream_error'
|
||||
)
|
||||
|
||||
data = {
|
||||
'error_type': error_type,
|
||||
'status': status,
|
||||
'provider': provider,
|
||||
'base_url': base_url,
|
||||
}
|
||||
if requested_model:
|
||||
data['requested_model'] = requested_model
|
||||
if api_key:
|
||||
data['api_key_suffix'] = f'...{api_key[-4:]}'
|
||||
if error_code:
|
||||
data['upstream_error_code'] = error_code
|
||||
if error:
|
||||
data['upstream_message'] = error
|
||||
|
||||
await publish_event(
|
||||
request_or_app,
|
||||
EVENTS.MODEL_PROVIDER_REQUEST_FAILED,
|
||||
actor=actor,
|
||||
subject_id=requested_model,
|
||||
subject_type='model',
|
||||
data=data,
|
||||
)
|
||||
@@ -1,10 +1,11 @@
|
||||
import asyncio
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
from typing import AsyncGenerator, Generator, Iterator
|
||||
import inspect
|
||||
import json
|
||||
import asyncio
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import AsyncGenerator, Generator, Iterator
|
||||
from fastapi import (
|
||||
Depends,
|
||||
FastAPI,
|
||||
@@ -15,35 +16,40 @@ from fastapi import (
|
||||
UploadFile,
|
||||
status,
|
||||
)
|
||||
from pydantic import BaseModel
|
||||
from starlette.responses import Response, StreamingResponse
|
||||
|
||||
from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL
|
||||
|
||||
from open_webui.constants import ERROR_MESSAGES
|
||||
from open_webui.env import BYPASS_MODEL_ACCESS_CONTROL, GLOBAL_LOG_LEVEL
|
||||
from open_webui.models.functions import Functions
|
||||
from open_webui.models.models import Models
|
||||
from open_webui.models.users import UserModel
|
||||
from open_webui.socket.main import (
|
||||
get_event_call,
|
||||
get_event_emitter,
|
||||
)
|
||||
|
||||
|
||||
from open_webui.models.users import UserModel
|
||||
from open_webui.models.functions import Functions
|
||||
from open_webui.models.models import Models
|
||||
|
||||
from open_webui.utils.plugin import (
|
||||
load_function_module_by_id,
|
||||
get_function_module_from_cache,
|
||||
)
|
||||
from open_webui.utils.access_control import check_model_access
|
||||
|
||||
from open_webui.env import GLOBAL_LOG_LEVEL, BYPASS_MODEL_ACCESS_CONTROL
|
||||
from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL
|
||||
|
||||
from open_webui.utils.misc import (
|
||||
add_or_update_system_message,
|
||||
get_last_user_message,
|
||||
prepend_to_first_user_message_content,
|
||||
openai_chat_chunk_message_template,
|
||||
openai_chat_completion_message_template,
|
||||
prepend_to_first_user_message_content,
|
||||
)
|
||||
from open_webui.utils.payload import (
|
||||
apply_model_params_to_body_openai,
|
||||
apply_system_prompt_to_body,
|
||||
)
|
||||
from open_webui.utils.plugin import (
|
||||
get_function_module_from_cache,
|
||||
load_function_module_by_id,
|
||||
)
|
||||
|
||||
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -278,7 +284,7 @@ async def generate_function_chat_completion(request, form_data, user, models: di
|
||||
if params:
|
||||
system = params.pop('system', None)
|
||||
form_data = apply_model_params_to_body_openai(params, form_data)
|
||||
form_data = await apply_system_prompt_to_body(system, form_data, metadata, user)
|
||||
form_data = apply_system_prompt_to_body(system, form_data, metadata, user)
|
||||
|
||||
pipe_id = get_pipe_id(form_data)
|
||||
function_module = await get_function_module_by_id(request, pipe_id)
|
||||
@@ -318,10 +324,11 @@ async def generate_function_chat_completion(request, form_data, user, models: di
|
||||
async for line in res:
|
||||
yield process_line(form_data, line)
|
||||
|
||||
finish_message = openai_chat_chunk_message_template(form_data['model'], '')
|
||||
finish_message['choices'][0]['finish_reason'] = 'stop'
|
||||
yield f'data: {json.dumps(finish_message)}\n\n'
|
||||
yield 'data: [DONE]'
|
||||
if isinstance(res, str) or isinstance(res, Generator):
|
||||
finish_message = openai_chat_chunk_message_template(form_data['model'], '')
|
||||
finish_message['choices'][0]['finish_reason'] = 'stop'
|
||||
yield f'data: {json.dumps(finish_message)}\n\n'
|
||||
yield 'data: [DONE]'
|
||||
|
||||
return StreamingResponse(stream_content(), media_type='text/event-stream')
|
||||
else:
|
||||
|
||||
@@ -1,39 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from contextlib import asynccontextmanager, contextmanager
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, Optional
|
||||
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
|
||||
|
||||
from open_webui.internal.wrappers import register_connection
|
||||
from open_webui.env import (
|
||||
DATABASE_ENABLE_IAM_TOKEN_AUTH,
|
||||
DATABASE_ENABLE_SESSION_SHARING,
|
||||
DATABASE_ENABLE_SQLITE_WAL,
|
||||
OPEN_WEBUI_DIR,
|
||||
DATABASE_URL,
|
||||
DATABASE_SCHEMA,
|
||||
DATABASE_POOL_MAX_OVERFLOW,
|
||||
DATABASE_POOL_RECYCLE,
|
||||
DATABASE_POOL_SIZE,
|
||||
DATABASE_POOL_TIMEOUT,
|
||||
DATABASE_SCHEMA,
|
||||
DATABASE_ENABLE_SQLITE_WAL,
|
||||
DATABASE_ENABLE_SESSION_SHARING,
|
||||
DATABASE_SQLITE_PRAGMA_SYNCHRONOUS,
|
||||
DATABASE_SQLITE_PRAGMA_BUSY_TIMEOUT,
|
||||
DATABASE_SQLITE_PRAGMA_CACHE_SIZE,
|
||||
DATABASE_SQLITE_PRAGMA_JOURNAL_SIZE_LIMIT,
|
||||
DATABASE_SQLITE_PRAGMA_MMAP_SIZE,
|
||||
DATABASE_SQLITE_PRAGMA_SYNCHRONOUS,
|
||||
DATABASE_SQLITE_PRAGMA_TEMP_STORE,
|
||||
DATABASE_URL,
|
||||
DATABASE_SQLITE_PRAGMA_MMAP_SIZE,
|
||||
DATABASE_SQLITE_PRAGMA_JOURNAL_SIZE_LIMIT,
|
||||
ENABLE_DB_MIGRATIONS,
|
||||
OPEN_WEBUI_DIR,
|
||||
)
|
||||
from sqlalchemy import Dialect, MetaData, create_engine, event, types
|
||||
from sqlalchemy.engine.url import make_url
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||
from peewee_migrate import Router
|
||||
from sqlalchemy import Dialect, create_engine, MetaData, event, types
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import Session, scoped_session, sessionmaker
|
||||
from sqlalchemy.pool import NullPool, QueuePool
|
||||
from sqlalchemy.orm import scoped_session, sessionmaker, Session
|
||||
from sqlalchemy.pool import QueuePool, NullPool
|
||||
from sqlalchemy.sql.type_api import _T
|
||||
from typing_extensions import Self
|
||||
|
||||
@@ -120,25 +116,61 @@ extract_ssl_mode_from_url = extract_ssl_params_from_url
|
||||
reattach_ssl_mode_to_url = reattach_ssl_params_to_url
|
||||
|
||||
|
||||
class JSONField(types.TypeDecorator): # TEXT-backed JSON storage
|
||||
"""Store arbitrary Python objects as JSON-encoded TEXT.
|
||||
|
||||
Used instead of native JSON columns for portability across SQLite and
|
||||
PostgreSQL. Values are serialized with ``json.dumps`` on write and
|
||||
deserialized with ``json.loads`` on read.
|
||||
"""
|
||||
|
||||
impl = types.UnicodeText
|
||||
class JSONField(types.TypeDecorator):
|
||||
impl = types.Text
|
||||
cache_ok = True
|
||||
|
||||
def process_bind_param(self, value: _T | None, dialect: Dialect) -> Any:
|
||||
return json.dumps(value) if value is not None else None
|
||||
def process_bind_param(self, value: Optional[_T], dialect: Dialect) -> Any:
|
||||
return json.dumps(value)
|
||||
|
||||
def process_result_value(self, value: _T | None, dialect: Dialect) -> Any:
|
||||
return json.loads(value) if value is not None else None
|
||||
def process_result_value(self, value: Optional[_T], dialect: Dialect) -> Any:
|
||||
if value is not None:
|
||||
return json.loads(value)
|
||||
|
||||
def copy(self, **kwargs: Any) -> Self:
|
||||
return JSONField(length=self.impl.length)
|
||||
def copy(self, **kw: Any) -> Self:
|
||||
return JSONField(self.impl.length)
|
||||
|
||||
def db_value(self, value):
|
||||
return json.dumps(value)
|
||||
|
||||
def python_value(self, value):
|
||||
if value is not None:
|
||||
return json.loads(value)
|
||||
|
||||
|
||||
# Workaround to handle the peewee migration
|
||||
# This is required to ensure the peewee migration is handled before the alembic migration
|
||||
def handle_peewee_migration(DATABASE_URL):
|
||||
db = None
|
||||
try:
|
||||
# Normalize SSL params so psycopg2 always sees `sslmode=` (never `ssl=`)
|
||||
# and cert-file params are preserved in the connection string.
|
||||
url_without_ssl, ssl_params = extract_ssl_params_from_url(DATABASE_URL)
|
||||
normalized_url = reattach_ssl_params_to_url(url_without_ssl, ssl_params)
|
||||
|
||||
# Replace the postgresql:// with postgres:// to handle the peewee migration
|
||||
db = register_connection(normalized_url.replace('postgresql://', 'postgres://'))
|
||||
migrate_dir = OPEN_WEBUI_DIR / 'internal' / 'migrations'
|
||||
router = Router(db, logger=log, migrate_dir=migrate_dir)
|
||||
router.run()
|
||||
db.close()
|
||||
|
||||
except Exception as e:
|
||||
log.error(f'Failed to initialize the database connection: {e}')
|
||||
log.warning('Hint: If your database password contains special characters, you may need to URL-encode it.')
|
||||
raise
|
||||
finally:
|
||||
# Properly closing the database connection
|
||||
if db and not db.is_closed():
|
||||
db.close()
|
||||
|
||||
# Assert if db connection has been closed
|
||||
if db is not None:
|
||||
assert db.is_closed(), 'Database connection is still open.'
|
||||
|
||||
|
||||
if ENABLE_DB_MIGRATIONS:
|
||||
handle_peewee_migration(DATABASE_URL)
|
||||
|
||||
|
||||
# Normalize SSL params from the URL once; the sync engine needs them
|
||||
@@ -149,63 +181,6 @@ _url_without_ssl, _ssl_dict = extract_ssl_params_from_url(DATABASE_URL)
|
||||
SQLALCHEMY_DATABASE_URL = reattach_ssl_params_to_url(_url_without_ssl, _ssl_dict) if _ssl_dict else DATABASE_URL
|
||||
|
||||
|
||||
class RDSIAMTokenAuth:
|
||||
_refresh_after = timedelta(minutes=14)
|
||||
|
||||
def __init__(self, database_url: str) -> None:
|
||||
url = make_url(database_url)
|
||||
if not url.drivername.startswith(('postgresql', 'postgres')):
|
||||
raise ValueError('DATABASE_ENABLE_IAM_TOKEN_AUTH is only supported for PostgreSQL databases')
|
||||
if not url.host or not url.username:
|
||||
raise ValueError('DATABASE_ENABLE_IAM_TOKEN_AUTH requires a database host and user')
|
||||
|
||||
self.host = url.host
|
||||
self.port = url.port or 5432
|
||||
self.username = url.username
|
||||
self._client = None
|
||||
self._token: str | None = None
|
||||
self._expires_at = datetime.min.replace(tzinfo=timezone.utc)
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
if self._client is None:
|
||||
import boto3
|
||||
|
||||
self._client = boto3.client('rds')
|
||||
return self._client
|
||||
|
||||
def get_password(self) -> str:
|
||||
now = datetime.now(timezone.utc)
|
||||
if self._token and now < self._expires_at:
|
||||
return self._token
|
||||
|
||||
self._token = self.client.generate_db_auth_token(
|
||||
DBHostname=self.host,
|
||||
Port=self.port,
|
||||
DBUsername=self.username,
|
||||
)
|
||||
self._expires_at = now + self._refresh_after
|
||||
log.info('AWS RDS IAM database token refreshed; next refresh after %s', self._expires_at.isoformat())
|
||||
return self._token
|
||||
|
||||
|
||||
_rds_iam_token_auth = RDSIAMTokenAuth(SQLALCHEMY_DATABASE_URL) if DATABASE_ENABLE_IAM_TOKEN_AUTH else None
|
||||
|
||||
|
||||
def _set_iam_token_password(dialect, conn_rec, cargs, cparams):
|
||||
if _rds_iam_token_auth is not None:
|
||||
cparams['password'] = _rds_iam_token_auth.get_password()
|
||||
|
||||
|
||||
def enable_iam_token_auth(connectable) -> None:
|
||||
if _rds_iam_token_auth is None:
|
||||
return
|
||||
|
||||
engine = getattr(connectable, 'sync_engine', connectable)
|
||||
if not event.contains(engine, 'do_connect', _set_iam_token_password):
|
||||
event.listen(engine, 'do_connect', _set_iam_token_password)
|
||||
|
||||
|
||||
def _make_async_url(url: str) -> str:
|
||||
"""Convert a sync database URL to its async driver equivalent.
|
||||
|
||||
@@ -328,8 +303,6 @@ else:
|
||||
else:
|
||||
engine = create_engine(SQLALCHEMY_DATABASE_URL, pool_pre_ping=True)
|
||||
|
||||
enable_iam_token_auth(engine)
|
||||
|
||||
|
||||
# Sync session — used ONLY for startup config loading (config.py runs at import time)
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine, expire_on_commit=False)
|
||||
@@ -359,15 +332,6 @@ get_db = contextmanager(get_session)
|
||||
# all work without any stripping or translation.
|
||||
ASYNC_SQLALCHEMY_DATABASE_URL = _make_async_url(SQLALCHEMY_DATABASE_URL)
|
||||
|
||||
# psycopg v3 cannot run in async mode under Windows' default
|
||||
# ProactorEventLoop — switch to SelectorEventLoop before creating
|
||||
# the async engine. This runs at import time, which is early enough
|
||||
# to cover every entry point (workers, reload, direct invocations).
|
||||
if sys.platform == 'win32' and _is_postgres_url(DATABASE_URL):
|
||||
import asyncio
|
||||
|
||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||
|
||||
if 'sqlite' in ASYNC_SQLALCHEMY_DATABASE_URL:
|
||||
# Generous default — async coroutines + no session sharing = high connection demand.
|
||||
_sqlite_pool_size = DATABASE_POOL_SIZE if isinstance(DATABASE_POOL_SIZE, int) and DATABASE_POOL_SIZE > 0 else 512
|
||||
@@ -406,8 +370,6 @@ else:
|
||||
pool_pre_ping=True,
|
||||
)
|
||||
|
||||
enable_iam_token_auth(async_engine)
|
||||
|
||||
|
||||
AsyncSessionLocal = async_sessionmaker(
|
||||
bind=async_engine,
|
||||
@@ -438,7 +400,7 @@ async def get_async_db():
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def get_async_db_context(db: AsyncSession | None = None):
|
||||
async def get_async_db_context(db: Optional[AsyncSession] = None):
|
||||
"""Async context manager that reuses an existing session if provided and session sharing is enabled."""
|
||||
if isinstance(db, AsyncSession) and DATABASE_ENABLE_SESSION_SHARING:
|
||||
yield db
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
"""Peewee migrations -- 001_initial_schema.py.
|
||||
|
||||
Some examples (model - class or model name)::
|
||||
|
||||
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||
> Model = migrator.ModelClass # Return model in current state by name
|
||||
|
||||
> migrator.sql(sql) # Run custom SQL
|
||||
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||
> migrator.change_fields(model, **fields) # Change fields
|
||||
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||
> migrator.rename_table(model, new_table_name)
|
||||
> migrator.add_index(model, *col_names, unique=False)
|
||||
> migrator.add_not_null(model, *field_names)
|
||||
> migrator.add_default(model, field_name, default)
|
||||
> migrator.add_constraint(model, name, sql)
|
||||
> migrator.drop_index(model, *col_names)
|
||||
> migrator.drop_not_null(model, *field_names)
|
||||
> migrator.drop_constraints(model, *constraints)
|
||||
|
||||
"""
|
||||
|
||||
from contextlib import suppress
|
||||
|
||||
import peewee as pw
|
||||
from peewee_migrate import Migrator
|
||||
|
||||
with suppress(ImportError):
|
||||
import playhouse.postgres_ext as pw_pext
|
||||
|
||||
|
||||
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your migrations here."""
|
||||
|
||||
# We perform different migrations for SQLite and other databases
|
||||
# This is because SQLite is very loose with enforcing its schema, and trying to migrate other databases like SQLite
|
||||
# will require per-database SQL queries.
|
||||
# Instead, we assume that because external DB support was added at a later date, it is safe to assume a newer base
|
||||
# schema instead of trying to migrate from an older schema.
|
||||
if isinstance(database, pw.SqliteDatabase):
|
||||
migrate_sqlite(migrator, database, fake=fake)
|
||||
else:
|
||||
migrate_external(migrator, database, fake=fake)
|
||||
|
||||
|
||||
def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
@migrator.create_model
|
||||
class Auth(pw.Model):
|
||||
id = pw.CharField(max_length=255, unique=True)
|
||||
email = pw.CharField(max_length=255)
|
||||
password = pw.CharField(max_length=255)
|
||||
active = pw.BooleanField()
|
||||
|
||||
class Meta:
|
||||
table_name = 'auth'
|
||||
|
||||
@migrator.create_model
|
||||
class Chat(pw.Model):
|
||||
id = pw.CharField(max_length=255, unique=True)
|
||||
user_id = pw.CharField(max_length=255)
|
||||
title = pw.CharField()
|
||||
chat = pw.TextField()
|
||||
timestamp = pw.BigIntegerField()
|
||||
|
||||
class Meta:
|
||||
table_name = 'chat'
|
||||
|
||||
@migrator.create_model
|
||||
class ChatIdTag(pw.Model):
|
||||
id = pw.CharField(max_length=255, unique=True)
|
||||
tag_name = pw.CharField(max_length=255)
|
||||
chat_id = pw.CharField(max_length=255)
|
||||
user_id = pw.CharField(max_length=255)
|
||||
timestamp = pw.BigIntegerField()
|
||||
|
||||
class Meta:
|
||||
table_name = 'chatidtag'
|
||||
|
||||
@migrator.create_model
|
||||
class Document(pw.Model):
|
||||
id = pw.AutoField()
|
||||
collection_name = pw.CharField(max_length=255, unique=True)
|
||||
name = pw.CharField(max_length=255, unique=True)
|
||||
title = pw.CharField()
|
||||
filename = pw.CharField()
|
||||
content = pw.TextField(null=True)
|
||||
user_id = pw.CharField(max_length=255)
|
||||
timestamp = pw.BigIntegerField()
|
||||
|
||||
class Meta:
|
||||
table_name = 'document'
|
||||
|
||||
@migrator.create_model
|
||||
class Modelfile(pw.Model):
|
||||
id = pw.AutoField()
|
||||
tag_name = pw.CharField(max_length=255, unique=True)
|
||||
user_id = pw.CharField(max_length=255)
|
||||
modelfile = pw.TextField()
|
||||
timestamp = pw.BigIntegerField()
|
||||
|
||||
class Meta:
|
||||
table_name = 'modelfile'
|
||||
|
||||
@migrator.create_model
|
||||
class Prompt(pw.Model):
|
||||
id = pw.AutoField()
|
||||
command = pw.CharField(max_length=255, unique=True)
|
||||
user_id = pw.CharField(max_length=255)
|
||||
title = pw.CharField()
|
||||
content = pw.TextField()
|
||||
timestamp = pw.BigIntegerField()
|
||||
|
||||
class Meta:
|
||||
table_name = 'prompt'
|
||||
|
||||
@migrator.create_model
|
||||
class Tag(pw.Model):
|
||||
id = pw.CharField(max_length=255, unique=True)
|
||||
name = pw.CharField(max_length=255)
|
||||
user_id = pw.CharField(max_length=255)
|
||||
data = pw.TextField(null=True)
|
||||
|
||||
class Meta:
|
||||
table_name = 'tag'
|
||||
|
||||
@migrator.create_model
|
||||
class User(pw.Model):
|
||||
id = pw.CharField(max_length=255, unique=True)
|
||||
name = pw.CharField(max_length=255)
|
||||
email = pw.CharField(max_length=255)
|
||||
role = pw.CharField(max_length=255)
|
||||
profile_image_url = pw.CharField(max_length=255)
|
||||
timestamp = pw.BigIntegerField()
|
||||
|
||||
class Meta:
|
||||
table_name = 'user'
|
||||
|
||||
|
||||
def migrate_external(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
@migrator.create_model
|
||||
class Auth(pw.Model):
|
||||
id = pw.CharField(max_length=255, unique=True)
|
||||
email = pw.CharField(max_length=255)
|
||||
password = pw.TextField()
|
||||
active = pw.BooleanField()
|
||||
|
||||
class Meta:
|
||||
table_name = 'auth'
|
||||
|
||||
@migrator.create_model
|
||||
class Chat(pw.Model):
|
||||
id = pw.CharField(max_length=255, unique=True)
|
||||
user_id = pw.CharField(max_length=255)
|
||||
title = pw.TextField()
|
||||
chat = pw.TextField()
|
||||
timestamp = pw.BigIntegerField()
|
||||
|
||||
class Meta:
|
||||
table_name = 'chat'
|
||||
|
||||
@migrator.create_model
|
||||
class ChatIdTag(pw.Model):
|
||||
id = pw.CharField(max_length=255, unique=True)
|
||||
tag_name = pw.CharField(max_length=255)
|
||||
chat_id = pw.CharField(max_length=255)
|
||||
user_id = pw.CharField(max_length=255)
|
||||
timestamp = pw.BigIntegerField()
|
||||
|
||||
class Meta:
|
||||
table_name = 'chatidtag'
|
||||
|
||||
@migrator.create_model
|
||||
class Document(pw.Model):
|
||||
id = pw.AutoField()
|
||||
collection_name = pw.CharField(max_length=255, unique=True)
|
||||
name = pw.CharField(max_length=255, unique=True)
|
||||
title = pw.TextField()
|
||||
filename = pw.TextField()
|
||||
content = pw.TextField(null=True)
|
||||
user_id = pw.CharField(max_length=255)
|
||||
timestamp = pw.BigIntegerField()
|
||||
|
||||
class Meta:
|
||||
table_name = 'document'
|
||||
|
||||
@migrator.create_model
|
||||
class Modelfile(pw.Model):
|
||||
id = pw.AutoField()
|
||||
tag_name = pw.CharField(max_length=255, unique=True)
|
||||
user_id = pw.CharField(max_length=255)
|
||||
modelfile = pw.TextField()
|
||||
timestamp = pw.BigIntegerField()
|
||||
|
||||
class Meta:
|
||||
table_name = 'modelfile'
|
||||
|
||||
@migrator.create_model
|
||||
class Prompt(pw.Model):
|
||||
id = pw.AutoField()
|
||||
command = pw.CharField(max_length=255, unique=True)
|
||||
user_id = pw.CharField(max_length=255)
|
||||
title = pw.TextField()
|
||||
content = pw.TextField()
|
||||
timestamp = pw.BigIntegerField()
|
||||
|
||||
class Meta:
|
||||
table_name = 'prompt'
|
||||
|
||||
@migrator.create_model
|
||||
class Tag(pw.Model):
|
||||
id = pw.CharField(max_length=255, unique=True)
|
||||
name = pw.CharField(max_length=255)
|
||||
user_id = pw.CharField(max_length=255)
|
||||
data = pw.TextField(null=True)
|
||||
|
||||
class Meta:
|
||||
table_name = 'tag'
|
||||
|
||||
@migrator.create_model
|
||||
class User(pw.Model):
|
||||
id = pw.CharField(max_length=255, unique=True)
|
||||
name = pw.CharField(max_length=255)
|
||||
email = pw.CharField(max_length=255)
|
||||
role = pw.CharField(max_length=255)
|
||||
profile_image_url = pw.TextField()
|
||||
timestamp = pw.BigIntegerField()
|
||||
|
||||
class Meta:
|
||||
table_name = 'user'
|
||||
|
||||
|
||||
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your rollback migrations here."""
|
||||
|
||||
migrator.remove_model('user')
|
||||
|
||||
migrator.remove_model('tag')
|
||||
|
||||
migrator.remove_model('prompt')
|
||||
|
||||
migrator.remove_model('modelfile')
|
||||
|
||||
migrator.remove_model('document')
|
||||
|
||||
migrator.remove_model('chatidtag')
|
||||
|
||||
migrator.remove_model('chat')
|
||||
|
||||
migrator.remove_model('auth')
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Peewee migrations -- 002_add_local_sharing.py.
|
||||
|
||||
Some examples (model - class or model name)::
|
||||
|
||||
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||
> Model = migrator.ModelClass # Return model in current state by name
|
||||
|
||||
> migrator.sql(sql) # Run custom SQL
|
||||
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||
> migrator.change_fields(model, **fields) # Change fields
|
||||
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||
> migrator.rename_table(model, new_table_name)
|
||||
> migrator.add_index(model, *col_names, unique=False)
|
||||
> migrator.add_not_null(model, *field_names)
|
||||
> migrator.add_default(model, field_name, default)
|
||||
> migrator.add_constraint(model, name, sql)
|
||||
> migrator.drop_index(model, *col_names)
|
||||
> migrator.drop_not_null(model, *field_names)
|
||||
> migrator.drop_constraints(model, *constraints)
|
||||
|
||||
"""
|
||||
|
||||
from contextlib import suppress
|
||||
|
||||
import peewee as pw
|
||||
from peewee_migrate import Migrator
|
||||
|
||||
with suppress(ImportError):
|
||||
import playhouse.postgres_ext as pw_pext
|
||||
|
||||
|
||||
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your migrations here."""
|
||||
|
||||
migrator.add_fields('chat', share_id=pw.CharField(max_length=255, null=True, unique=True))
|
||||
|
||||
|
||||
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your rollback migrations here."""
|
||||
|
||||
migrator.remove_fields('chat', 'share_id')
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Peewee migrations -- 002_add_local_sharing.py.
|
||||
|
||||
Some examples (model - class or model name)::
|
||||
|
||||
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||
> Model = migrator.ModelClass # Return model in current state by name
|
||||
|
||||
> migrator.sql(sql) # Run custom SQL
|
||||
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||
> migrator.change_fields(model, **fields) # Change fields
|
||||
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||
> migrator.rename_table(model, new_table_name)
|
||||
> migrator.add_index(model, *col_names, unique=False)
|
||||
> migrator.add_not_null(model, *field_names)
|
||||
> migrator.add_default(model, field_name, default)
|
||||
> migrator.add_constraint(model, name, sql)
|
||||
> migrator.drop_index(model, *col_names)
|
||||
> migrator.drop_not_null(model, *field_names)
|
||||
> migrator.drop_constraints(model, *constraints)
|
||||
|
||||
"""
|
||||
|
||||
from contextlib import suppress
|
||||
|
||||
import peewee as pw
|
||||
from peewee_migrate import Migrator
|
||||
|
||||
with suppress(ImportError):
|
||||
import playhouse.postgres_ext as pw_pext
|
||||
|
||||
|
||||
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your migrations here."""
|
||||
|
||||
migrator.add_fields('user', api_key=pw.CharField(max_length=255, null=True, unique=True))
|
||||
|
||||
|
||||
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your rollback migrations here."""
|
||||
|
||||
migrator.remove_fields('user', 'api_key')
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Peewee migrations -- 002_add_local_sharing.py.
|
||||
|
||||
Some examples (model - class or model name)::
|
||||
|
||||
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||
> Model = migrator.ModelClass # Return model in current state by name
|
||||
|
||||
> migrator.sql(sql) # Run custom SQL
|
||||
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||
> migrator.change_fields(model, **fields) # Change fields
|
||||
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||
> migrator.rename_table(model, new_table_name)
|
||||
> migrator.add_index(model, *col_names, unique=False)
|
||||
> migrator.add_not_null(model, *field_names)
|
||||
> migrator.add_default(model, field_name, default)
|
||||
> migrator.add_constraint(model, name, sql)
|
||||
> migrator.drop_index(model, *col_names)
|
||||
> migrator.drop_not_null(model, *field_names)
|
||||
> migrator.drop_constraints(model, *constraints)
|
||||
|
||||
"""
|
||||
|
||||
from contextlib import suppress
|
||||
|
||||
import peewee as pw
|
||||
from peewee_migrate import Migrator
|
||||
|
||||
with suppress(ImportError):
|
||||
import playhouse.postgres_ext as pw_pext
|
||||
|
||||
|
||||
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your migrations here."""
|
||||
|
||||
migrator.add_fields('chat', archived=pw.BooleanField(default=False))
|
||||
|
||||
|
||||
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your rollback migrations here."""
|
||||
|
||||
migrator.remove_fields('chat', 'archived')
|
||||
@@ -0,0 +1,125 @@
|
||||
"""Peewee migrations -- 002_add_local_sharing.py.
|
||||
|
||||
Some examples (model - class or model name)::
|
||||
|
||||
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||
> Model = migrator.ModelClass # Return model in current state by name
|
||||
|
||||
> migrator.sql(sql) # Run custom SQL
|
||||
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||
> migrator.change_fields(model, **fields) # Change fields
|
||||
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||
> migrator.rename_table(model, new_table_name)
|
||||
> migrator.add_index(model, *col_names, unique=False)
|
||||
> migrator.add_not_null(model, *field_names)
|
||||
> migrator.add_default(model, field_name, default)
|
||||
> migrator.add_constraint(model, name, sql)
|
||||
> migrator.drop_index(model, *col_names)
|
||||
> migrator.drop_not_null(model, *field_names)
|
||||
> migrator.drop_constraints(model, *constraints)
|
||||
|
||||
"""
|
||||
|
||||
from contextlib import suppress
|
||||
|
||||
import peewee as pw
|
||||
from peewee_migrate import Migrator
|
||||
|
||||
with suppress(ImportError):
|
||||
import playhouse.postgres_ext as pw_pext
|
||||
|
||||
|
||||
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your migrations here."""
|
||||
|
||||
if isinstance(database, pw.SqliteDatabase):
|
||||
migrate_sqlite(migrator, database, fake=fake)
|
||||
else:
|
||||
migrate_external(migrator, database, fake=fake)
|
||||
|
||||
|
||||
def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
# Adding fields created_at and updated_at to the 'chat' table
|
||||
migrator.add_fields(
|
||||
'chat',
|
||||
created_at=pw.DateTimeField(null=True), # Allow null for transition
|
||||
updated_at=pw.DateTimeField(null=True), # Allow null for transition
|
||||
)
|
||||
|
||||
# Populate the new fields from an existing 'timestamp' field
|
||||
migrator.sql('UPDATE chat SET created_at = timestamp, updated_at = timestamp WHERE timestamp IS NOT NULL')
|
||||
|
||||
# Now that the data has been copied, remove the original 'timestamp' field
|
||||
migrator.remove_fields('chat', 'timestamp')
|
||||
|
||||
# Update the fields to be not null now that they are populated
|
||||
migrator.change_fields(
|
||||
'chat',
|
||||
created_at=pw.DateTimeField(null=False),
|
||||
updated_at=pw.DateTimeField(null=False),
|
||||
)
|
||||
|
||||
|
||||
def migrate_external(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
# Adding fields created_at and updated_at to the 'chat' table
|
||||
migrator.add_fields(
|
||||
'chat',
|
||||
created_at=pw.BigIntegerField(null=True), # Allow null for transition
|
||||
updated_at=pw.BigIntegerField(null=True), # Allow null for transition
|
||||
)
|
||||
|
||||
# Populate the new fields from an existing 'timestamp' field
|
||||
migrator.sql('UPDATE chat SET created_at = timestamp, updated_at = timestamp WHERE timestamp IS NOT NULL')
|
||||
|
||||
# Now that the data has been copied, remove the original 'timestamp' field
|
||||
migrator.remove_fields('chat', 'timestamp')
|
||||
|
||||
# Update the fields to be not null now that they are populated
|
||||
migrator.change_fields(
|
||||
'chat',
|
||||
created_at=pw.BigIntegerField(null=False),
|
||||
updated_at=pw.BigIntegerField(null=False),
|
||||
)
|
||||
|
||||
|
||||
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your rollback migrations here."""
|
||||
|
||||
if isinstance(database, pw.SqliteDatabase):
|
||||
rollback_sqlite(migrator, database, fake=fake)
|
||||
else:
|
||||
rollback_external(migrator, database, fake=fake)
|
||||
|
||||
|
||||
def rollback_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
# Recreate the timestamp field initially allowing null values for safe transition
|
||||
migrator.add_fields('chat', timestamp=pw.DateTimeField(null=True))
|
||||
|
||||
# Copy the earliest created_at date back into the new timestamp field
|
||||
# This assumes created_at was originally a copy of timestamp
|
||||
migrator.sql('UPDATE chat SET timestamp = created_at')
|
||||
|
||||
# Remove the created_at and updated_at fields
|
||||
migrator.remove_fields('chat', 'created_at', 'updated_at')
|
||||
|
||||
# Finally, alter the timestamp field to not allow nulls if that was the original setting
|
||||
migrator.change_fields('chat', timestamp=pw.DateTimeField(null=False))
|
||||
|
||||
|
||||
def rollback_external(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
# Recreate the timestamp field initially allowing null values for safe transition
|
||||
migrator.add_fields('chat', timestamp=pw.BigIntegerField(null=True))
|
||||
|
||||
# Copy the earliest created_at date back into the new timestamp field
|
||||
# This assumes created_at was originally a copy of timestamp
|
||||
migrator.sql('UPDATE chat SET timestamp = created_at')
|
||||
|
||||
# Remove the created_at and updated_at fields
|
||||
migrator.remove_fields('chat', 'created_at', 'updated_at')
|
||||
|
||||
# Finally, alter the timestamp field to not allow nulls if that was the original setting
|
||||
migrator.change_fields('chat', timestamp=pw.BigIntegerField(null=False))
|
||||
@@ -0,0 +1,129 @@
|
||||
"""Peewee migrations -- 006_migrate_timestamps_and_charfields.py.
|
||||
|
||||
Some examples (model - class or model name)::
|
||||
|
||||
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||
> Model = migrator.ModelClass # Return model in current state by name
|
||||
|
||||
> migrator.sql(sql) # Run custom SQL
|
||||
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||
> migrator.change_fields(model, **fields) # Change fields
|
||||
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||
> migrator.rename_table(model, new_table_name)
|
||||
> migrator.add_index(model, *col_names, unique=False)
|
||||
> migrator.add_not_null(model, *field_names)
|
||||
> migrator.add_default(model, field_name, default)
|
||||
> migrator.add_constraint(model, name, sql)
|
||||
> migrator.drop_index(model, *col_names)
|
||||
> migrator.drop_not_null(model, *field_names)
|
||||
> migrator.drop_constraints(model, *constraints)
|
||||
|
||||
"""
|
||||
|
||||
from contextlib import suppress
|
||||
|
||||
import peewee as pw
|
||||
from peewee_migrate import Migrator
|
||||
|
||||
with suppress(ImportError):
|
||||
import playhouse.postgres_ext as pw_pext
|
||||
|
||||
|
||||
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your migrations here."""
|
||||
|
||||
# Alter the tables with timestamps
|
||||
migrator.change_fields(
|
||||
'chatidtag',
|
||||
timestamp=pw.BigIntegerField(),
|
||||
)
|
||||
migrator.change_fields(
|
||||
'document',
|
||||
timestamp=pw.BigIntegerField(),
|
||||
)
|
||||
migrator.change_fields(
|
||||
'modelfile',
|
||||
timestamp=pw.BigIntegerField(),
|
||||
)
|
||||
migrator.change_fields(
|
||||
'prompt',
|
||||
timestamp=pw.BigIntegerField(),
|
||||
)
|
||||
migrator.change_fields(
|
||||
'user',
|
||||
timestamp=pw.BigIntegerField(),
|
||||
)
|
||||
# Alter the tables with varchar to text where necessary
|
||||
migrator.change_fields(
|
||||
'auth',
|
||||
password=pw.TextField(),
|
||||
)
|
||||
migrator.change_fields(
|
||||
'chat',
|
||||
title=pw.TextField(),
|
||||
)
|
||||
migrator.change_fields(
|
||||
'document',
|
||||
title=pw.TextField(),
|
||||
filename=pw.TextField(),
|
||||
)
|
||||
migrator.change_fields(
|
||||
'prompt',
|
||||
title=pw.TextField(),
|
||||
)
|
||||
migrator.change_fields(
|
||||
'user',
|
||||
profile_image_url=pw.TextField(),
|
||||
)
|
||||
|
||||
|
||||
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your rollback migrations here."""
|
||||
|
||||
if isinstance(database, pw.SqliteDatabase):
|
||||
# Alter the tables with timestamps
|
||||
migrator.change_fields(
|
||||
'chatidtag',
|
||||
timestamp=pw.DateField(),
|
||||
)
|
||||
migrator.change_fields(
|
||||
'document',
|
||||
timestamp=pw.DateField(),
|
||||
)
|
||||
migrator.change_fields(
|
||||
'modelfile',
|
||||
timestamp=pw.DateField(),
|
||||
)
|
||||
migrator.change_fields(
|
||||
'prompt',
|
||||
timestamp=pw.DateField(),
|
||||
)
|
||||
migrator.change_fields(
|
||||
'user',
|
||||
timestamp=pw.DateField(),
|
||||
)
|
||||
migrator.change_fields(
|
||||
'auth',
|
||||
password=pw.CharField(max_length=255),
|
||||
)
|
||||
migrator.change_fields(
|
||||
'chat',
|
||||
title=pw.CharField(),
|
||||
)
|
||||
migrator.change_fields(
|
||||
'document',
|
||||
title=pw.CharField(),
|
||||
filename=pw.CharField(),
|
||||
)
|
||||
migrator.change_fields(
|
||||
'prompt',
|
||||
title=pw.CharField(),
|
||||
)
|
||||
migrator.change_fields(
|
||||
'user',
|
||||
profile_image_url=pw.CharField(),
|
||||
)
|
||||
@@ -0,0 +1,78 @@
|
||||
"""Peewee migrations -- 002_add_local_sharing.py.
|
||||
|
||||
Some examples (model - class or model name)::
|
||||
|
||||
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||
> Model = migrator.ModelClass # Return model in current state by name
|
||||
|
||||
> migrator.sql(sql) # Run custom SQL
|
||||
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||
> migrator.change_fields(model, **fields) # Change fields
|
||||
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||
> migrator.rename_table(model, new_table_name)
|
||||
> migrator.add_index(model, *col_names, unique=False)
|
||||
> migrator.add_not_null(model, *field_names)
|
||||
> migrator.add_default(model, field_name, default)
|
||||
> migrator.add_constraint(model, name, sql)
|
||||
> migrator.drop_index(model, *col_names)
|
||||
> migrator.drop_not_null(model, *field_names)
|
||||
> migrator.drop_constraints(model, *constraints)
|
||||
|
||||
"""
|
||||
|
||||
from contextlib import suppress
|
||||
|
||||
import peewee as pw
|
||||
from peewee_migrate import Migrator
|
||||
|
||||
with suppress(ImportError):
|
||||
import playhouse.postgres_ext as pw_pext
|
||||
|
||||
|
||||
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your migrations here."""
|
||||
|
||||
# Adding fields created_at and updated_at to the 'user' table
|
||||
migrator.add_fields(
|
||||
'user',
|
||||
created_at=pw.BigIntegerField(null=True), # Allow null for transition
|
||||
updated_at=pw.BigIntegerField(null=True), # Allow null for transition
|
||||
last_active_at=pw.BigIntegerField(null=True), # Allow null for transition
|
||||
)
|
||||
|
||||
# Populate the new fields from an existing 'timestamp' field
|
||||
migrator.sql(
|
||||
'UPDATE "user" SET created_at = timestamp, updated_at = timestamp, last_active_at = timestamp WHERE timestamp IS NOT NULL'
|
||||
)
|
||||
|
||||
# Now that the data has been copied, remove the original 'timestamp' field
|
||||
migrator.remove_fields('user', 'timestamp')
|
||||
|
||||
# Update the fields to be not null now that they are populated
|
||||
migrator.change_fields(
|
||||
'user',
|
||||
created_at=pw.BigIntegerField(null=False),
|
||||
updated_at=pw.BigIntegerField(null=False),
|
||||
last_active_at=pw.BigIntegerField(null=False),
|
||||
)
|
||||
|
||||
|
||||
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your rollback migrations here."""
|
||||
|
||||
# Recreate the timestamp field initially allowing null values for safe transition
|
||||
migrator.add_fields('user', timestamp=pw.BigIntegerField(null=True))
|
||||
|
||||
# Copy the earliest created_at date back into the new timestamp field
|
||||
# This assumes created_at was originally a copy of timestamp
|
||||
migrator.sql('UPDATE "user" SET timestamp = created_at')
|
||||
|
||||
# Remove the created_at and updated_at fields
|
||||
migrator.remove_fields('user', 'created_at', 'updated_at', 'last_active_at')
|
||||
|
||||
# Finally, alter the timestamp field to not allow nulls if that was the original setting
|
||||
migrator.change_fields('user', timestamp=pw.BigIntegerField(null=False))
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Peewee migrations -- 002_add_local_sharing.py.
|
||||
|
||||
Some examples (model - class or model name)::
|
||||
|
||||
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||
> Model = migrator.ModelClass # Return model in current state by name
|
||||
|
||||
> migrator.sql(sql) # Run custom SQL
|
||||
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||
> migrator.change_fields(model, **fields) # Change fields
|
||||
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||
> migrator.rename_table(model, new_table_name)
|
||||
> migrator.add_index(model, *col_names, unique=False)
|
||||
> migrator.add_not_null(model, *field_names)
|
||||
> migrator.add_default(model, field_name, default)
|
||||
> migrator.add_constraint(model, name, sql)
|
||||
> migrator.drop_index(model, *col_names)
|
||||
> migrator.drop_not_null(model, *field_names)
|
||||
> migrator.drop_constraints(model, *constraints)
|
||||
|
||||
"""
|
||||
|
||||
from contextlib import suppress
|
||||
|
||||
import peewee as pw
|
||||
from peewee_migrate import Migrator
|
||||
|
||||
with suppress(ImportError):
|
||||
import playhouse.postgres_ext as pw_pext
|
||||
|
||||
|
||||
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
@migrator.create_model
|
||||
class Memory(pw.Model):
|
||||
id = pw.CharField(max_length=255, unique=True)
|
||||
user_id = pw.CharField(max_length=255)
|
||||
content = pw.TextField(null=False)
|
||||
updated_at = pw.BigIntegerField(null=False)
|
||||
created_at = pw.BigIntegerField(null=False)
|
||||
|
||||
class Meta:
|
||||
table_name = 'memory'
|
||||
|
||||
|
||||
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your rollback migrations here."""
|
||||
|
||||
migrator.remove_model('memory')
|
||||
@@ -0,0 +1,60 @@
|
||||
"""Peewee migrations -- 009_add_models.py.
|
||||
|
||||
Some examples (model - class or model name)::
|
||||
|
||||
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||
> Model = migrator.ModelClass # Return model in current state by name
|
||||
|
||||
> migrator.sql(sql) # Run custom SQL
|
||||
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||
> migrator.change_fields(model, **fields) # Change fields
|
||||
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||
> migrator.rename_table(model, new_table_name)
|
||||
> migrator.add_index(model, *col_names, unique=False)
|
||||
> migrator.add_not_null(model, *field_names)
|
||||
> migrator.add_default(model, field_name, default)
|
||||
> migrator.add_constraint(model, name, sql)
|
||||
> migrator.drop_index(model, *col_names)
|
||||
> migrator.drop_not_null(model, *field_names)
|
||||
> migrator.drop_constraints(model, *constraints)
|
||||
|
||||
"""
|
||||
|
||||
from contextlib import suppress
|
||||
|
||||
import peewee as pw
|
||||
from peewee_migrate import Migrator
|
||||
|
||||
with suppress(ImportError):
|
||||
import playhouse.postgres_ext as pw_pext
|
||||
|
||||
|
||||
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your migrations here."""
|
||||
|
||||
@migrator.create_model
|
||||
class Model(pw.Model):
|
||||
id = pw.TextField(unique=True)
|
||||
user_id = pw.TextField()
|
||||
base_model_id = pw.TextField(null=True)
|
||||
|
||||
name = pw.TextField()
|
||||
|
||||
meta = pw.TextField()
|
||||
params = pw.TextField()
|
||||
|
||||
created_at = pw.BigIntegerField(null=False)
|
||||
updated_at = pw.BigIntegerField(null=False)
|
||||
|
||||
class Meta:
|
||||
table_name = 'model'
|
||||
|
||||
|
||||
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your rollback migrations here."""
|
||||
|
||||
migrator.remove_model('model')
|
||||
@@ -0,0 +1,130 @@
|
||||
"""Peewee migrations -- 009_add_models.py.
|
||||
|
||||
Some examples (model - class or model name)::
|
||||
|
||||
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||
> Model = migrator.ModelClass # Return model in current state by name
|
||||
|
||||
> migrator.sql(sql) # Run custom SQL
|
||||
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||
> migrator.change_fields(model, **fields) # Change fields
|
||||
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||
> migrator.rename_table(model, new_table_name)
|
||||
> migrator.add_index(model, *col_names, unique=False)
|
||||
> migrator.add_not_null(model, *field_names)
|
||||
> migrator.add_default(model, field_name, default)
|
||||
> migrator.add_constraint(model, name, sql)
|
||||
> migrator.drop_index(model, *col_names)
|
||||
> migrator.drop_not_null(model, *field_names)
|
||||
> migrator.drop_constraints(model, *constraints)
|
||||
|
||||
"""
|
||||
|
||||
from contextlib import suppress
|
||||
|
||||
import peewee as pw
|
||||
from peewee_migrate import Migrator
|
||||
import json
|
||||
|
||||
from open_webui.utils.misc import parse_ollama_modelfile
|
||||
|
||||
with suppress(ImportError):
|
||||
import playhouse.postgres_ext as pw_pext
|
||||
|
||||
|
||||
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your migrations here."""
|
||||
|
||||
# Fetch data from 'modelfile' table and insert into 'model' table
|
||||
migrate_modelfile_to_model(migrator, database)
|
||||
# Drop the 'modelfile' table
|
||||
migrator.remove_model('modelfile')
|
||||
|
||||
|
||||
def migrate_modelfile_to_model(migrator: Migrator, database: pw.Database):
|
||||
ModelFile = migrator.orm['modelfile']
|
||||
Model = migrator.orm['model']
|
||||
|
||||
modelfiles = ModelFile.select()
|
||||
|
||||
for modelfile in modelfiles:
|
||||
# Extract and transform data in Python
|
||||
|
||||
modelfile.modelfile = json.loads(modelfile.modelfile)
|
||||
meta = json.dumps(
|
||||
{
|
||||
'description': modelfile.modelfile.get('desc'),
|
||||
'profile_image_url': modelfile.modelfile.get('imageUrl'),
|
||||
'ollama': {'modelfile': modelfile.modelfile.get('content')},
|
||||
'suggestion_prompts': modelfile.modelfile.get('suggestionPrompts'),
|
||||
'categories': modelfile.modelfile.get('categories'),
|
||||
'user': {**modelfile.modelfile.get('user', {}), 'community': True},
|
||||
}
|
||||
)
|
||||
|
||||
info = parse_ollama_modelfile(modelfile.modelfile.get('content'))
|
||||
|
||||
# Insert the processed data into the 'model' table
|
||||
Model.create(
|
||||
id=f'ollama-{modelfile.tag_name}',
|
||||
user_id=modelfile.user_id,
|
||||
base_model_id=info.get('base_model_id'),
|
||||
name=modelfile.modelfile.get('title'),
|
||||
meta=meta,
|
||||
params=json.dumps(info.get('params', {})),
|
||||
created_at=modelfile.timestamp,
|
||||
updated_at=modelfile.timestamp,
|
||||
)
|
||||
|
||||
|
||||
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your rollback migrations here."""
|
||||
|
||||
recreate_modelfile_table(migrator, database)
|
||||
move_data_back_to_modelfile(migrator, database)
|
||||
migrator.remove_model('model')
|
||||
|
||||
|
||||
def recreate_modelfile_table(migrator: Migrator, database: pw.Database):
|
||||
query = """
|
||||
CREATE TABLE IF NOT EXISTS modelfile (
|
||||
user_id TEXT,
|
||||
tag_name TEXT,
|
||||
modelfile JSON,
|
||||
timestamp BIGINT
|
||||
)
|
||||
"""
|
||||
migrator.sql(query)
|
||||
|
||||
|
||||
def move_data_back_to_modelfile(migrator: Migrator, database: pw.Database):
|
||||
Model = migrator.orm['model']
|
||||
Modelfile = migrator.orm['modelfile']
|
||||
|
||||
models = Model.select()
|
||||
|
||||
for model in models:
|
||||
# Extract and transform data in Python
|
||||
meta = json.loads(model.meta)
|
||||
|
||||
modelfile_data = {
|
||||
'title': model.name,
|
||||
'desc': meta.get('description'),
|
||||
'imageUrl': meta.get('profile_image_url'),
|
||||
'content': meta.get('ollama', {}).get('modelfile'),
|
||||
'suggestionPrompts': meta.get('suggestion_prompts'),
|
||||
'categories': meta.get('categories'),
|
||||
'user': {k: v for k, v in meta.get('user', {}).items() if k != 'community'},
|
||||
}
|
||||
|
||||
# Insert the processed data back into the 'modelfile' table
|
||||
Modelfile.create(
|
||||
user_id=model.user_id,
|
||||
tag_name=model.id,
|
||||
modelfile=modelfile_data,
|
||||
timestamp=model.created_at,
|
||||
)
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Peewee migrations -- 002_add_local_sharing.py.
|
||||
|
||||
Some examples (model - class or model name)::
|
||||
|
||||
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||
> Model = migrator.ModelClass # Return model in current state by name
|
||||
|
||||
> migrator.sql(sql) # Run custom SQL
|
||||
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||
> migrator.change_fields(model, **fields) # Change fields
|
||||
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||
> migrator.rename_table(model, new_table_name)
|
||||
> migrator.add_index(model, *col_names, unique=False)
|
||||
> migrator.add_not_null(model, *field_names)
|
||||
> migrator.add_default(model, field_name, default)
|
||||
> migrator.add_constraint(model, name, sql)
|
||||
> migrator.drop_index(model, *col_names)
|
||||
> migrator.drop_not_null(model, *field_names)
|
||||
> migrator.drop_constraints(model, *constraints)
|
||||
|
||||
"""
|
||||
|
||||
from contextlib import suppress
|
||||
|
||||
import peewee as pw
|
||||
from peewee_migrate import Migrator
|
||||
|
||||
with suppress(ImportError):
|
||||
import playhouse.postgres_ext as pw_pext
|
||||
|
||||
|
||||
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your migrations here."""
|
||||
|
||||
# Adding fields settings to the 'user' table
|
||||
migrator.add_fields('user', settings=pw.TextField(null=True))
|
||||
|
||||
|
||||
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your rollback migrations here."""
|
||||
|
||||
# Remove the settings field
|
||||
migrator.remove_fields('user', 'settings')
|
||||
@@ -0,0 +1,60 @@
|
||||
"""Peewee migrations -- 009_add_models.py.
|
||||
|
||||
Some examples (model - class or model name)::
|
||||
|
||||
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||
> Model = migrator.ModelClass # Return model in current state by name
|
||||
|
||||
> migrator.sql(sql) # Run custom SQL
|
||||
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||
> migrator.change_fields(model, **fields) # Change fields
|
||||
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||
> migrator.rename_table(model, new_table_name)
|
||||
> migrator.add_index(model, *col_names, unique=False)
|
||||
> migrator.add_not_null(model, *field_names)
|
||||
> migrator.add_default(model, field_name, default)
|
||||
> migrator.add_constraint(model, name, sql)
|
||||
> migrator.drop_index(model, *col_names)
|
||||
> migrator.drop_not_null(model, *field_names)
|
||||
> migrator.drop_constraints(model, *constraints)
|
||||
|
||||
"""
|
||||
|
||||
from contextlib import suppress
|
||||
|
||||
import peewee as pw
|
||||
from peewee_migrate import Migrator
|
||||
|
||||
with suppress(ImportError):
|
||||
import playhouse.postgres_ext as pw_pext
|
||||
|
||||
|
||||
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your migrations here."""
|
||||
|
||||
@migrator.create_model
|
||||
class Tool(pw.Model):
|
||||
id = pw.TextField(unique=True)
|
||||
user_id = pw.TextField()
|
||||
|
||||
name = pw.TextField()
|
||||
content = pw.TextField()
|
||||
specs = pw.TextField()
|
||||
|
||||
meta = pw.TextField()
|
||||
|
||||
created_at = pw.BigIntegerField(null=False)
|
||||
updated_at = pw.BigIntegerField(null=False)
|
||||
|
||||
class Meta:
|
||||
table_name = 'tool'
|
||||
|
||||
|
||||
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your rollback migrations here."""
|
||||
|
||||
migrator.remove_model('tool')
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Peewee migrations -- 002_add_local_sharing.py.
|
||||
|
||||
Some examples (model - class or model name)::
|
||||
|
||||
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||
> Model = migrator.ModelClass # Return model in current state by name
|
||||
|
||||
> migrator.sql(sql) # Run custom SQL
|
||||
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||
> migrator.change_fields(model, **fields) # Change fields
|
||||
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||
> migrator.rename_table(model, new_table_name)
|
||||
> migrator.add_index(model, *col_names, unique=False)
|
||||
> migrator.add_not_null(model, *field_names)
|
||||
> migrator.add_default(model, field_name, default)
|
||||
> migrator.add_constraint(model, name, sql)
|
||||
> migrator.drop_index(model, *col_names)
|
||||
> migrator.drop_not_null(model, *field_names)
|
||||
> migrator.drop_constraints(model, *constraints)
|
||||
|
||||
"""
|
||||
|
||||
from contextlib import suppress
|
||||
|
||||
import peewee as pw
|
||||
from peewee_migrate import Migrator
|
||||
|
||||
with suppress(ImportError):
|
||||
import playhouse.postgres_ext as pw_pext
|
||||
|
||||
|
||||
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your migrations here."""
|
||||
|
||||
# Adding fields info to the 'user' table
|
||||
migrator.add_fields('user', info=pw.TextField(null=True))
|
||||
|
||||
|
||||
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your rollback migrations here."""
|
||||
|
||||
# Remove the settings field
|
||||
migrator.remove_fields('user', 'info')
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Peewee migrations -- 009_add_models.py.
|
||||
|
||||
Some examples (model - class or model name)::
|
||||
|
||||
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||
> Model = migrator.ModelClass # Return model in current state by name
|
||||
|
||||
> migrator.sql(sql) # Run custom SQL
|
||||
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||
> migrator.change_fields(model, **fields) # Change fields
|
||||
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||
> migrator.rename_table(model, new_table_name)
|
||||
> migrator.add_index(model, *col_names, unique=False)
|
||||
> migrator.add_not_null(model, *field_names)
|
||||
> migrator.add_default(model, field_name, default)
|
||||
> migrator.add_constraint(model, name, sql)
|
||||
> migrator.drop_index(model, *col_names)
|
||||
> migrator.drop_not_null(model, *field_names)
|
||||
> migrator.drop_constraints(model, *constraints)
|
||||
|
||||
"""
|
||||
|
||||
from contextlib import suppress
|
||||
|
||||
import peewee as pw
|
||||
from peewee_migrate import Migrator
|
||||
|
||||
with suppress(ImportError):
|
||||
import playhouse.postgres_ext as pw_pext
|
||||
|
||||
|
||||
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your migrations here."""
|
||||
|
||||
@migrator.create_model
|
||||
class File(pw.Model):
|
||||
id = pw.TextField(unique=True)
|
||||
user_id = pw.TextField()
|
||||
filename = pw.TextField()
|
||||
meta = pw.TextField()
|
||||
created_at = pw.BigIntegerField(null=False)
|
||||
|
||||
class Meta:
|
||||
table_name = 'file'
|
||||
|
||||
|
||||
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your rollback migrations here."""
|
||||
|
||||
migrator.remove_model('file')
|
||||
@@ -0,0 +1,60 @@
|
||||
"""Peewee migrations -- 009_add_models.py.
|
||||
|
||||
Some examples (model - class or model name)::
|
||||
|
||||
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||
> Model = migrator.ModelClass # Return model in current state by name
|
||||
|
||||
> migrator.sql(sql) # Run custom SQL
|
||||
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||
> migrator.change_fields(model, **fields) # Change fields
|
||||
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||
> migrator.rename_table(model, new_table_name)
|
||||
> migrator.add_index(model, *col_names, unique=False)
|
||||
> migrator.add_not_null(model, *field_names)
|
||||
> migrator.add_default(model, field_name, default)
|
||||
> migrator.add_constraint(model, name, sql)
|
||||
> migrator.drop_index(model, *col_names)
|
||||
> migrator.drop_not_null(model, *field_names)
|
||||
> migrator.drop_constraints(model, *constraints)
|
||||
|
||||
"""
|
||||
|
||||
from contextlib import suppress
|
||||
|
||||
import peewee as pw
|
||||
from peewee_migrate import Migrator
|
||||
|
||||
with suppress(ImportError):
|
||||
import playhouse.postgres_ext as pw_pext
|
||||
|
||||
|
||||
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your migrations here."""
|
||||
|
||||
@migrator.create_model
|
||||
class Function(pw.Model):
|
||||
id = pw.TextField(unique=True)
|
||||
user_id = pw.TextField()
|
||||
|
||||
name = pw.TextField()
|
||||
type = pw.TextField()
|
||||
|
||||
content = pw.TextField()
|
||||
meta = pw.TextField()
|
||||
|
||||
created_at = pw.BigIntegerField(null=False)
|
||||
updated_at = pw.BigIntegerField(null=False)
|
||||
|
||||
class Meta:
|
||||
table_name = 'function'
|
||||
|
||||
|
||||
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your rollback migrations here."""
|
||||
|
||||
migrator.remove_model('function')
|
||||
@@ -0,0 +1,49 @@
|
||||
"""Peewee migrations -- 009_add_models.py.
|
||||
|
||||
Some examples (model - class or model name)::
|
||||
|
||||
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||
> Model = migrator.ModelClass # Return model in current state by name
|
||||
|
||||
> migrator.sql(sql) # Run custom SQL
|
||||
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||
> migrator.change_fields(model, **fields) # Change fields
|
||||
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||
> migrator.rename_table(model, new_table_name)
|
||||
> migrator.add_index(model, *col_names, unique=False)
|
||||
> migrator.add_not_null(model, *field_names)
|
||||
> migrator.add_default(model, field_name, default)
|
||||
> migrator.add_constraint(model, name, sql)
|
||||
> migrator.drop_index(model, *col_names)
|
||||
> migrator.drop_not_null(model, *field_names)
|
||||
> migrator.drop_constraints(model, *constraints)
|
||||
|
||||
"""
|
||||
|
||||
from contextlib import suppress
|
||||
|
||||
import peewee as pw
|
||||
from peewee_migrate import Migrator
|
||||
|
||||
with suppress(ImportError):
|
||||
import playhouse.postgres_ext as pw_pext
|
||||
|
||||
|
||||
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your migrations here."""
|
||||
|
||||
migrator.add_fields('tool', valves=pw.TextField(null=True))
|
||||
migrator.add_fields('function', valves=pw.TextField(null=True))
|
||||
migrator.add_fields('function', is_active=pw.BooleanField(default=False))
|
||||
|
||||
|
||||
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your rollback migrations here."""
|
||||
|
||||
migrator.remove_fields('tool', 'valves')
|
||||
migrator.remove_fields('function', 'valves')
|
||||
migrator.remove_fields('function', 'is_active')
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Peewee migrations -- 017_add_user_oauth_sub.py.
|
||||
Some examples (model - class or model name)::
|
||||
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||
> Model = migrator.ModelClass # Return model in current state by name
|
||||
> migrator.sql(sql) # Run custom SQL
|
||||
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||
> migrator.change_fields(model, **fields) # Change fields
|
||||
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||
> migrator.rename_table(model, new_table_name)
|
||||
> migrator.add_index(model, *col_names, unique=False)
|
||||
> migrator.add_not_null(model, *field_names)
|
||||
> migrator.add_default(model, field_name, default)
|
||||
> migrator.add_constraint(model, name, sql)
|
||||
> migrator.drop_index(model, *col_names)
|
||||
> migrator.drop_not_null(model, *field_names)
|
||||
> migrator.drop_constraints(model, *constraints)
|
||||
"""
|
||||
|
||||
from contextlib import suppress
|
||||
|
||||
import peewee as pw
|
||||
from peewee_migrate import Migrator
|
||||
|
||||
with suppress(ImportError):
|
||||
import playhouse.postgres_ext as pw_pext
|
||||
|
||||
|
||||
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your migrations here."""
|
||||
|
||||
migrator.add_fields(
|
||||
'user',
|
||||
oauth_sub=pw.TextField(null=True, unique=True),
|
||||
)
|
||||
|
||||
|
||||
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your rollback migrations here."""
|
||||
|
||||
migrator.remove_fields('user', 'oauth_sub')
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Peewee migrations -- 017_add_user_oauth_sub.py.
|
||||
|
||||
Some examples (model - class or model name)::
|
||||
|
||||
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||
> Model = migrator.ModelClass # Return model in current state by name
|
||||
|
||||
> migrator.sql(sql) # Run custom SQL
|
||||
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||
> migrator.change_fields(model, **fields) # Change fields
|
||||
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||
> migrator.rename_table(model, new_table_name)
|
||||
> migrator.add_index(model, *col_names, unique=False)
|
||||
> migrator.add_not_null(model, *field_names)
|
||||
> migrator.add_default(model, field_name, default)
|
||||
> migrator.add_constraint(model, name, sql)
|
||||
> migrator.drop_index(model, *col_names)
|
||||
> migrator.drop_not_null(model, *field_names)
|
||||
> migrator.drop_constraints(model, *constraints)
|
||||
|
||||
"""
|
||||
|
||||
from contextlib import suppress
|
||||
|
||||
import peewee as pw
|
||||
from peewee_migrate import Migrator
|
||||
|
||||
with suppress(ImportError):
|
||||
import playhouse.postgres_ext as pw_pext
|
||||
|
||||
|
||||
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your migrations here."""
|
||||
|
||||
migrator.add_fields(
|
||||
'function',
|
||||
is_global=pw.BooleanField(default=False),
|
||||
)
|
||||
|
||||
|
||||
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your rollback migrations here."""
|
||||
|
||||
migrator.remove_fields('function', 'is_global')
|
||||
@@ -0,0 +1,84 @@
|
||||
import logging
|
||||
import os
|
||||
from contextvars import ContextVar
|
||||
|
||||
from peewee import *
|
||||
from peewee import InterfaceError as PeeWeeInterfaceError
|
||||
from peewee import PostgresqlDatabase
|
||||
from playhouse.db_url import connect, parse
|
||||
from playhouse.shortcuts import ReconnectMixin
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
db_state_default = {'closed': None, 'conn': None, 'ctx': None, 'transactions': None}
|
||||
db_state = ContextVar('db_state', default=db_state_default.copy())
|
||||
|
||||
|
||||
class PeeweeConnectionState(object):
|
||||
def __init__(self, **kwargs):
|
||||
super().__setattr__('_state', db_state)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
self._state.get()[name] = value
|
||||
|
||||
def __getattr__(self, name):
|
||||
value = self._state.get()[name]
|
||||
return value
|
||||
|
||||
|
||||
class CustomReconnectMixin(ReconnectMixin):
|
||||
reconnect_errors = (
|
||||
# psycopg2
|
||||
(OperationalError, 'termin'),
|
||||
(InterfaceError, 'closed'),
|
||||
# peewee
|
||||
(PeeWeeInterfaceError, 'closed'),
|
||||
)
|
||||
|
||||
|
||||
class ReconnectingPostgresqlDatabase(CustomReconnectMixin, PostgresqlDatabase):
|
||||
pass
|
||||
|
||||
|
||||
def register_connection(db_url):
|
||||
# Check if using SQLCipher protocol
|
||||
if db_url.startswith('sqlite+sqlcipher://'):
|
||||
database_password = os.environ.get('DATABASE_PASSWORD')
|
||||
if not database_password or database_password.strip() == '':
|
||||
raise ValueError('DATABASE_PASSWORD is required when using sqlite+sqlcipher:// URLs')
|
||||
from playhouse.sqlcipher_ext import SqlCipherDatabase
|
||||
|
||||
# Parse the database path from SQLCipher URL
|
||||
# Convert sqlite+sqlcipher:///path/to/db.sqlite to /path/to/db.sqlite
|
||||
db_path = db_url.replace('sqlite+sqlcipher://', '')
|
||||
|
||||
# Use Peewee's native SqlCipherDatabase with encryption
|
||||
db = SqlCipherDatabase(db_path, passphrase=database_password)
|
||||
db.autoconnect = True
|
||||
db.reuse_if_open = True
|
||||
log.info('Connected to encrypted SQLite database using SQLCipher')
|
||||
|
||||
else:
|
||||
# Standard database connection (existing logic)
|
||||
db = connect(db_url, unquote_user=True, unquote_password=True)
|
||||
if isinstance(db, PostgresqlDatabase):
|
||||
# Enable autoconnect for SQLite databases, managed by Peewee
|
||||
db.autoconnect = True
|
||||
db.reuse_if_open = True
|
||||
log.info('Connected to PostgreSQL database')
|
||||
|
||||
# Get the connection details
|
||||
connection = parse(db_url, unquote_user=True, unquote_password=True)
|
||||
|
||||
# Use our custom database class that supports reconnection
|
||||
db = ReconnectingPostgresqlDatabase(**connection)
|
||||
db.connect(reuse_if_open=True)
|
||||
elif isinstance(db, SqliteDatabase):
|
||||
# Enable autoconnect for SQLite databases, managed by Peewee
|
||||
db.autoconnect = True
|
||||
db.reuse_if_open = True
|
||||
log.info('Connected to SQLite database')
|
||||
else:
|
||||
raise ValueError('Unsupported database connection')
|
||||
return db
|
||||
+1218
-1038
@@ -1,202 +1,62 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
from contextlib import asynccontextmanager
|
||||
import random
|
||||
import re
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
from urllib.parse import urlencode, parse_qs, urlparse
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import text
|
||||
|
||||
from typing import Optional
|
||||
from aiocache import cached
|
||||
import aiohttp
|
||||
import anyio.to_thread
|
||||
|
||||
from redis import Redis
|
||||
|
||||
|
||||
from fastapi import (
|
||||
Depends,
|
||||
FastAPI,
|
||||
File,
|
||||
Form,
|
||||
HTTPException,
|
||||
Request,
|
||||
applications,
|
||||
UploadFile,
|
||||
status,
|
||||
applications,
|
||||
BackgroundTasks,
|
||||
)
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.openapi.docs import get_swagger_ui_html
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from starlette.datastructures import Headers
|
||||
|
||||
from starlette_compress import CompressMiddleware
|
||||
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
from starlette.responses import Response, StreamingResponse
|
||||
from starlette_compress import CompressMiddleware
|
||||
from starsessions import (
|
||||
SessionAutoloadMiddleware,
|
||||
)
|
||||
from starlette.datastructures import Headers
|
||||
|
||||
from starsessions import (
|
||||
SessionMiddleware as StarSessionsMiddleware,
|
||||
SessionAutoloadMiddleware,
|
||||
)
|
||||
from starsessions.stores.redis import RedisStore
|
||||
|
||||
from open_webui.config import (
|
||||
BYPASS_ADMIN_ACCESS_CONTROL,
|
||||
CACHE_DIR,
|
||||
CORS_ALLOW_ORIGIN,
|
||||
DEFAULT_LOCALE,
|
||||
ENABLE_ADMIN_ANALYTICS,
|
||||
# Admin
|
||||
ENABLE_ADMIN_CHAT_ACCESS,
|
||||
ENABLE_ADMIN_EXPORT,
|
||||
ENABLE_ONEDRIVE_BUSINESS,
|
||||
ENABLE_ONEDRIVE_PERSONAL,
|
||||
# OpenAI
|
||||
ENV,
|
||||
FRONTEND_BUILD_DIR,
|
||||
GOOGLE_DRIVE_API_KEY,
|
||||
GOOGLE_DRIVE_CLIENT_ID,
|
||||
IFRAME_CSP,
|
||||
OAUTH_PROVIDERS,
|
||||
ONEDRIVE_CLIENT_ID_BUSINESS,
|
||||
ONEDRIVE_CLIENT_ID_PERSONAL,
|
||||
ONEDRIVE_SHAREPOINT_TENANT_ID,
|
||||
ONEDRIVE_SHAREPOINT_URL,
|
||||
STATIC_DIR,
|
||||
THREAD_POOL_SIZE,
|
||||
WEBUI_AUTH,
|
||||
WEBUI_NAME,
|
||||
async_reset_config,
|
||||
import_legacy_config_json,
|
||||
seed_registered_defaults,
|
||||
)
|
||||
from open_webui.constants import ERROR_MESSAGES, TASKS
|
||||
from open_webui.env import (
|
||||
AIOHTTP_CLIENT_SESSION_SSL,
|
||||
AUDIT_EXCLUDED_PATHS,
|
||||
AUDIT_INCLUDED_PATHS,
|
||||
AUDIT_LOG_LEVEL,
|
||||
BYPASS_MODEL_ACCESS_CONTROL,
|
||||
CHANGELOG,
|
||||
DEPLOYMENT_ID,
|
||||
ENABLE_AUDIT_GET_REQUESTS,
|
||||
ENABLE_COMPRESSION_MIDDLEWARE,
|
||||
ENABLE_CUSTOM_MODEL_FALLBACK,
|
||||
ENABLE_EASTER_EGGS,
|
||||
EXTERNAL_PWA_MANIFEST_URL,
|
||||
# OAuth Back-Channel Logout
|
||||
ENABLE_OAUTH_BACKCHANNEL_LOGOUT,
|
||||
ENABLE_OTEL,
|
||||
ENABLE_PUBLIC_ACTIVE_USERS_COUNT,
|
||||
# SCIM
|
||||
ENABLE_SCIM,
|
||||
ENABLE_SIGNUP_PASSWORD_CONFIRMATION,
|
||||
ENABLE_STAR_SESSIONS_MIDDLEWARE,
|
||||
ENABLE_PYODIDE_FILE_PERSISTENCE,
|
||||
ENABLE_VERSION_UPDATE_CHECK,
|
||||
ENABLE_WEBSOCKET_SUPPORT,
|
||||
GLOBAL_LOG_LEVEL,
|
||||
INSTANCE_ID,
|
||||
LICENSE_KEY,
|
||||
LOG_FORMAT,
|
||||
MAX_BODY_LOG_SIZE,
|
||||
# Redis
|
||||
REDIS_KEY_PREFIX,
|
||||
REDIS_URL,
|
||||
RESET_CONFIG_ON_START,
|
||||
SAFE_MODE,
|
||||
SCIM_TOKEN,
|
||||
VERSION,
|
||||
# Admin Account Runtime Creation
|
||||
WEBUI_ADMIN_EMAIL,
|
||||
WEBUI_ADMIN_NAME,
|
||||
WEBUI_ADMIN_PASSWORD,
|
||||
WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
|
||||
WEBUI_BUILD_HASH,
|
||||
WEBUI_SECRET_KEY,
|
||||
WEBUI_SESSION_COOKIE_SAME_SITE,
|
||||
WEBUI_SESSION_COOKIE_SECURE,
|
||||
)
|
||||
from open_webui.events import (
|
||||
EVENTS,
|
||||
delete_event_webhook,
|
||||
get_event_catalog as get_event_catalog_items,
|
||||
get_event_webhooks,
|
||||
migrate_legacy_webhook_config,
|
||||
publish_event,
|
||||
upsert_event_webhook,
|
||||
)
|
||||
from open_webui.internal.db import engine, get_async_session
|
||||
from open_webui.models.access_grants import AccessGrants
|
||||
from open_webui.models.channels import Channels
|
||||
from open_webui.models.chats import ChatForm, Chats
|
||||
from open_webui.models.config import Config
|
||||
from open_webui.models.functions import Functions
|
||||
from open_webui.models.messages import Messages
|
||||
from open_webui.models.models import Models
|
||||
from open_webui.models.users import Users
|
||||
from open_webui.routers import (
|
||||
analytics,
|
||||
audio,
|
||||
auths,
|
||||
automations,
|
||||
calendar,
|
||||
channels,
|
||||
chats,
|
||||
configs,
|
||||
evaluations,
|
||||
files,
|
||||
folders,
|
||||
functions,
|
||||
groups,
|
||||
images,
|
||||
knowledge,
|
||||
memories,
|
||||
models,
|
||||
notes,
|
||||
ollama,
|
||||
openai,
|
||||
pipelines,
|
||||
prompts,
|
||||
retrieval,
|
||||
scim,
|
||||
skills,
|
||||
tasks,
|
||||
terminals,
|
||||
tools,
|
||||
users,
|
||||
utils,
|
||||
)
|
||||
from open_webui.routers.retrieval import (
|
||||
get_ef,
|
||||
get_embedding_function,
|
||||
get_reranking_function,
|
||||
get_rf,
|
||||
)
|
||||
from open_webui.socket.main import (
|
||||
MODELS,
|
||||
get_event_emitter,
|
||||
get_models_in_use,
|
||||
get_user_id_from_session_pool,
|
||||
periodic_session_pool_cleanup,
|
||||
periodic_usage_pool_cleanup,
|
||||
)
|
||||
from open_webui.socket.main import (
|
||||
app as socket_app,
|
||||
)
|
||||
from open_webui.tasks import (
|
||||
cleanup_task,
|
||||
create_task,
|
||||
has_active_tasks,
|
||||
list_task_ids_by_item_id,
|
||||
list_tasks,
|
||||
redis_task_command_listener,
|
||||
stop_item_tasks,
|
||||
stop_task,
|
||||
) # Import from tasks.py
|
||||
from open_webui.utils import logger
|
||||
from open_webui.utils.access_control import has_permission
|
||||
from open_webui.utils.actions import chat_action as chat_action_handler
|
||||
from open_webui.utils.asgi_middleware import (
|
||||
AuthTokenMiddleware,
|
||||
CommitSessionMiddleware,
|
||||
@@ -204,51 +64,532 @@ from open_webui.utils.asgi_middleware import (
|
||||
WebsocketUpgradeGuardMiddleware,
|
||||
)
|
||||
from open_webui.utils.audit import AuditLevel, AuditLoggingMiddleware
|
||||
from open_webui.utils.auth import (
|
||||
create_admin_user,
|
||||
decode_token,
|
||||
get_admin_user,
|
||||
get_http_authorization_cred,
|
||||
get_license_data,
|
||||
get_verified_user,
|
||||
from open_webui.utils.logger import start_logger
|
||||
from open_webui.utils.session_pool import get_session
|
||||
from open_webui.socket.main import (
|
||||
MODELS,
|
||||
app as socket_app,
|
||||
periodic_usage_pool_cleanup,
|
||||
periodic_session_pool_cleanup,
|
||||
get_event_emitter,
|
||||
get_models_in_use,
|
||||
get_user_id_from_session_pool,
|
||||
)
|
||||
from open_webui.utils.chat import (
|
||||
chat_completed as chat_completed_handler,
|
||||
from open_webui.routers import (
|
||||
analytics,
|
||||
audio,
|
||||
images,
|
||||
ollama,
|
||||
openai,
|
||||
retrieval,
|
||||
pipelines,
|
||||
tasks,
|
||||
auths,
|
||||
channels,
|
||||
chats,
|
||||
notes,
|
||||
folders,
|
||||
configs,
|
||||
groups,
|
||||
files,
|
||||
functions,
|
||||
memories,
|
||||
models,
|
||||
knowledge,
|
||||
prompts,
|
||||
evaluations,
|
||||
skills,
|
||||
tools,
|
||||
users,
|
||||
utils,
|
||||
scim,
|
||||
terminals,
|
||||
automations,
|
||||
calendar,
|
||||
)
|
||||
|
||||
from open_webui.routers.retrieval import (
|
||||
get_embedding_function,
|
||||
get_reranking_function,
|
||||
get_ef,
|
||||
get_rf,
|
||||
)
|
||||
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from open_webui.internal.db import ScopedSession, engine, get_async_session
|
||||
|
||||
from open_webui.models.functions import Functions
|
||||
from open_webui.models.models import Models
|
||||
from open_webui.models.users import UserModel, Users
|
||||
from open_webui.models.chats import Chats, ChatForm
|
||||
|
||||
from open_webui.config import (
|
||||
# Ollama
|
||||
ENABLE_OLLAMA_API,
|
||||
OLLAMA_BASE_URLS,
|
||||
OLLAMA_API_CONFIGS,
|
||||
# OpenAI
|
||||
ENABLE_OPENAI_API,
|
||||
OPENAI_API_BASE_URLS,
|
||||
OPENAI_API_KEYS,
|
||||
OPENAI_API_CONFIGS,
|
||||
# Direct Connections
|
||||
ENABLE_DIRECT_CONNECTIONS,
|
||||
# Model list
|
||||
ENABLE_BASE_MODELS_CACHE,
|
||||
# Thread pool size for FastAPI/AnyIO
|
||||
THREAD_POOL_SIZE,
|
||||
# Tool Server Configs
|
||||
TOOL_SERVER_CONNECTIONS,
|
||||
# Terminal Server
|
||||
TERMINAL_SERVER_CONNECTIONS,
|
||||
# Code Execution
|
||||
ENABLE_CODE_EXECUTION,
|
||||
CODE_EXECUTION_ENGINE,
|
||||
CODE_EXECUTION_JUPYTER_URL,
|
||||
CODE_EXECUTION_JUPYTER_AUTH,
|
||||
CODE_EXECUTION_JUPYTER_AUTH_TOKEN,
|
||||
CODE_EXECUTION_JUPYTER_AUTH_PASSWORD,
|
||||
CODE_EXECUTION_JUPYTER_TIMEOUT,
|
||||
ENABLE_CODE_INTERPRETER,
|
||||
CODE_INTERPRETER_ENGINE,
|
||||
CODE_INTERPRETER_PROMPT_TEMPLATE,
|
||||
CODE_INTERPRETER_JUPYTER_URL,
|
||||
CODE_INTERPRETER_JUPYTER_AUTH,
|
||||
CODE_INTERPRETER_JUPYTER_AUTH_TOKEN,
|
||||
CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD,
|
||||
CODE_INTERPRETER_JUPYTER_TIMEOUT,
|
||||
ENABLE_MEMORIES,
|
||||
# Image
|
||||
AUTOMATIC1111_API_AUTH,
|
||||
AUTOMATIC1111_BASE_URL,
|
||||
AUTOMATIC1111_PARAMS,
|
||||
COMFYUI_BASE_URL,
|
||||
COMFYUI_API_KEY,
|
||||
COMFYUI_WORKFLOW,
|
||||
COMFYUI_WORKFLOW_NODES,
|
||||
ENABLE_IMAGE_GENERATION,
|
||||
ENABLE_IMAGE_PROMPT_GENERATION,
|
||||
IMAGE_GENERATION_ENGINE,
|
||||
IMAGE_GENERATION_MODEL,
|
||||
IMAGE_SIZE,
|
||||
IMAGE_STEPS,
|
||||
IMAGES_OPENAI_API_BASE_URL,
|
||||
IMAGES_OPENAI_API_VERSION,
|
||||
IMAGES_OPENAI_API_KEY,
|
||||
IMAGES_OPENAI_API_PARAMS,
|
||||
IMAGES_GEMINI_API_BASE_URL,
|
||||
IMAGES_GEMINI_API_KEY,
|
||||
IMAGES_GEMINI_ENDPOINT_METHOD,
|
||||
ENABLE_IMAGE_EDIT,
|
||||
IMAGE_EDIT_ENGINE,
|
||||
IMAGE_EDIT_MODEL,
|
||||
IMAGE_EDIT_SIZE,
|
||||
IMAGES_EDIT_OPENAI_API_BASE_URL,
|
||||
IMAGES_EDIT_OPENAI_API_KEY,
|
||||
IMAGES_EDIT_OPENAI_API_VERSION,
|
||||
IMAGES_EDIT_GEMINI_API_BASE_URL,
|
||||
IMAGES_EDIT_GEMINI_API_KEY,
|
||||
IMAGES_EDIT_COMFYUI_BASE_URL,
|
||||
IMAGES_EDIT_COMFYUI_API_KEY,
|
||||
IMAGES_EDIT_COMFYUI_WORKFLOW,
|
||||
IMAGES_EDIT_COMFYUI_WORKFLOW_NODES,
|
||||
# Audio
|
||||
AUDIO_STT_ENGINE,
|
||||
AUDIO_STT_MODEL,
|
||||
AUDIO_STT_SUPPORTED_CONTENT_TYPES,
|
||||
AUDIO_STT_OPENAI_API_BASE_URL,
|
||||
AUDIO_STT_OPENAI_API_KEY,
|
||||
AUDIO_STT_AZURE_API_KEY,
|
||||
AUDIO_STT_AZURE_REGION,
|
||||
AUDIO_STT_AZURE_LOCALES,
|
||||
AUDIO_STT_AZURE_BASE_URL,
|
||||
AUDIO_STT_AZURE_MAX_SPEAKERS,
|
||||
AUDIO_STT_MISTRAL_API_KEY,
|
||||
AUDIO_STT_MISTRAL_API_BASE_URL,
|
||||
AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS,
|
||||
AUDIO_TTS_ENGINE,
|
||||
AUDIO_TTS_MODEL,
|
||||
AUDIO_TTS_VOICE,
|
||||
AUDIO_TTS_OPENAI_API_BASE_URL,
|
||||
AUDIO_TTS_OPENAI_API_KEY,
|
||||
AUDIO_TTS_OPENAI_PARAMS,
|
||||
AUDIO_TTS_API_KEY,
|
||||
AUDIO_TTS_SPLIT_ON,
|
||||
AUDIO_TTS_AZURE_SPEECH_REGION,
|
||||
AUDIO_TTS_AZURE_SPEECH_BASE_URL,
|
||||
AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT,
|
||||
AUDIO_TTS_MISTRAL_API_KEY,
|
||||
AUDIO_TTS_MISTRAL_API_BASE_URL,
|
||||
PLAYWRIGHT_WS_URL,
|
||||
PLAYWRIGHT_TIMEOUT,
|
||||
FIRECRAWL_API_BASE_URL,
|
||||
FIRECRAWL_API_KEY,
|
||||
FIRECRAWL_TIMEOUT,
|
||||
WEB_LOADER_ENGINE,
|
||||
WEB_LOADER_CONCURRENT_REQUESTS,
|
||||
WEB_LOADER_TIMEOUT,
|
||||
WHISPER_MODEL,
|
||||
WHISPER_VAD_FILTER,
|
||||
WHISPER_LANGUAGE,
|
||||
DEEPGRAM_API_KEY,
|
||||
WHISPER_MODEL_AUTO_UPDATE,
|
||||
WHISPER_MODEL_DIR,
|
||||
# Retrieval
|
||||
RAG_TEMPLATE,
|
||||
DEFAULT_RAG_TEMPLATE,
|
||||
RAG_FULL_CONTEXT,
|
||||
BYPASS_EMBEDDING_AND_RETRIEVAL,
|
||||
RAG_EMBEDDING_MODEL,
|
||||
RAG_EMBEDDING_MODEL_AUTO_UPDATE,
|
||||
RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
|
||||
RAG_RERANKING_ENGINE,
|
||||
RAG_RERANKING_MODEL,
|
||||
RAG_EXTERNAL_RERANKER_URL,
|
||||
RAG_EXTERNAL_RERANKER_API_KEY,
|
||||
RAG_EXTERNAL_RERANKER_TIMEOUT,
|
||||
RAG_RERANKING_BATCH_SIZE,
|
||||
RAG_RERANKING_MODEL_AUTO_UPDATE,
|
||||
RAG_RERANKING_MODEL_TRUST_REMOTE_CODE,
|
||||
RAG_EMBEDDING_ENGINE,
|
||||
RAG_EMBEDDING_BATCH_SIZE,
|
||||
ENABLE_ASYNC_EMBEDDING,
|
||||
RAG_EMBEDDING_CONCURRENT_REQUESTS,
|
||||
RAG_TOP_K,
|
||||
RAG_TOP_K_RERANKER,
|
||||
RAG_RELEVANCE_THRESHOLD,
|
||||
RAG_HYBRID_BM25_WEIGHT,
|
||||
RAG_ALLOWED_FILE_EXTENSIONS,
|
||||
RAG_FILE_MAX_COUNT,
|
||||
RAG_FILE_MAX_SIZE,
|
||||
FILE_IMAGE_COMPRESSION_WIDTH,
|
||||
FILE_IMAGE_COMPRESSION_HEIGHT,
|
||||
RAG_OPENAI_API_BASE_URL,
|
||||
RAG_OPENAI_API_KEY,
|
||||
RAG_AZURE_OPENAI_BASE_URL,
|
||||
RAG_AZURE_OPENAI_API_KEY,
|
||||
RAG_AZURE_OPENAI_API_VERSION,
|
||||
RAG_OLLAMA_BASE_URL,
|
||||
RAG_OLLAMA_API_KEY,
|
||||
CHUNK_OVERLAP,
|
||||
CHUNK_MIN_SIZE_TARGET,
|
||||
CHUNK_SIZE,
|
||||
CONTENT_EXTRACTION_ENGINE,
|
||||
DATALAB_MARKER_API_KEY,
|
||||
DATALAB_MARKER_API_BASE_URL,
|
||||
DATALAB_MARKER_ADDITIONAL_CONFIG,
|
||||
DATALAB_MARKER_SKIP_CACHE,
|
||||
DATALAB_MARKER_FORCE_OCR,
|
||||
DATALAB_MARKER_PAGINATE,
|
||||
DATALAB_MARKER_STRIP_EXISTING_OCR,
|
||||
DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION,
|
||||
DATALAB_MARKER_FORMAT_LINES,
|
||||
DATALAB_MARKER_OUTPUT_FORMAT,
|
||||
MINERU_API_MODE,
|
||||
MINERU_API_URL,
|
||||
MINERU_API_KEY,
|
||||
MINERU_API_TIMEOUT,
|
||||
MINERU_PARAMS,
|
||||
DATALAB_MARKER_USE_LLM,
|
||||
EXTERNAL_DOCUMENT_LOADER_URL,
|
||||
EXTERNAL_DOCUMENT_LOADER_API_KEY,
|
||||
TIKA_SERVER_URL,
|
||||
DOCLING_SERVER_URL,
|
||||
DOCLING_API_KEY,
|
||||
DOCLING_PARAMS,
|
||||
DOCUMENT_INTELLIGENCE_ENDPOINT,
|
||||
DOCUMENT_INTELLIGENCE_KEY,
|
||||
DOCUMENT_INTELLIGENCE_MODEL,
|
||||
MISTRAL_OCR_API_BASE_URL,
|
||||
MISTRAL_OCR_API_KEY,
|
||||
PADDLEOCR_VL_BASE_URL,
|
||||
PADDLEOCR_VL_TOKEN,
|
||||
RAG_TEXT_SPLITTER,
|
||||
ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER,
|
||||
TIKTOKEN_ENCODING_NAME,
|
||||
PDF_EXTRACT_IMAGES,
|
||||
PDF_LOADER_MODE,
|
||||
YOUTUBE_LOADER_LANGUAGE,
|
||||
YOUTUBE_LOADER_PROXY_URL,
|
||||
# Retrieval (Web Search)
|
||||
ENABLE_WEB_SEARCH,
|
||||
WEB_SEARCH_ENGINE,
|
||||
BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL,
|
||||
BYPASS_WEB_SEARCH_WEB_LOADER,
|
||||
WEB_SEARCH_RESULT_COUNT,
|
||||
WEB_SEARCH_CONCURRENT_REQUESTS,
|
||||
WEB_FETCH_MAX_CONTENT_LENGTH,
|
||||
WEB_SEARCH_TRUST_ENV,
|
||||
WEB_SEARCH_DOMAIN_FILTER_LIST,
|
||||
OLLAMA_CLOUD_WEB_SEARCH_API_KEY,
|
||||
JINA_API_KEY,
|
||||
JINA_API_BASE_URL,
|
||||
SEARCHAPI_API_KEY,
|
||||
SEARCHAPI_ENGINE,
|
||||
SERPAPI_API_KEY,
|
||||
SERPAPI_ENGINE,
|
||||
SEARXNG_QUERY_URL,
|
||||
SEARXNG_LANGUAGE,
|
||||
YACY_QUERY_URL,
|
||||
YACY_USERNAME,
|
||||
YACY_PASSWORD,
|
||||
SERPER_API_KEY,
|
||||
SERPLY_API_KEY,
|
||||
DDGS_BACKEND,
|
||||
SERPSTACK_API_KEY,
|
||||
SERPSTACK_HTTPS,
|
||||
TAVILY_API_KEY,
|
||||
TAVILY_EXTRACT_DEPTH,
|
||||
BING_SEARCH_V7_ENDPOINT,
|
||||
BING_SEARCH_V7_SUBSCRIPTION_KEY,
|
||||
BRAVE_SEARCH_API_KEY,
|
||||
EXA_API_KEY,
|
||||
PERPLEXITY_API_KEY,
|
||||
PERPLEXITY_MODEL,
|
||||
PERPLEXITY_SEARCH_CONTEXT_USAGE,
|
||||
PERPLEXITY_SEARCH_API_URL,
|
||||
SOUGOU_API_SID,
|
||||
SOUGOU_API_SK,
|
||||
KAGI_SEARCH_API_KEY,
|
||||
MOJEEK_SEARCH_API_KEY,
|
||||
BOCHA_SEARCH_API_KEY,
|
||||
GOOGLE_PSE_API_KEY,
|
||||
GOOGLE_PSE_ENGINE_ID,
|
||||
GOOGLE_DRIVE_CLIENT_ID,
|
||||
GOOGLE_DRIVE_API_KEY,
|
||||
ENABLE_ONEDRIVE_INTEGRATION,
|
||||
ONEDRIVE_CLIENT_ID_PERSONAL,
|
||||
ONEDRIVE_CLIENT_ID_BUSINESS,
|
||||
ONEDRIVE_SHAREPOINT_URL,
|
||||
ONEDRIVE_SHAREPOINT_TENANT_ID,
|
||||
ENABLE_ONEDRIVE_PERSONAL,
|
||||
ENABLE_ONEDRIVE_BUSINESS,
|
||||
ENABLE_RAG_HYBRID_SEARCH,
|
||||
ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS,
|
||||
ENABLE_RAG_LOCAL_WEB_FETCH,
|
||||
ENABLE_WEB_LOADER_SSL_VERIFICATION,
|
||||
ENABLE_GOOGLE_DRIVE_INTEGRATION,
|
||||
UPLOAD_DIR,
|
||||
EXTERNAL_WEB_SEARCH_URL,
|
||||
EXTERNAL_WEB_SEARCH_API_KEY,
|
||||
EXTERNAL_WEB_LOADER_URL,
|
||||
EXTERNAL_WEB_LOADER_API_KEY,
|
||||
YANDEX_WEB_SEARCH_URL,
|
||||
YANDEX_WEB_SEARCH_API_KEY,
|
||||
YANDEX_WEB_SEARCH_CONFIG,
|
||||
YOUCOM_API_KEY,
|
||||
# WebUI
|
||||
WEBUI_AUTH,
|
||||
WEBUI_NAME,
|
||||
WEBUI_BANNERS,
|
||||
WEBHOOK_URL,
|
||||
ADMIN_EMAIL,
|
||||
SHOW_ADMIN_DETAILS,
|
||||
JWT_EXPIRES_IN,
|
||||
ENABLE_SIGNUP,
|
||||
ENABLE_LOGIN_FORM,
|
||||
ENABLE_PASSWORD_CHANGE_FORM,
|
||||
ENABLE_API_KEYS,
|
||||
ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS,
|
||||
API_KEYS_ALLOWED_ENDPOINTS,
|
||||
ENABLE_FOLDERS,
|
||||
FOLDER_MAX_FILE_COUNT,
|
||||
ENABLE_AUTOMATIONS,
|
||||
AUTOMATION_MAX_COUNT,
|
||||
AUTOMATION_MIN_INTERVAL,
|
||||
ENABLE_CHANNELS,
|
||||
ENABLE_CALENDAR,
|
||||
ENABLE_NOTES,
|
||||
ENABLE_USER_STATUS,
|
||||
ENABLE_COMMUNITY_SHARING,
|
||||
ENABLE_MESSAGE_RATING,
|
||||
ENABLE_USER_WEBHOOKS,
|
||||
ENABLE_EVALUATION_ARENA_MODELS,
|
||||
BYPASS_ADMIN_ACCESS_CONTROL,
|
||||
USER_PERMISSIONS,
|
||||
DEFAULT_USER_ROLE,
|
||||
DEFAULT_GROUP_ID,
|
||||
PENDING_USER_OVERLAY_CONTENT,
|
||||
PENDING_USER_OVERLAY_TITLE,
|
||||
DEFAULT_PROMPT_SUGGESTIONS,
|
||||
DEFAULT_MODELS,
|
||||
DEFAULT_PINNED_MODELS,
|
||||
DEFAULT_ARENA_MODEL,
|
||||
MODEL_ORDER_LIST,
|
||||
DEFAULT_MODEL_METADATA,
|
||||
DEFAULT_MODEL_PARAMS,
|
||||
EVALUATION_ARENA_MODELS,
|
||||
# WebUI (OAuth)
|
||||
ENABLE_OAUTH_ROLE_MANAGEMENT,
|
||||
OAUTH_SUB_CLAIM,
|
||||
OAUTH_ROLES_CLAIM,
|
||||
OAUTH_EMAIL_CLAIM,
|
||||
OAUTH_PICTURE_CLAIM,
|
||||
OAUTH_USERNAME_CLAIM,
|
||||
OAUTH_ALLOWED_ROLES,
|
||||
OAUTH_ADMIN_ROLES,
|
||||
# WebUI (LDAP)
|
||||
ENABLE_LDAP,
|
||||
LDAP_SERVER_LABEL,
|
||||
LDAP_SERVER_HOST,
|
||||
LDAP_SERVER_PORT,
|
||||
LDAP_ATTRIBUTE_FOR_MAIL,
|
||||
LDAP_ATTRIBUTE_FOR_USERNAME,
|
||||
LDAP_SEARCH_FILTERS,
|
||||
LDAP_SEARCH_BASE,
|
||||
LDAP_APP_DN,
|
||||
LDAP_APP_PASSWORD,
|
||||
LDAP_USE_TLS,
|
||||
LDAP_CA_CERT_FILE,
|
||||
LDAP_VALIDATE_CERT,
|
||||
LDAP_CIPHERS,
|
||||
# LDAP Group Management
|
||||
ENABLE_LDAP_GROUP_MANAGEMENT,
|
||||
ENABLE_LDAP_GROUP_CREATION,
|
||||
LDAP_ATTRIBUTE_FOR_GROUPS,
|
||||
# Misc
|
||||
ENV,
|
||||
CACHE_DIR,
|
||||
STATIC_DIR,
|
||||
FRONTEND_BUILD_DIR,
|
||||
CORS_ALLOW_ORIGIN,
|
||||
DEFAULT_LOCALE,
|
||||
OAUTH_PROVIDERS,
|
||||
WEBUI_URL,
|
||||
RESPONSE_WATERMARK,
|
||||
# Admin
|
||||
ENABLE_ADMIN_CHAT_ACCESS,
|
||||
ENABLE_ADMIN_ANALYTICS,
|
||||
BYPASS_ADMIN_ACCESS_CONTROL,
|
||||
ENABLE_ADMIN_EXPORT,
|
||||
# Tasks
|
||||
TASK_MODEL,
|
||||
TASK_MODEL_EXTERNAL,
|
||||
ENABLE_TAGS_GENERATION,
|
||||
ENABLE_TITLE_GENERATION,
|
||||
ENABLE_FOLLOW_UP_GENERATION,
|
||||
ENABLE_SEARCH_QUERY_GENERATION,
|
||||
ENABLE_RETRIEVAL_QUERY_GENERATION,
|
||||
ENABLE_AUTOCOMPLETE_GENERATION,
|
||||
TITLE_GENERATION_PROMPT_TEMPLATE,
|
||||
FOLLOW_UP_GENERATION_PROMPT_TEMPLATE,
|
||||
TAGS_GENERATION_PROMPT_TEMPLATE,
|
||||
IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE,
|
||||
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
|
||||
VOICE_MODE_PROMPT_TEMPLATE,
|
||||
QUERY_GENERATION_PROMPT_TEMPLATE,
|
||||
AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE,
|
||||
AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH,
|
||||
AppConfig,
|
||||
reset_config,
|
||||
async_reset_config,
|
||||
)
|
||||
from open_webui.env import (
|
||||
ENABLE_CUSTOM_MODEL_FALLBACK,
|
||||
LICENSE_KEY,
|
||||
AUDIT_EXCLUDED_PATHS,
|
||||
AUDIT_INCLUDED_PATHS,
|
||||
ENABLE_AUDIT_GET_REQUESTS,
|
||||
AUDIT_LOG_LEVEL,
|
||||
CHANGELOG,
|
||||
REDIS_URL,
|
||||
REDIS_CLUSTER,
|
||||
REDIS_KEY_PREFIX,
|
||||
REDIS_SENTINEL_HOSTS,
|
||||
REDIS_SENTINEL_PORT,
|
||||
GLOBAL_LOG_LEVEL,
|
||||
MAX_BODY_LOG_SIZE,
|
||||
SAFE_MODE,
|
||||
VERSION,
|
||||
DEPLOYMENT_ID,
|
||||
INSTANCE_ID,
|
||||
WEBUI_BUILD_HASH,
|
||||
WEBUI_SECRET_KEY,
|
||||
WEBUI_SESSION_COOKIE_SAME_SITE,
|
||||
WEBUI_SESSION_COOKIE_SECURE,
|
||||
ENABLE_SIGNUP_PASSWORD_CONFIRMATION,
|
||||
WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
|
||||
WEBUI_AUTH_TRUSTED_NAME_HEADER,
|
||||
WEBUI_AUTH_SIGNOUT_REDIRECT_URL,
|
||||
# SCIM
|
||||
ENABLE_SCIM,
|
||||
SCIM_TOKEN,
|
||||
ENABLE_COMPRESSION_MIDDLEWARE,
|
||||
ENABLE_WEBSOCKET_SUPPORT,
|
||||
BYPASS_MODEL_ACCESS_CONTROL,
|
||||
RESET_CONFIG_ON_START,
|
||||
ENABLE_VERSION_UPDATE_CHECK,
|
||||
ENABLE_OTEL,
|
||||
EXTERNAL_PWA_MANIFEST_URL,
|
||||
AIOHTTP_CLIENT_SESSION_SSL,
|
||||
ENABLE_STAR_SESSIONS_MIDDLEWARE,
|
||||
ENABLE_PUBLIC_ACTIVE_USERS_COUNT,
|
||||
# Admin Account Runtime Creation
|
||||
WEBUI_ADMIN_EMAIL,
|
||||
WEBUI_ADMIN_PASSWORD,
|
||||
WEBUI_ADMIN_NAME,
|
||||
ENABLE_EASTER_EGGS,
|
||||
LOG_FORMAT,
|
||||
# OAuth Back-Channel Logout
|
||||
ENABLE_OAUTH_BACKCHANNEL_LOGOUT,
|
||||
)
|
||||
|
||||
|
||||
from open_webui.utils.models import (
|
||||
get_all_models,
|
||||
get_all_base_models,
|
||||
check_model_access,
|
||||
get_filtered_models,
|
||||
)
|
||||
from open_webui.utils.chat import (
|
||||
generate_chat_completion as chat_completion_handler,
|
||||
chat_completed as chat_completed_handler,
|
||||
)
|
||||
from open_webui.utils.actions import chat_action as chat_action_handler
|
||||
from open_webui.utils.embeddings import generate_embeddings
|
||||
from open_webui.utils.logger import start_logger
|
||||
from open_webui.utils.middleware import (
|
||||
background_tasks_handler,
|
||||
build_chat_response_context,
|
||||
process_chat_payload,
|
||||
process_chat_response,
|
||||
)
|
||||
from open_webui.utils.models import (
|
||||
check_model_access,
|
||||
get_all_base_models,
|
||||
get_all_models,
|
||||
get_filtered_models,
|
||||
)
|
||||
from open_webui.utils.oauth import (
|
||||
OAuthClientInformationFull,
|
||||
OAuthClientManager,
|
||||
OAuthManager,
|
||||
apply_connection_oauth_options,
|
||||
decrypt_data,
|
||||
encrypt_data,
|
||||
get_oauth_client_info_with_dynamic_client_registration,
|
||||
get_oauth_client_info_with_static_credentials,
|
||||
recover_static_oauth_client_metadata,
|
||||
resolve_oauth_client_info,
|
||||
from open_webui.utils.tools import set_tool_servers, set_terminal_servers
|
||||
|
||||
from open_webui.utils.auth import (
|
||||
get_license_data,
|
||||
get_http_authorization_cred,
|
||||
decode_token,
|
||||
get_admin_user,
|
||||
get_verified_user,
|
||||
create_admin_user,
|
||||
)
|
||||
from open_webui.utils.plugin import install_tool_and_function_dependencies
|
||||
from open_webui.utils.redis import get_redis_client
|
||||
from open_webui.utils.oauth import (
|
||||
get_oauth_client_info_with_dynamic_client_registration,
|
||||
get_oauth_client_info_with_static_credentials,
|
||||
encrypt_data,
|
||||
decrypt_data,
|
||||
resolve_oauth_client_info,
|
||||
OAuthManager,
|
||||
OAuthClientManager,
|
||||
OAuthClientInformationFull,
|
||||
)
|
||||
from open_webui.utils.security_headers import SecurityHeadersMiddleware
|
||||
from open_webui.utils.session_pool import get_session
|
||||
from open_webui.utils.tools import set_terminal_servers, set_tool_servers
|
||||
from open_webui.utils.redis import get_redis_connection
|
||||
|
||||
from open_webui.tasks import (
|
||||
redis_task_command_listener,
|
||||
list_task_ids_by_item_id,
|
||||
create_task,
|
||||
stop_task,
|
||||
stop_item_tasks,
|
||||
list_tasks,
|
||||
) # Import from tasks.py
|
||||
|
||||
from open_webui.utils.redis import get_sentinels_from_env
|
||||
|
||||
|
||||
from open_webui.constants import ERROR_MESSAGES, TASKS
|
||||
|
||||
if SAFE_MODE:
|
||||
print('SAFE MODE ENABLED')
|
||||
@@ -273,15 +614,8 @@ class SPAStaticFiles(StaticFiles):
|
||||
raise ex
|
||||
|
||||
|
||||
class CORSStaticFiles(StaticFiles):
|
||||
async def get_response(self, path: str, scope):
|
||||
response = await super().get_response(path, scope)
|
||||
response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
return response
|
||||
|
||||
|
||||
if LOG_FORMAT != 'json':
|
||||
banner = rf"""
|
||||
print(rf"""
|
||||
██████╗ ██████╗ ███████╗███╗ ██╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗██╗
|
||||
██╔═══██╗██╔══██╗██╔════╝████╗ ██║ ██║ ██║██╔════╝██╔══██╗██║ ██║██║
|
||||
██║ ██║██████╔╝█████╗ ██╔██╗ ██║ ██║ █╗ ██║█████╗ ██████╔╝██║ ██║██║
|
||||
@@ -293,12 +627,7 @@ if LOG_FORMAT != 'json':
|
||||
v{VERSION} - building the best AI user interface.
|
||||
{f'Commit: {WEBUI_BUILD_HASH}' if WEBUI_BUILD_HASH != 'dev-build' else ''}
|
||||
https://github.com/open-webui/open-webui
|
||||
"""
|
||||
try:
|
||||
print(banner)
|
||||
except UnicodeEncodeError:
|
||||
# Stdout can't encode the box-drawing banner (Windows cp1252, redirected/headless stdout); fall back to ASCII.
|
||||
print(f'Open WebUI v{VERSION} - building the best AI user interface.\nhttps://github.com/open-webui/open-webui')
|
||||
""")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -313,12 +642,6 @@ async def lifespan(app: FastAPI):
|
||||
if RESET_CONFIG_ON_START:
|
||||
await async_reset_config()
|
||||
|
||||
await import_legacy_config_json()
|
||||
await seed_registered_defaults()
|
||||
await initialize_runtime_config(app)
|
||||
await migrate_legacy_webhook_config()
|
||||
await publish_event(app, EVENTS.SYSTEM_STARTUP_STARTED, source='system')
|
||||
|
||||
if LICENSE_KEY:
|
||||
get_license_data(app, LICENSE_KEY)
|
||||
|
||||
@@ -326,7 +649,7 @@ async def lifespan(app: FastAPI):
|
||||
if WEBUI_ADMIN_EMAIL and WEBUI_ADMIN_PASSWORD:
|
||||
if await create_admin_user(WEBUI_ADMIN_EMAIL, WEBUI_ADMIN_PASSWORD, WEBUI_ADMIN_NAME):
|
||||
# Disable signup since we now have an admin
|
||||
await Config.upsert({'ui.enable_signup': False})
|
||||
app.state.config.ENABLE_SIGNUP = False
|
||||
|
||||
if SAFE_MODE:
|
||||
await Functions.deactivate_all_functions()
|
||||
@@ -336,7 +659,12 @@ async def lifespan(app: FastAPI):
|
||||
log.info('Installing external dependencies of functions and tools...')
|
||||
await install_tool_and_function_dependencies()
|
||||
|
||||
app.state.redis = get_redis_client(async_mode=True)
|
||||
app.state.redis = get_redis_connection(
|
||||
redis_url=REDIS_URL,
|
||||
redis_sentinels=get_sentinels_from_env(REDIS_SENTINEL_HOSTS, REDIS_SENTINEL_PORT),
|
||||
redis_cluster=REDIS_CLUSTER,
|
||||
async_mode=True,
|
||||
)
|
||||
|
||||
if app.state.redis is not None:
|
||||
app.state.redis_task_command_listener = asyncio.create_task(redis_task_command_listener(app))
|
||||
@@ -352,7 +680,7 @@ async def lifespan(app: FastAPI):
|
||||
|
||||
asyncio.create_task(scheduler_worker_loop(app))
|
||||
|
||||
if await Config.get('models.base_models_cache'):
|
||||
if app.state.config.ENABLE_BASE_MODELS_CACHE:
|
||||
try:
|
||||
await get_all_models(
|
||||
Request(
|
||||
@@ -377,44 +705,37 @@ async def lifespan(app: FastAPI):
|
||||
log.warning(f'Failed to pre-fetch models at startup: {e}')
|
||||
|
||||
# Pre-fetch tool server specs so the first request doesn't pay the latency cost
|
||||
if len(await Config.get('tool_server.connections', []) or []) > 0:
|
||||
mock_request = Request(
|
||||
{
|
||||
'type': 'http',
|
||||
'asgi.version': '3.0',
|
||||
'asgi.spec_version': '2.0',
|
||||
'method': 'GET',
|
||||
'path': '/internal',
|
||||
'query_string': b'',
|
||||
'headers': Headers({}).raw,
|
||||
'client': ('127.0.0.1', 12345),
|
||||
'server': ('127.0.0.1', 80),
|
||||
'scheme': 'http',
|
||||
'app': app,
|
||||
}
|
||||
)
|
||||
|
||||
if len(app.state.config.TOOL_SERVER_CONNECTIONS) > 0:
|
||||
log.info('Initializing tool servers...')
|
||||
try:
|
||||
mock_request = Request(
|
||||
{
|
||||
'type': 'http',
|
||||
'asgi.version': '3.0',
|
||||
'asgi.spec_version': '2.0',
|
||||
'method': 'GET',
|
||||
'path': '/internal',
|
||||
'query_string': b'',
|
||||
'headers': Headers({}).raw,
|
||||
'client': ('127.0.0.1', 12345),
|
||||
'server': ('127.0.0.1', 80),
|
||||
'scheme': 'http',
|
||||
'app': app,
|
||||
}
|
||||
)
|
||||
await set_tool_servers(mock_request)
|
||||
log.info(f'Initialized {len(app.state.TOOL_SERVERS)} tool server(s)')
|
||||
except Exception as e:
|
||||
log.warning(f'Failed to initialize tool servers at startup: {e}')
|
||||
|
||||
try:
|
||||
await set_terminal_servers(mock_request)
|
||||
log.info(f'Initialized {len(app.state.TERMINAL_SERVERS)} terminal server(s)')
|
||||
except Exception as e:
|
||||
log.warning(f'Failed to initialize terminal servers at startup: {e}')
|
||||
log.warning(f'Failed to initialize tool/terminal servers at startup: {e}')
|
||||
|
||||
# Mark application as ready to accept traffic from a startup perspective.
|
||||
app.state.startup_complete = True
|
||||
await publish_event(app, EVENTS.SYSTEM_STARTUP_COMPLETED, source='system')
|
||||
|
||||
yield
|
||||
|
||||
await publish_event(app, EVENTS.SYSTEM_SHUTDOWN_STARTED, source='system')
|
||||
|
||||
# Shutdown: clean up shared resources
|
||||
from open_webui.utils.session_pool import close_session
|
||||
|
||||
@@ -423,8 +744,6 @@ async def lifespan(app: FastAPI):
|
||||
if hasattr(app.state, 'redis_task_command_listener'):
|
||||
app.state.redis_task_command_listener.cancel()
|
||||
|
||||
await publish_event(app, EVENTS.SYSTEM_SHUTDOWN_COMPLETED, source='system')
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title='Open WebUI',
|
||||
@@ -446,12 +765,16 @@ oauth_client_manager = OAuthClientManager(app)
|
||||
app.state.oauth_client_manager = oauth_client_manager
|
||||
|
||||
app.state.instance_id = None
|
||||
app.state.config = AppConfig(
|
||||
redis_url=REDIS_URL,
|
||||
redis_sentinels=get_sentinels_from_env(REDIS_SENTINEL_HOSTS, REDIS_SENTINEL_PORT),
|
||||
redis_cluster=REDIS_CLUSTER,
|
||||
redis_key_prefix=REDIS_KEY_PREFIX,
|
||||
)
|
||||
app.state.redis = None
|
||||
|
||||
app.state.WEBUI_NAME = WEBUI_NAME
|
||||
app.state.LICENSE_METADATA = None
|
||||
app.state.USER_COUNT = None
|
||||
app.state.EXTERNAL_PWA_MANIFEST_URL = EXTERNAL_PWA_MANIFEST_URL
|
||||
|
||||
|
||||
########################################
|
||||
@@ -473,6 +796,10 @@ if ENABLE_OTEL:
|
||||
########################################
|
||||
|
||||
|
||||
app.state.config.ENABLE_OLLAMA_API = ENABLE_OLLAMA_API
|
||||
app.state.config.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS
|
||||
app.state.config.OLLAMA_API_CONFIGS = OLLAMA_API_CONFIGS
|
||||
|
||||
app.state.OLLAMA_MODELS = {}
|
||||
|
||||
########################################
|
||||
@@ -481,6 +808,10 @@ app.state.OLLAMA_MODELS = {}
|
||||
#
|
||||
########################################
|
||||
|
||||
app.state.config.ENABLE_OPENAI_API = ENABLE_OPENAI_API
|
||||
app.state.config.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS
|
||||
app.state.config.OPENAI_API_KEYS = OPENAI_API_KEYS
|
||||
app.state.config.OPENAI_API_CONFIGS = OPENAI_API_CONFIGS
|
||||
|
||||
app.state.OPENAI_MODELS = {}
|
||||
|
||||
@@ -490,6 +821,7 @@ app.state.OPENAI_MODELS = {}
|
||||
#
|
||||
########################################
|
||||
|
||||
app.state.config.TOOL_SERVER_CONNECTIONS = TOOL_SERVER_CONNECTIONS
|
||||
app.state.TOOL_SERVERS = []
|
||||
|
||||
########################################
|
||||
@@ -498,6 +830,7 @@ app.state.TOOL_SERVERS = []
|
||||
#
|
||||
########################################
|
||||
|
||||
app.state.config.TERMINAL_SERVER_CONNECTIONS = TERMINAL_SERVER_CONNECTIONS
|
||||
app.state.TERMINAL_SERVERS = []
|
||||
|
||||
########################################
|
||||
@@ -506,6 +839,7 @@ app.state.TERMINAL_SERVERS = []
|
||||
#
|
||||
########################################
|
||||
|
||||
app.state.config.ENABLE_DIRECT_CONNECTIONS = ENABLE_DIRECT_CONNECTIONS
|
||||
|
||||
########################################
|
||||
#
|
||||
@@ -522,6 +856,7 @@ app.state.SCIM_TOKEN = SCIM_TOKEN
|
||||
#
|
||||
########################################
|
||||
|
||||
app.state.config.ENABLE_BASE_MODELS_CACHE = ENABLE_BASE_MODELS_CACHE
|
||||
app.state.BASE_MODELS = []
|
||||
|
||||
########################################
|
||||
@@ -530,130 +865,347 @@ app.state.BASE_MODELS = []
|
||||
#
|
||||
########################################
|
||||
|
||||
app.state.config.WEBUI_URL = WEBUI_URL
|
||||
app.state.config.ENABLE_SIGNUP = ENABLE_SIGNUP
|
||||
app.state.config.ENABLE_LOGIN_FORM = ENABLE_LOGIN_FORM
|
||||
app.state.config.ENABLE_PASSWORD_CHANGE_FORM = ENABLE_PASSWORD_CHANGE_FORM
|
||||
|
||||
async def initialize_runtime_config(app: FastAPI):
|
||||
# Migrate legacy access_control → access_grants on boot.
|
||||
from open_webui.utils.access_control import migrate_access_control
|
||||
app.state.config.ENABLE_API_KEYS = ENABLE_API_KEYS
|
||||
app.state.config.ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS = ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS
|
||||
app.state.config.API_KEYS_ALLOWED_ENDPOINTS = API_KEYS_ALLOWED_ENDPOINTS
|
||||
|
||||
connections = await Config.get('tool_server.connections', []) or []
|
||||
if any('access_control' in c.get('config', {}) for c in connections):
|
||||
for connection in connections:
|
||||
migrate_access_control(connection.get('config', {}))
|
||||
await Config.upsert({'tool_server.connections': connections})
|
||||
app.state.config.JWT_EXPIRES_IN = JWT_EXPIRES_IN
|
||||
|
||||
for tool_server_connection in connections:
|
||||
if tool_server_connection.get('type', 'openapi') == 'mcp':
|
||||
server_id = (tool_server_connection.get('info') or {}).get('id')
|
||||
auth_type = tool_server_connection.get('auth_type', 'none')
|
||||
app.state.config.SHOW_ADMIN_DETAILS = SHOW_ADMIN_DETAILS
|
||||
app.state.config.ADMIN_EMAIL = ADMIN_EMAIL
|
||||
|
||||
if server_id and auth_type in ('oauth_2.1', 'oauth_2.1_static'):
|
||||
try:
|
||||
oauth_client_info = resolve_oauth_client_info(tool_server_connection)
|
||||
oauth_client_info = await recover_static_oauth_client_metadata(
|
||||
tool_server_connection, oauth_client_info
|
||||
)
|
||||
oauth_client_info = apply_connection_oauth_options(tool_server_connection, oauth_client_info)
|
||||
app.state.oauth_client_manager.add_client(
|
||||
f'mcp:{server_id}',
|
||||
OAuthClientInformationFull(**oauth_client_info),
|
||||
)
|
||||
except Exception as e:
|
||||
log.error(f'Error adding OAuth client for MCP tool server {server_id}: {e}')
|
||||
|
||||
arena_models = await Config.get('evaluation.arena.models', []) or []
|
||||
if any('access_control' in m.get('meta', {}) for m in arena_models):
|
||||
for model in arena_models:
|
||||
migrate_access_control(model.get('meta', {}))
|
||||
await Config.upsert({'evaluation.arena.models': arena_models})
|
||||
app.state.config.DEFAULT_MODELS = DEFAULT_MODELS
|
||||
app.state.config.DEFAULT_PINNED_MODELS = DEFAULT_PINNED_MODELS
|
||||
app.state.config.MODEL_ORDER_LIST = MODEL_ORDER_LIST
|
||||
app.state.config.DEFAULT_MODEL_METADATA = DEFAULT_MODEL_METADATA
|
||||
app.state.config.DEFAULT_MODEL_PARAMS = DEFAULT_MODEL_PARAMS
|
||||
|
||||
app.state.EMBEDDING_FUNCTION = None
|
||||
app.state.RERANKING_FUNCTION = None
|
||||
app.state.ef = None
|
||||
app.state.rf = None
|
||||
app.state.YOUTUBE_LOADER_TRANSLATION = None
|
||||
|
||||
try:
|
||||
rag_config = await Config.get_many(
|
||||
'rag.embedding_engine',
|
||||
'rag.embedding_model',
|
||||
'rag.enable_hybrid_search',
|
||||
'rag.bypass_embedding_and_retrieval',
|
||||
'rag.reranking_engine',
|
||||
'rag.reranking_model',
|
||||
'rag.external_reranker_url',
|
||||
'rag.external_reranker_api_key',
|
||||
'rag.external_reranker_timeout',
|
||||
app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
|
||||
app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
|
||||
app.state.config.DEFAULT_GROUP_ID = DEFAULT_GROUP_ID
|
||||
|
||||
app.state.config.PENDING_USER_OVERLAY_CONTENT = PENDING_USER_OVERLAY_CONTENT
|
||||
app.state.config.PENDING_USER_OVERLAY_TITLE = PENDING_USER_OVERLAY_TITLE
|
||||
|
||||
app.state.config.RESPONSE_WATERMARK = RESPONSE_WATERMARK
|
||||
|
||||
app.state.config.USER_PERMISSIONS = USER_PERMISSIONS
|
||||
app.state.config.WEBHOOK_URL = WEBHOOK_URL
|
||||
app.state.config.BANNERS = WEBUI_BANNERS
|
||||
|
||||
|
||||
app.state.config.ENABLE_FOLDERS = ENABLE_FOLDERS
|
||||
app.state.config.FOLDER_MAX_FILE_COUNT = FOLDER_MAX_FILE_COUNT
|
||||
app.state.config.ENABLE_AUTOMATIONS = ENABLE_AUTOMATIONS
|
||||
app.state.config.AUTOMATION_MAX_COUNT = AUTOMATION_MAX_COUNT
|
||||
app.state.config.AUTOMATION_MIN_INTERVAL = AUTOMATION_MIN_INTERVAL
|
||||
app.state.config.ENABLE_CHANNELS = ENABLE_CHANNELS
|
||||
app.state.config.ENABLE_CALENDAR = ENABLE_CALENDAR
|
||||
app.state.config.ENABLE_NOTES = ENABLE_NOTES
|
||||
app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING
|
||||
app.state.config.ENABLE_MESSAGE_RATING = ENABLE_MESSAGE_RATING
|
||||
app.state.config.ENABLE_USER_WEBHOOKS = ENABLE_USER_WEBHOOKS
|
||||
app.state.config.ENABLE_USER_STATUS = ENABLE_USER_STATUS
|
||||
|
||||
app.state.config.ENABLE_EVALUATION_ARENA_MODELS = ENABLE_EVALUATION_ARENA_MODELS
|
||||
app.state.config.EVALUATION_ARENA_MODELS = EVALUATION_ARENA_MODELS
|
||||
|
||||
# Migrate legacy access_control → access_grants on boot
|
||||
from open_webui.utils.access_control import migrate_access_control
|
||||
|
||||
connections = app.state.config.TOOL_SERVER_CONNECTIONS
|
||||
if any('access_control' in c.get('config', {}) for c in connections):
|
||||
for connection in connections:
|
||||
migrate_access_control(connection.get('config', {}))
|
||||
app.state.config.TOOL_SERVER_CONNECTIONS = connections
|
||||
|
||||
arena_models = app.state.config.EVALUATION_ARENA_MODELS
|
||||
if any('access_control' in m.get('meta', {}) for m in arena_models):
|
||||
for model in arena_models:
|
||||
migrate_access_control(model.get('meta', {}))
|
||||
app.state.config.EVALUATION_ARENA_MODELS = arena_models
|
||||
|
||||
app.state.config.OAUTH_SUB_CLAIM = OAUTH_SUB_CLAIM
|
||||
app.state.config.OAUTH_USERNAME_CLAIM = OAUTH_USERNAME_CLAIM
|
||||
app.state.config.OAUTH_PICTURE_CLAIM = OAUTH_PICTURE_CLAIM
|
||||
app.state.config.OAUTH_EMAIL_CLAIM = OAUTH_EMAIL_CLAIM
|
||||
|
||||
app.state.config.ENABLE_OAUTH_ROLE_MANAGEMENT = ENABLE_OAUTH_ROLE_MANAGEMENT
|
||||
app.state.config.OAUTH_ROLES_CLAIM = OAUTH_ROLES_CLAIM
|
||||
app.state.config.OAUTH_ALLOWED_ROLES = OAUTH_ALLOWED_ROLES
|
||||
app.state.config.OAUTH_ADMIN_ROLES = OAUTH_ADMIN_ROLES
|
||||
|
||||
app.state.config.ENABLE_LDAP = ENABLE_LDAP
|
||||
app.state.config.LDAP_SERVER_LABEL = LDAP_SERVER_LABEL
|
||||
app.state.config.LDAP_SERVER_HOST = LDAP_SERVER_HOST
|
||||
app.state.config.LDAP_SERVER_PORT = LDAP_SERVER_PORT
|
||||
app.state.config.LDAP_ATTRIBUTE_FOR_MAIL = LDAP_ATTRIBUTE_FOR_MAIL
|
||||
app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME = LDAP_ATTRIBUTE_FOR_USERNAME
|
||||
app.state.config.LDAP_APP_DN = LDAP_APP_DN
|
||||
app.state.config.LDAP_APP_PASSWORD = LDAP_APP_PASSWORD
|
||||
app.state.config.LDAP_SEARCH_BASE = LDAP_SEARCH_BASE
|
||||
app.state.config.LDAP_SEARCH_FILTERS = LDAP_SEARCH_FILTERS
|
||||
app.state.config.LDAP_USE_TLS = LDAP_USE_TLS
|
||||
app.state.config.LDAP_CA_CERT_FILE = LDAP_CA_CERT_FILE
|
||||
app.state.config.LDAP_VALIDATE_CERT = LDAP_VALIDATE_CERT
|
||||
app.state.config.LDAP_CIPHERS = LDAP_CIPHERS
|
||||
|
||||
# For LDAP Group Management
|
||||
app.state.config.ENABLE_LDAP_GROUP_MANAGEMENT = ENABLE_LDAP_GROUP_MANAGEMENT
|
||||
app.state.config.ENABLE_LDAP_GROUP_CREATION = ENABLE_LDAP_GROUP_CREATION
|
||||
app.state.config.LDAP_ATTRIBUTE_FOR_GROUPS = LDAP_ATTRIBUTE_FOR_GROUPS
|
||||
|
||||
|
||||
app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
|
||||
app.state.AUTH_TRUSTED_NAME_HEADER = WEBUI_AUTH_TRUSTED_NAME_HEADER
|
||||
app.state.WEBUI_AUTH_SIGNOUT_REDIRECT_URL = WEBUI_AUTH_SIGNOUT_REDIRECT_URL
|
||||
app.state.EXTERNAL_PWA_MANIFEST_URL = EXTERNAL_PWA_MANIFEST_URL
|
||||
|
||||
app.state.USER_COUNT = None
|
||||
|
||||
app.state.TOOLS = {}
|
||||
app.state.TOOL_CONTENTS = {}
|
||||
|
||||
app.state.FUNCTIONS = {}
|
||||
app.state.FUNCTION_CONTENTS = {}
|
||||
|
||||
########################################
|
||||
#
|
||||
# RETRIEVAL
|
||||
#
|
||||
########################################
|
||||
|
||||
|
||||
app.state.config.TOP_K = RAG_TOP_K
|
||||
app.state.config.TOP_K_RERANKER = RAG_TOP_K_RERANKER
|
||||
app.state.config.RELEVANCE_THRESHOLD = RAG_RELEVANCE_THRESHOLD
|
||||
app.state.config.HYBRID_BM25_WEIGHT = RAG_HYBRID_BM25_WEIGHT
|
||||
|
||||
|
||||
app.state.config.ALLOWED_FILE_EXTENSIONS = RAG_ALLOWED_FILE_EXTENSIONS
|
||||
app.state.config.FILE_MAX_SIZE = RAG_FILE_MAX_SIZE
|
||||
app.state.config.FILE_MAX_COUNT = RAG_FILE_MAX_COUNT
|
||||
app.state.config.FILE_IMAGE_COMPRESSION_WIDTH = FILE_IMAGE_COMPRESSION_WIDTH
|
||||
app.state.config.FILE_IMAGE_COMPRESSION_HEIGHT = FILE_IMAGE_COMPRESSION_HEIGHT
|
||||
|
||||
|
||||
app.state.config.RAG_FULL_CONTEXT = RAG_FULL_CONTEXT
|
||||
app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL = BYPASS_EMBEDDING_AND_RETRIEVAL
|
||||
app.state.config.ENABLE_RAG_HYBRID_SEARCH = ENABLE_RAG_HYBRID_SEARCH
|
||||
app.state.config.ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS = ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS
|
||||
app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION = ENABLE_WEB_LOADER_SSL_VERIFICATION
|
||||
|
||||
app.state.config.CONTENT_EXTRACTION_ENGINE = CONTENT_EXTRACTION_ENGINE
|
||||
app.state.config.DATALAB_MARKER_API_KEY = DATALAB_MARKER_API_KEY
|
||||
app.state.config.DATALAB_MARKER_API_BASE_URL = DATALAB_MARKER_API_BASE_URL
|
||||
app.state.config.DATALAB_MARKER_ADDITIONAL_CONFIG = DATALAB_MARKER_ADDITIONAL_CONFIG
|
||||
app.state.config.DATALAB_MARKER_SKIP_CACHE = DATALAB_MARKER_SKIP_CACHE
|
||||
app.state.config.DATALAB_MARKER_FORCE_OCR = DATALAB_MARKER_FORCE_OCR
|
||||
app.state.config.DATALAB_MARKER_PAGINATE = DATALAB_MARKER_PAGINATE
|
||||
app.state.config.DATALAB_MARKER_STRIP_EXISTING_OCR = DATALAB_MARKER_STRIP_EXISTING_OCR
|
||||
app.state.config.DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION = DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION
|
||||
app.state.config.DATALAB_MARKER_FORMAT_LINES = DATALAB_MARKER_FORMAT_LINES
|
||||
app.state.config.DATALAB_MARKER_USE_LLM = DATALAB_MARKER_USE_LLM
|
||||
app.state.config.DATALAB_MARKER_OUTPUT_FORMAT = DATALAB_MARKER_OUTPUT_FORMAT
|
||||
app.state.config.EXTERNAL_DOCUMENT_LOADER_URL = EXTERNAL_DOCUMENT_LOADER_URL
|
||||
app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY = EXTERNAL_DOCUMENT_LOADER_API_KEY
|
||||
app.state.config.TIKA_SERVER_URL = TIKA_SERVER_URL
|
||||
app.state.config.DOCLING_SERVER_URL = DOCLING_SERVER_URL
|
||||
app.state.config.DOCLING_API_KEY = DOCLING_API_KEY
|
||||
app.state.config.DOCLING_PARAMS = DOCLING_PARAMS
|
||||
app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT = DOCUMENT_INTELLIGENCE_ENDPOINT
|
||||
app.state.config.DOCUMENT_INTELLIGENCE_KEY = DOCUMENT_INTELLIGENCE_KEY
|
||||
app.state.config.DOCUMENT_INTELLIGENCE_MODEL = DOCUMENT_INTELLIGENCE_MODEL
|
||||
app.state.config.MISTRAL_OCR_API_BASE_URL = MISTRAL_OCR_API_BASE_URL
|
||||
app.state.config.MISTRAL_OCR_API_KEY = MISTRAL_OCR_API_KEY
|
||||
app.state.config.PADDLEOCR_VL_BASE_URL = PADDLEOCR_VL_BASE_URL
|
||||
app.state.config.PADDLEOCR_VL_TOKEN = PADDLEOCR_VL_TOKEN
|
||||
app.state.config.MINERU_API_MODE = MINERU_API_MODE
|
||||
app.state.config.MINERU_API_URL = MINERU_API_URL
|
||||
app.state.config.MINERU_API_KEY = MINERU_API_KEY
|
||||
app.state.config.MINERU_API_TIMEOUT = MINERU_API_TIMEOUT
|
||||
app.state.config.MINERU_PARAMS = MINERU_PARAMS
|
||||
|
||||
app.state.config.TEXT_SPLITTER = RAG_TEXT_SPLITTER
|
||||
app.state.config.ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER = ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER
|
||||
|
||||
app.state.config.TIKTOKEN_ENCODING_NAME = TIKTOKEN_ENCODING_NAME
|
||||
|
||||
app.state.config.CHUNK_SIZE = CHUNK_SIZE
|
||||
app.state.config.CHUNK_MIN_SIZE_TARGET = CHUNK_MIN_SIZE_TARGET
|
||||
app.state.config.CHUNK_OVERLAP = CHUNK_OVERLAP
|
||||
|
||||
|
||||
app.state.config.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE
|
||||
app.state.config.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
|
||||
app.state.config.RAG_EMBEDDING_BATCH_SIZE = RAG_EMBEDDING_BATCH_SIZE
|
||||
app.state.config.ENABLE_ASYNC_EMBEDDING = ENABLE_ASYNC_EMBEDDING
|
||||
app.state.config.RAG_EMBEDDING_CONCURRENT_REQUESTS = RAG_EMBEDDING_CONCURRENT_REQUESTS
|
||||
|
||||
app.state.config.RAG_RERANKING_ENGINE = RAG_RERANKING_ENGINE
|
||||
app.state.config.RAG_RERANKING_MODEL = RAG_RERANKING_MODEL
|
||||
app.state.config.RAG_EXTERNAL_RERANKER_URL = RAG_EXTERNAL_RERANKER_URL
|
||||
app.state.config.RAG_EXTERNAL_RERANKER_API_KEY = RAG_EXTERNAL_RERANKER_API_KEY
|
||||
app.state.config.RAG_EXTERNAL_RERANKER_TIMEOUT = RAG_EXTERNAL_RERANKER_TIMEOUT
|
||||
app.state.config.RAG_RERANKING_BATCH_SIZE = RAG_RERANKING_BATCH_SIZE
|
||||
|
||||
app.state.config.RAG_TEMPLATE = RAG_TEMPLATE
|
||||
|
||||
app.state.config.RAG_OPENAI_API_BASE_URL = RAG_OPENAI_API_BASE_URL
|
||||
app.state.config.RAG_OPENAI_API_KEY = RAG_OPENAI_API_KEY
|
||||
|
||||
app.state.config.RAG_AZURE_OPENAI_BASE_URL = RAG_AZURE_OPENAI_BASE_URL
|
||||
app.state.config.RAG_AZURE_OPENAI_API_KEY = RAG_AZURE_OPENAI_API_KEY
|
||||
app.state.config.RAG_AZURE_OPENAI_API_VERSION = RAG_AZURE_OPENAI_API_VERSION
|
||||
|
||||
app.state.config.RAG_OLLAMA_BASE_URL = RAG_OLLAMA_BASE_URL
|
||||
app.state.config.RAG_OLLAMA_API_KEY = RAG_OLLAMA_API_KEY
|
||||
|
||||
app.state.config.PDF_EXTRACT_IMAGES = PDF_EXTRACT_IMAGES
|
||||
app.state.config.PDF_LOADER_MODE = PDF_LOADER_MODE
|
||||
|
||||
app.state.config.YOUTUBE_LOADER_LANGUAGE = YOUTUBE_LOADER_LANGUAGE
|
||||
app.state.config.YOUTUBE_LOADER_PROXY_URL = YOUTUBE_LOADER_PROXY_URL
|
||||
|
||||
|
||||
app.state.config.ENABLE_WEB_SEARCH = ENABLE_WEB_SEARCH
|
||||
app.state.config.WEB_SEARCH_ENGINE = WEB_SEARCH_ENGINE
|
||||
app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST = WEB_SEARCH_DOMAIN_FILTER_LIST
|
||||
app.state.config.WEB_SEARCH_RESULT_COUNT = WEB_SEARCH_RESULT_COUNT
|
||||
app.state.config.WEB_SEARCH_CONCURRENT_REQUESTS = WEB_SEARCH_CONCURRENT_REQUESTS
|
||||
app.state.config.WEB_FETCH_MAX_CONTENT_LENGTH = WEB_FETCH_MAX_CONTENT_LENGTH
|
||||
|
||||
app.state.config.WEB_LOADER_ENGINE = WEB_LOADER_ENGINE
|
||||
app.state.config.WEB_LOADER_CONCURRENT_REQUESTS = WEB_LOADER_CONCURRENT_REQUESTS
|
||||
app.state.config.WEB_LOADER_TIMEOUT = WEB_LOADER_TIMEOUT
|
||||
|
||||
app.state.config.WEB_SEARCH_TRUST_ENV = WEB_SEARCH_TRUST_ENV
|
||||
app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL
|
||||
app.state.config.BYPASS_WEB_SEARCH_WEB_LOADER = BYPASS_WEB_SEARCH_WEB_LOADER
|
||||
|
||||
app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = ENABLE_GOOGLE_DRIVE_INTEGRATION
|
||||
app.state.config.ENABLE_ONEDRIVE_INTEGRATION = ENABLE_ONEDRIVE_INTEGRATION
|
||||
|
||||
app.state.config.OLLAMA_CLOUD_WEB_SEARCH_API_KEY = OLLAMA_CLOUD_WEB_SEARCH_API_KEY
|
||||
app.state.config.SEARXNG_QUERY_URL = SEARXNG_QUERY_URL
|
||||
app.state.config.SEARXNG_LANGUAGE = SEARXNG_LANGUAGE
|
||||
app.state.config.YACY_QUERY_URL = YACY_QUERY_URL
|
||||
app.state.config.YACY_USERNAME = YACY_USERNAME
|
||||
app.state.config.YACY_PASSWORD = YACY_PASSWORD
|
||||
app.state.config.GOOGLE_PSE_API_KEY = GOOGLE_PSE_API_KEY
|
||||
app.state.config.GOOGLE_PSE_ENGINE_ID = GOOGLE_PSE_ENGINE_ID
|
||||
app.state.config.BRAVE_SEARCH_API_KEY = BRAVE_SEARCH_API_KEY
|
||||
app.state.config.KAGI_SEARCH_API_KEY = KAGI_SEARCH_API_KEY
|
||||
app.state.config.MOJEEK_SEARCH_API_KEY = MOJEEK_SEARCH_API_KEY
|
||||
app.state.config.BOCHA_SEARCH_API_KEY = BOCHA_SEARCH_API_KEY
|
||||
app.state.config.SERPSTACK_API_KEY = SERPSTACK_API_KEY
|
||||
app.state.config.SERPSTACK_HTTPS = SERPSTACK_HTTPS
|
||||
app.state.config.SERPER_API_KEY = SERPER_API_KEY
|
||||
app.state.config.SERPLY_API_KEY = SERPLY_API_KEY
|
||||
app.state.config.DDGS_BACKEND = DDGS_BACKEND
|
||||
app.state.config.TAVILY_API_KEY = TAVILY_API_KEY
|
||||
app.state.config.SEARCHAPI_API_KEY = SEARCHAPI_API_KEY
|
||||
app.state.config.SEARCHAPI_ENGINE = SEARCHAPI_ENGINE
|
||||
app.state.config.SERPAPI_API_KEY = SERPAPI_API_KEY
|
||||
app.state.config.SERPAPI_ENGINE = SERPAPI_ENGINE
|
||||
app.state.config.JINA_API_KEY = JINA_API_KEY
|
||||
app.state.config.JINA_API_BASE_URL = JINA_API_BASE_URL
|
||||
app.state.config.BING_SEARCH_V7_ENDPOINT = BING_SEARCH_V7_ENDPOINT
|
||||
app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY = BING_SEARCH_V7_SUBSCRIPTION_KEY
|
||||
app.state.config.EXA_API_KEY = EXA_API_KEY
|
||||
app.state.config.PERPLEXITY_API_KEY = PERPLEXITY_API_KEY
|
||||
app.state.config.PERPLEXITY_MODEL = PERPLEXITY_MODEL
|
||||
app.state.config.PERPLEXITY_SEARCH_CONTEXT_USAGE = PERPLEXITY_SEARCH_CONTEXT_USAGE
|
||||
app.state.config.PERPLEXITY_SEARCH_API_URL = PERPLEXITY_SEARCH_API_URL
|
||||
app.state.config.SOUGOU_API_SID = SOUGOU_API_SID
|
||||
app.state.config.SOUGOU_API_SK = SOUGOU_API_SK
|
||||
app.state.config.EXTERNAL_WEB_SEARCH_URL = EXTERNAL_WEB_SEARCH_URL
|
||||
app.state.config.EXTERNAL_WEB_SEARCH_API_KEY = EXTERNAL_WEB_SEARCH_API_KEY
|
||||
app.state.config.EXTERNAL_WEB_LOADER_URL = EXTERNAL_WEB_LOADER_URL
|
||||
app.state.config.EXTERNAL_WEB_LOADER_API_KEY = EXTERNAL_WEB_LOADER_API_KEY
|
||||
app.state.config.YANDEX_WEB_SEARCH_URL = YANDEX_WEB_SEARCH_URL
|
||||
app.state.config.YANDEX_WEB_SEARCH_API_KEY = YANDEX_WEB_SEARCH_API_KEY
|
||||
app.state.config.YANDEX_WEB_SEARCH_CONFIG = YANDEX_WEB_SEARCH_CONFIG
|
||||
app.state.config.YOUCOM_API_KEY = YOUCOM_API_KEY
|
||||
|
||||
|
||||
app.state.config.PLAYWRIGHT_WS_URL = PLAYWRIGHT_WS_URL
|
||||
app.state.config.PLAYWRIGHT_TIMEOUT = PLAYWRIGHT_TIMEOUT
|
||||
app.state.config.FIRECRAWL_API_BASE_URL = FIRECRAWL_API_BASE_URL
|
||||
app.state.config.FIRECRAWL_API_KEY = FIRECRAWL_API_KEY
|
||||
app.state.config.FIRECRAWL_TIMEOUT = FIRECRAWL_TIMEOUT
|
||||
app.state.config.TAVILY_EXTRACT_DEPTH = TAVILY_EXTRACT_DEPTH
|
||||
|
||||
app.state.EMBEDDING_FUNCTION = None
|
||||
app.state.RERANKING_FUNCTION = None
|
||||
app.state.ef = None
|
||||
app.state.rf = None
|
||||
|
||||
app.state.YOUTUBE_LOADER_TRANSLATION = None
|
||||
|
||||
|
||||
try:
|
||||
app.state.ef = get_ef(app.state.config.RAG_EMBEDDING_ENGINE, app.state.config.RAG_EMBEDDING_MODEL)
|
||||
if app.state.config.ENABLE_RAG_HYBRID_SEARCH and not app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL:
|
||||
app.state.rf = get_rf(
|
||||
app.state.config.RAG_RERANKING_ENGINE,
|
||||
app.state.config.RAG_RERANKING_MODEL,
|
||||
app.state.config.RAG_EXTERNAL_RERANKER_URL,
|
||||
app.state.config.RAG_EXTERNAL_RERANKER_API_KEY,
|
||||
app.state.config.RAG_EXTERNAL_RERANKER_TIMEOUT,
|
||||
)
|
||||
app.state.ef = get_ef(rag_config.get('rag.embedding_engine'), rag_config.get('rag.embedding_model'))
|
||||
if rag_config.get('rag.enable_hybrid_search') and not rag_config.get('rag.bypass_embedding_and_retrieval'):
|
||||
app.state.rf = get_rf(
|
||||
rag_config.get('rag.reranking_engine'),
|
||||
rag_config.get('rag.reranking_model'),
|
||||
rag_config.get('rag.external_reranker_url'),
|
||||
rag_config.get('rag.external_reranker_api_key'),
|
||||
rag_config.get('rag.external_reranker_timeout'),
|
||||
)
|
||||
else:
|
||||
app.state.rf = None
|
||||
except Exception as e:
|
||||
log.error(f'Error updating models: {e}')
|
||||
else:
|
||||
app.state.rf = None
|
||||
except Exception as e:
|
||||
log.error(f'Error updating models: {e}')
|
||||
pass
|
||||
|
||||
rag_config = await Config.get_many(
|
||||
'rag.embedding_engine',
|
||||
'rag.embedding_model',
|
||||
'rag.openai.api_base_url',
|
||||
'rag.ollama.base_url',
|
||||
'rag.azure_openai.base_url',
|
||||
'rag.openai.api_key',
|
||||
'rag.ollama.api_key',
|
||||
'rag.azure_openai.api_key',
|
||||
'rag.embedding_batch_size',
|
||||
'rag.azure_openai.api_version',
|
||||
'rag.enable_async_embedding',
|
||||
'rag.embedding_concurrent_requests',
|
||||
'rag.reranking_engine',
|
||||
'rag.reranking_model',
|
||||
'rag.reranking_batch_size',
|
||||
)
|
||||
embedding_engine = rag_config.get('rag.embedding_engine')
|
||||
app.state.EMBEDDING_FUNCTION = get_embedding_function(
|
||||
embedding_engine,
|
||||
rag_config.get('rag.embedding_model'),
|
||||
embedding_function=app.state.ef,
|
||||
url=(
|
||||
rag_config.get('rag.openai.api_base_url')
|
||||
if embedding_engine == 'openai'
|
||||
else (
|
||||
rag_config.get('rag.ollama.base_url')
|
||||
if embedding_engine == 'ollama'
|
||||
else rag_config.get('rag.azure_openai.base_url')
|
||||
)
|
||||
),
|
||||
key=(
|
||||
rag_config.get('rag.openai.api_key')
|
||||
if embedding_engine == 'openai'
|
||||
else (
|
||||
rag_config.get('rag.ollama.api_key')
|
||||
if embedding_engine == 'ollama'
|
||||
else rag_config.get('rag.azure_openai.api_key')
|
||||
)
|
||||
),
|
||||
embedding_batch_size=rag_config.get('rag.embedding_batch_size'),
|
||||
azure_api_version=(
|
||||
rag_config.get('rag.azure_openai.api_version') if embedding_engine == 'azure_openai' else None
|
||||
),
|
||||
enable_async=rag_config.get('rag.enable_async_embedding'),
|
||||
concurrent_requests=rag_config.get('rag.embedding_concurrent_requests'),
|
||||
)
|
||||
|
||||
app.state.RERANKING_FUNCTION = get_reranking_function(
|
||||
rag_config.get('rag.reranking_engine'),
|
||||
rag_config.get('rag.reranking_model'),
|
||||
reranking_function=app.state.rf,
|
||||
reranking_batch_size=rag_config.get('rag.reranking_batch_size'),
|
||||
)
|
||||
app.state.EMBEDDING_FUNCTION = get_embedding_function(
|
||||
app.state.config.RAG_EMBEDDING_ENGINE,
|
||||
app.state.config.RAG_EMBEDDING_MODEL,
|
||||
embedding_function=app.state.ef,
|
||||
url=(
|
||||
app.state.config.RAG_OPENAI_API_BASE_URL
|
||||
if app.state.config.RAG_EMBEDDING_ENGINE == 'openai'
|
||||
else (
|
||||
app.state.config.RAG_OLLAMA_BASE_URL
|
||||
if app.state.config.RAG_EMBEDDING_ENGINE == 'ollama'
|
||||
else app.state.config.RAG_AZURE_OPENAI_BASE_URL
|
||||
)
|
||||
),
|
||||
key=(
|
||||
app.state.config.RAG_OPENAI_API_KEY
|
||||
if app.state.config.RAG_EMBEDDING_ENGINE == 'openai'
|
||||
else (
|
||||
app.state.config.RAG_OLLAMA_API_KEY
|
||||
if app.state.config.RAG_EMBEDDING_ENGINE == 'ollama'
|
||||
else app.state.config.RAG_AZURE_OPENAI_API_KEY
|
||||
)
|
||||
),
|
||||
embedding_batch_size=app.state.config.RAG_EMBEDDING_BATCH_SIZE,
|
||||
azure_api_version=(
|
||||
app.state.config.RAG_AZURE_OPENAI_API_VERSION
|
||||
if app.state.config.RAG_EMBEDDING_ENGINE == 'azure_openai'
|
||||
else None
|
||||
),
|
||||
enable_async=app.state.config.ENABLE_ASYNC_EMBEDDING,
|
||||
concurrent_requests=app.state.config.RAG_EMBEDDING_CONCURRENT_REQUESTS,
|
||||
)
|
||||
|
||||
app.state.RERANKING_FUNCTION = get_reranking_function(
|
||||
app.state.config.RAG_RERANKING_ENGINE,
|
||||
app.state.config.RAG_RERANKING_MODEL,
|
||||
reranking_function=app.state.rf,
|
||||
reranking_batch_size=app.state.config.RAG_RERANKING_BATCH_SIZE,
|
||||
)
|
||||
|
||||
########################################
|
||||
#
|
||||
@@ -661,6 +1213,23 @@ async def initialize_runtime_config(app: FastAPI):
|
||||
#
|
||||
########################################
|
||||
|
||||
app.state.config.ENABLE_CODE_EXECUTION = ENABLE_CODE_EXECUTION
|
||||
app.state.config.CODE_EXECUTION_ENGINE = CODE_EXECUTION_ENGINE
|
||||
app.state.config.CODE_EXECUTION_JUPYTER_URL = CODE_EXECUTION_JUPYTER_URL
|
||||
app.state.config.CODE_EXECUTION_JUPYTER_AUTH = CODE_EXECUTION_JUPYTER_AUTH
|
||||
app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN = CODE_EXECUTION_JUPYTER_AUTH_TOKEN
|
||||
app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = CODE_EXECUTION_JUPYTER_AUTH_PASSWORD
|
||||
app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT = CODE_EXECUTION_JUPYTER_TIMEOUT
|
||||
|
||||
app.state.config.ENABLE_CODE_INTERPRETER = ENABLE_CODE_INTERPRETER
|
||||
app.state.config.CODE_INTERPRETER_ENGINE = CODE_INTERPRETER_ENGINE
|
||||
app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE = CODE_INTERPRETER_PROMPT_TEMPLATE
|
||||
|
||||
app.state.config.CODE_INTERPRETER_JUPYTER_URL = CODE_INTERPRETER_JUPYTER_URL
|
||||
app.state.config.CODE_INTERPRETER_JUPYTER_AUTH = CODE_INTERPRETER_JUPYTER_AUTH
|
||||
app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN = CODE_INTERPRETER_JUPYTER_AUTH_TOKEN
|
||||
app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD
|
||||
app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT = CODE_INTERPRETER_JUPYTER_TIMEOUT
|
||||
|
||||
########################################
|
||||
#
|
||||
@@ -668,6 +1237,48 @@ async def initialize_runtime_config(app: FastAPI):
|
||||
#
|
||||
########################################
|
||||
|
||||
app.state.config.IMAGE_GENERATION_ENGINE = IMAGE_GENERATION_ENGINE
|
||||
app.state.config.ENABLE_IMAGE_GENERATION = ENABLE_IMAGE_GENERATION
|
||||
app.state.config.ENABLE_IMAGE_PROMPT_GENERATION = ENABLE_IMAGE_PROMPT_GENERATION
|
||||
app.state.config.ENABLE_MEMORIES = ENABLE_MEMORIES
|
||||
|
||||
app.state.config.IMAGE_GENERATION_MODEL = IMAGE_GENERATION_MODEL
|
||||
app.state.config.IMAGE_SIZE = IMAGE_SIZE
|
||||
app.state.config.IMAGE_STEPS = IMAGE_STEPS
|
||||
|
||||
app.state.config.IMAGES_OPENAI_API_BASE_URL = IMAGES_OPENAI_API_BASE_URL
|
||||
app.state.config.IMAGES_OPENAI_API_VERSION = IMAGES_OPENAI_API_VERSION
|
||||
app.state.config.IMAGES_OPENAI_API_KEY = IMAGES_OPENAI_API_KEY
|
||||
app.state.config.IMAGES_OPENAI_API_PARAMS = IMAGES_OPENAI_API_PARAMS
|
||||
|
||||
app.state.config.IMAGES_GEMINI_API_BASE_URL = IMAGES_GEMINI_API_BASE_URL
|
||||
app.state.config.IMAGES_GEMINI_API_KEY = IMAGES_GEMINI_API_KEY
|
||||
app.state.config.IMAGES_GEMINI_ENDPOINT_METHOD = IMAGES_GEMINI_ENDPOINT_METHOD
|
||||
|
||||
app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
|
||||
app.state.config.AUTOMATIC1111_API_AUTH = AUTOMATIC1111_API_AUTH
|
||||
app.state.config.AUTOMATIC1111_PARAMS = AUTOMATIC1111_PARAMS
|
||||
|
||||
app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
|
||||
app.state.config.COMFYUI_API_KEY = COMFYUI_API_KEY
|
||||
app.state.config.COMFYUI_WORKFLOW = COMFYUI_WORKFLOW
|
||||
app.state.config.COMFYUI_WORKFLOW_NODES = COMFYUI_WORKFLOW_NODES
|
||||
|
||||
|
||||
app.state.config.ENABLE_IMAGE_EDIT = ENABLE_IMAGE_EDIT
|
||||
app.state.config.IMAGE_EDIT_ENGINE = IMAGE_EDIT_ENGINE
|
||||
app.state.config.IMAGE_EDIT_MODEL = IMAGE_EDIT_MODEL
|
||||
app.state.config.IMAGE_EDIT_SIZE = IMAGE_EDIT_SIZE
|
||||
app.state.config.IMAGES_EDIT_OPENAI_API_BASE_URL = IMAGES_EDIT_OPENAI_API_BASE_URL
|
||||
app.state.config.IMAGES_EDIT_OPENAI_API_KEY = IMAGES_EDIT_OPENAI_API_KEY
|
||||
app.state.config.IMAGES_EDIT_OPENAI_API_VERSION = IMAGES_EDIT_OPENAI_API_VERSION
|
||||
app.state.config.IMAGES_EDIT_GEMINI_API_BASE_URL = IMAGES_EDIT_GEMINI_API_BASE_URL
|
||||
app.state.config.IMAGES_EDIT_GEMINI_API_KEY = IMAGES_EDIT_GEMINI_API_KEY
|
||||
app.state.config.IMAGES_EDIT_COMFYUI_BASE_URL = IMAGES_EDIT_COMFYUI_BASE_URL
|
||||
app.state.config.IMAGES_EDIT_COMFYUI_API_KEY = IMAGES_EDIT_COMFYUI_API_KEY
|
||||
app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW = IMAGES_EDIT_COMFYUI_WORKFLOW
|
||||
app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES = IMAGES_EDIT_COMFYUI_WORKFLOW_NODES
|
||||
|
||||
|
||||
########################################
|
||||
#
|
||||
@@ -675,6 +1286,46 @@ async def initialize_runtime_config(app: FastAPI):
|
||||
#
|
||||
########################################
|
||||
|
||||
app.state.config.STT_ENGINE = AUDIO_STT_ENGINE
|
||||
app.state.config.STT_MODEL = AUDIO_STT_MODEL
|
||||
app.state.config.STT_SUPPORTED_CONTENT_TYPES = AUDIO_STT_SUPPORTED_CONTENT_TYPES
|
||||
|
||||
app.state.config.STT_OPENAI_API_BASE_URL = AUDIO_STT_OPENAI_API_BASE_URL
|
||||
app.state.config.STT_OPENAI_API_KEY = AUDIO_STT_OPENAI_API_KEY
|
||||
|
||||
app.state.config.WHISPER_MODEL = WHISPER_MODEL
|
||||
app.state.config.DEEPGRAM_API_KEY = DEEPGRAM_API_KEY
|
||||
|
||||
app.state.config.AUDIO_STT_AZURE_API_KEY = AUDIO_STT_AZURE_API_KEY
|
||||
app.state.config.AUDIO_STT_AZURE_REGION = AUDIO_STT_AZURE_REGION
|
||||
app.state.config.AUDIO_STT_AZURE_LOCALES = AUDIO_STT_AZURE_LOCALES
|
||||
app.state.config.AUDIO_STT_AZURE_BASE_URL = AUDIO_STT_AZURE_BASE_URL
|
||||
app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS = AUDIO_STT_AZURE_MAX_SPEAKERS
|
||||
|
||||
app.state.config.AUDIO_STT_MISTRAL_API_KEY = AUDIO_STT_MISTRAL_API_KEY
|
||||
app.state.config.AUDIO_STT_MISTRAL_API_BASE_URL = AUDIO_STT_MISTRAL_API_BASE_URL
|
||||
app.state.config.AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS = AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS
|
||||
|
||||
app.state.config.TTS_ENGINE = AUDIO_TTS_ENGINE
|
||||
|
||||
app.state.config.TTS_MODEL = AUDIO_TTS_MODEL
|
||||
app.state.config.TTS_VOICE = AUDIO_TTS_VOICE
|
||||
|
||||
app.state.config.TTS_OPENAI_API_BASE_URL = AUDIO_TTS_OPENAI_API_BASE_URL
|
||||
app.state.config.TTS_OPENAI_API_KEY = AUDIO_TTS_OPENAI_API_KEY
|
||||
app.state.config.TTS_OPENAI_PARAMS = AUDIO_TTS_OPENAI_PARAMS
|
||||
|
||||
app.state.config.TTS_API_KEY = AUDIO_TTS_API_KEY
|
||||
app.state.config.TTS_SPLIT_ON = AUDIO_TTS_SPLIT_ON
|
||||
|
||||
|
||||
app.state.config.TTS_AZURE_SPEECH_REGION = AUDIO_TTS_AZURE_SPEECH_REGION
|
||||
app.state.config.TTS_AZURE_SPEECH_BASE_URL = AUDIO_TTS_AZURE_SPEECH_BASE_URL
|
||||
app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT = AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT
|
||||
|
||||
app.state.config.TTS_MISTRAL_API_KEY = AUDIO_TTS_MISTRAL_API_KEY
|
||||
app.state.config.TTS_MISTRAL_API_BASE_URL = AUDIO_TTS_MISTRAL_API_BASE_URL
|
||||
|
||||
|
||||
app.state.faster_whisper_model = None
|
||||
app.state.speech_synthesiser = None
|
||||
@@ -688,6 +1339,30 @@ app.state.speech_speaker_embeddings_dataset = None
|
||||
########################################
|
||||
|
||||
|
||||
app.state.config.TASK_MODEL = TASK_MODEL
|
||||
app.state.config.TASK_MODEL_EXTERNAL = TASK_MODEL_EXTERNAL
|
||||
|
||||
|
||||
app.state.config.ENABLE_SEARCH_QUERY_GENERATION = ENABLE_SEARCH_QUERY_GENERATION
|
||||
app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION = ENABLE_RETRIEVAL_QUERY_GENERATION
|
||||
app.state.config.ENABLE_AUTOCOMPLETE_GENERATION = ENABLE_AUTOCOMPLETE_GENERATION
|
||||
app.state.config.ENABLE_TAGS_GENERATION = ENABLE_TAGS_GENERATION
|
||||
app.state.config.ENABLE_TITLE_GENERATION = ENABLE_TITLE_GENERATION
|
||||
app.state.config.ENABLE_FOLLOW_UP_GENERATION = ENABLE_FOLLOW_UP_GENERATION
|
||||
|
||||
|
||||
app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = TITLE_GENERATION_PROMPT_TEMPLATE
|
||||
app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE = TAGS_GENERATION_PROMPT_TEMPLATE
|
||||
app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE = IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE
|
||||
app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE = FOLLOW_UP_GENERATION_PROMPT_TEMPLATE
|
||||
|
||||
app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE
|
||||
app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE = QUERY_GENERATION_PROMPT_TEMPLATE
|
||||
app.state.config.AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE = AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE
|
||||
app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH = AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH
|
||||
app.state.config.VOICE_MODE_PROMPT_TEMPLATE = VOICE_MODE_PROMPT_TEMPLATE
|
||||
|
||||
|
||||
########################################
|
||||
#
|
||||
# WEBUI
|
||||
@@ -824,11 +1499,7 @@ async def get_models(request: Request, refresh: bool = False, user=Depends(get_v
|
||||
|
||||
models.append(model)
|
||||
|
||||
# Chat requests resolve models by ID from request.app.state.MODELS, where
|
||||
# duplicate IDs collapse to the last model. Return the same effective list.
|
||||
models = list({model['id']: model for model in models}.values())
|
||||
|
||||
model_order_list = await Config.get('ui.model_order_list')
|
||||
model_order_list = request.app.state.config.MODEL_ORDER_LIST
|
||||
if model_order_list:
|
||||
model_order_dict = {model_id: i for i, model_id in enumerate(model_order_list)}
|
||||
# Sort models by order list priority, with fallback for those not in the list
|
||||
@@ -853,127 +1524,6 @@ async def get_base_models(request: Request, user=Depends(get_admin_user)):
|
||||
return {'data': models}
|
||||
|
||||
|
||||
class ModelUnloadForm(BaseModel):
|
||||
model: str
|
||||
|
||||
|
||||
def strip_provider_model_prefix(model_id: str, prefix_id: str | None) -> str:
|
||||
if prefix_id and model_id.startswith(f'{prefix_id}.'):
|
||||
return model_id[len(f'{prefix_id}.') :]
|
||||
return model_id
|
||||
|
||||
|
||||
@app.post('/api/models/unload')
|
||||
async def unload_model(request: Request, form_data: ModelUnloadForm, user=Depends(get_admin_user)):
|
||||
"""
|
||||
Unified model unload endpoint.
|
||||
Resolves the provider that owns the model and calls its native unload mechanism.
|
||||
Supports: Ollama (keep_alive=0) and llama.cpp (/models/unload).
|
||||
"""
|
||||
model_id = form_data.model
|
||||
|
||||
ollama_models = getattr(request.app.state, 'OLLAMA_MODELS', None) or {}
|
||||
openai_models = getattr(request.app.state, 'OPENAI_MODELS', None) or {}
|
||||
|
||||
seen = set()
|
||||
while model_id not in ollama_models and model_id not in openai_models and model_id not in seen:
|
||||
seen.add(model_id)
|
||||
model_info = await Models.get_model_by_id(model_id)
|
||||
if not model_info or not model_info.base_model_id:
|
||||
break
|
||||
model_id = model_info.base_model_id
|
||||
|
||||
# --- Ollama provider ---
|
||||
if model_id in ollama_models:
|
||||
ollama_config = await Config.get_many('ollama.base_urls', 'ollama.api_configs')
|
||||
ollama_base_urls = ollama_config.get('ollama.base_urls') or []
|
||||
ollama_api_configs = ollama_config.get('ollama.api_configs') or {}
|
||||
url_indices = ollama_models[model_id].get('urls', [])
|
||||
errors = []
|
||||
for idx in url_indices:
|
||||
url = ollama_base_urls[idx]
|
||||
api_config = ollama_api_configs.get(
|
||||
str(idx),
|
||||
ollama_api_configs.get(url, {}),
|
||||
)
|
||||
key = api_config.get('key', None)
|
||||
|
||||
prefix_id = api_config.get('prefix_id', None)
|
||||
actual_model = strip_provider_model_prefix(model_id, prefix_id)
|
||||
|
||||
payload = json.dumps({'model': actual_model, 'keep_alive': 0, 'prompt': ''})
|
||||
|
||||
try:
|
||||
timeout = aiohttp.ClientTimeout(total=30)
|
||||
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
**({'Authorization': f'Bearer {key}'} if key else {}),
|
||||
}
|
||||
async with session.post(
|
||||
f'{url}/api/generate',
|
||||
data=payload,
|
||||
headers=headers,
|
||||
) as r:
|
||||
if not r.ok:
|
||||
errors.append({'url_idx': idx, 'error': await r.text()})
|
||||
except Exception as e:
|
||||
log.exception(f'Failed to unload model on Ollama node {idx}: {e}')
|
||||
errors.append({'url_idx': idx, 'error': str(e)})
|
||||
|
||||
if errors:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f'Failed to unload model on {len(errors)} node(s): {errors}',
|
||||
)
|
||||
return {'status': True}
|
||||
|
||||
# --- OpenAI-compatible providers ---
|
||||
if model_id in openai_models:
|
||||
openai_config = await Config.get_many('openai.api_configs', 'openai.api_base_urls', 'openai.api_keys')
|
||||
openai_api_configs = openai_config.get('openai.api_configs') or {}
|
||||
openai_base_urls = openai_config.get('openai.api_base_urls') or []
|
||||
openai_api_keys = openai_config.get('openai.api_keys') or []
|
||||
model_info = openai_models[model_id]
|
||||
idx = model_info.get('urlIdx')
|
||||
api_config = openai_api_configs.get(str(idx), {})
|
||||
provider = api_config.get('provider', '')
|
||||
base_url = openai_base_urls[idx]
|
||||
key = openai_api_keys[idx] if idx < len(openai_api_keys) else ''
|
||||
|
||||
if provider == 'llama.cpp':
|
||||
root_url = base_url.rstrip('/').removesuffix('/v1')
|
||||
actual_model = strip_provider_model_prefix(model_id, api_config.get('prefix_id'))
|
||||
try:
|
||||
timeout = aiohttp.ClientTimeout(total=30)
|
||||
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
**({'Authorization': f'Bearer {key}'} if key else {}),
|
||||
}
|
||||
async with session.post(
|
||||
f'{root_url}/models/unload',
|
||||
json={'model': actual_model},
|
||||
headers=headers,
|
||||
) as r:
|
||||
if not r.ok:
|
||||
detail = await r.text()
|
||||
raise HTTPException(status_code=r.status, detail=detail)
|
||||
return await r.json()
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
log.exception(f'Failed to unload model via llama.cpp: {e}')
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f'Provider "{provider or "default"}" does not support model unloading',
|
||||
)
|
||||
|
||||
raise HTTPException(status_code=404, detail=f'Model "{model_id}" not found')
|
||||
|
||||
|
||||
##################################
|
||||
# Embeddings
|
||||
##################################
|
||||
@@ -1041,24 +1591,18 @@ async def chat_completion(
|
||||
request.state.model = model
|
||||
|
||||
# Model params: global defaults as base, per-model overrides win
|
||||
default_model_params = await Config.get('models.default_params', {}) or {}
|
||||
default_model_params = getattr(request.app.state.config, 'DEFAULT_MODEL_PARAMS', None) or {}
|
||||
model_info_params = {
|
||||
**default_model_params,
|
||||
**(model_info.params.model_dump() if model_info and model_info.params else {}),
|
||||
}
|
||||
request_params = {key: value for key, value in (form_data.get('params') or {}).items() if value is not None}
|
||||
if model_info_params or request_params:
|
||||
form_data['params'] = {
|
||||
**model_info_params,
|
||||
**request_params,
|
||||
}
|
||||
|
||||
# Check base model existence for custom models
|
||||
if model_info and model_info.base_model_id:
|
||||
base_model_id = model_info.base_model_id
|
||||
if base_model_id not in request.app.state.MODELS:
|
||||
if ENABLE_CUSTOM_MODEL_FALLBACK:
|
||||
default_models = ((await Config.get('ui.default_models')) or '').split(',')
|
||||
default_models = (request.app.state.config.DEFAULT_MODELS or '').split(',')
|
||||
|
||||
fallback_model_id = default_models[0].strip() if default_models[0] else None
|
||||
|
||||
@@ -1074,7 +1618,6 @@ async def chat_completion(
|
||||
# Chat Params
|
||||
stream_delta_chunk_size = form_data.get('params', {}).get('stream_delta_chunk_size')
|
||||
reasoning_tags = form_data.get('params', {}).get('reasoning_tags')
|
||||
compact_token_threshold = form_data.get('params', {}).get('compact_token_threshold')
|
||||
|
||||
# Model Params
|
||||
if model_info_params.get('stream_response') is not None:
|
||||
@@ -1086,9 +1629,6 @@ async def chat_completion(
|
||||
if model_info_params.get('reasoning_tags') is not None:
|
||||
reasoning_tags = model_info_params.get('reasoning_tags')
|
||||
|
||||
if model_info_params.get('compact_token_threshold') is not None:
|
||||
compact_token_threshold = model_info_params.get('compact_token_threshold')
|
||||
|
||||
# parent_id signals intent:
|
||||
# null → new chat (root message, no parent)
|
||||
# value → follow-up (user message's parentId = prev assistant)
|
||||
@@ -1097,48 +1637,25 @@ async def chat_completion(
|
||||
parent_id = form_data.pop('parent_id', None)
|
||||
form_data.pop('new_chat', None) # Legacy field
|
||||
|
||||
# Multi-model message_ids: list of {model_id, message_id} entries.
|
||||
# Supports both the new array format and legacy dict format for backward compat.
|
||||
# Multi-model: {model_id: assistant_message_id}
|
||||
# Single-model fallback: built from 'model' + 'id'
|
||||
message_ids = form_data.pop('message_ids', None)
|
||||
if isinstance(message_ids, list):
|
||||
# New format: [{"model_id": ..., "message_id": ...}, ...]
|
||||
form_data.pop('id', None)
|
||||
elif isinstance(message_ids, dict):
|
||||
# Legacy dict format: {model_id: message_id} — convert to list
|
||||
message_ids = [{'model_id': k, 'message_id': v} for k, v in message_ids.items()]
|
||||
form_data.pop('id', None)
|
||||
if not message_ids:
|
||||
message_ids = {model_id: form_data.pop('id', None)}
|
||||
else:
|
||||
# Single-model fallback
|
||||
message_ids = [{'model_id': model_id, 'message_id': form_data.pop('id', None)}]
|
||||
form_data.pop('id', None)
|
||||
|
||||
user_message = form_data.pop('user_message', None) or form_data.pop('parent_message', None)
|
||||
|
||||
# Drop tool_servers if caller lacks features.direct_tool_servers —
|
||||
# mirrors the storage-side strip in user/settings/update.
|
||||
tool_servers = form_data.pop('tool_servers', None)
|
||||
if (
|
||||
tool_servers
|
||||
and user.role != 'admin'
|
||||
and not await has_permission(
|
||||
user.id,
|
||||
'features.direct_tool_servers',
|
||||
await Config.get('user.permissions'),
|
||||
)
|
||||
):
|
||||
tool_servers = None
|
||||
|
||||
metadata = {
|
||||
'user_id': user.id,
|
||||
'user_agent': request.headers.get('user-agent', '') or '',
|
||||
'chat_id': form_data.pop('chat_id', None) or '',
|
||||
'chat_id': form_data.pop('chat_id', None),
|
||||
'user_message': user_message,
|
||||
'user_message_id': user_message.get('id') if user_message else None,
|
||||
'assistant_message_id': form_data.pop('assistant_message_id', None),
|
||||
'session_id': form_data.pop('session_id', None),
|
||||
'folder_id': form_data.pop('folder_id', None),
|
||||
'filter_ids': form_data.pop('filter_ids', []),
|
||||
'tool_ids': form_data.get('tool_ids', None),
|
||||
'tool_servers': tool_servers,
|
||||
'tool_servers': form_data.pop('tool_servers', None),
|
||||
'files': form_data.get('files', None),
|
||||
'features': form_data.get('features', {}),
|
||||
'variables': form_data.get('variables', {}),
|
||||
@@ -1147,11 +1664,13 @@ async def chat_completion(
|
||||
'params': {
|
||||
'stream_delta_chunk_size': stream_delta_chunk_size,
|
||||
'reasoning_tags': reasoning_tags,
|
||||
'compact_token_threshold': compact_token_threshold,
|
||||
'function_calling': (
|
||||
form_data.get('params', {}).get('function_calling')
|
||||
or model_info_params.get('function_calling')
|
||||
or 'native'
|
||||
'native'
|
||||
if (
|
||||
form_data.get('params', {}).get('function_calling') == 'native'
|
||||
or model_info_params.get('function_calling') == 'native'
|
||||
)
|
||||
else 'default'
|
||||
),
|
||||
},
|
||||
}
|
||||
@@ -1159,70 +1678,22 @@ async def chat_completion(
|
||||
if is_new_chat:
|
||||
metadata['chat_id'] = str(uuid4())
|
||||
|
||||
initial_title_generation = None
|
||||
if is_new_chat and tasks and TASKS.TITLE_GENERATION in tasks:
|
||||
initial_title_generation = tasks.pop(TASKS.TITLE_GENERATION)
|
||||
|
||||
if metadata.get('chat_id') and user:
|
||||
chat_id = metadata['chat_id']
|
||||
|
||||
# Gate channel: branch — caller needs write access on the channel
|
||||
# and the supplied message_id must belong to that channel.
|
||||
if chat_id.startswith('channel:'):
|
||||
channel_id = chat_id.removeprefix('channel:')
|
||||
channel = await Channels.get_channel_by_id(channel_id)
|
||||
if not channel:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
if user.role != 'admin':
|
||||
if channel.type in ['group', 'dm']:
|
||||
if not await Channels.is_user_channel_member(channel.id, user.id):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=ERROR_MESSAGES.DEFAULT(),
|
||||
)
|
||||
else:
|
||||
if not await AccessGrants.has_access(
|
||||
user_id=user.id,
|
||||
resource_type='channel',
|
||||
resource_id=channel.id,
|
||||
permission='write',
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=ERROR_MESSAGES.DEFAULT(),
|
||||
)
|
||||
for entry in message_ids:
|
||||
target_message_id = entry.get('message_id')
|
||||
if not target_message_id:
|
||||
continue
|
||||
target_message = await Messages.get_message_by_id(target_message_id)
|
||||
if target_message and target_message.channel_id != channel.id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=ERROR_MESSAGES.DEFAULT(),
|
||||
)
|
||||
|
||||
if not chat_id.startswith('local:') and not chat_id.startswith(
|
||||
'channel:'
|
||||
): # temporary/channel chats are not stored
|
||||
if not chat_id.startswith('local:'): # temporary chats are not stored
|
||||
if is_new_chat:
|
||||
# Build the full history upfront with ALL assistant placeholders
|
||||
user_message = metadata.get('user_message') or {}
|
||||
user_message_id = user_message.get('id') if user_message else None
|
||||
|
||||
history_messages = {}
|
||||
all_assistant_ids = [entry['message_id'] for entry in message_ids if entry.get('message_id')]
|
||||
all_assistant_ids = [assistant_id for assistant_id in message_ids.values() if assistant_id]
|
||||
|
||||
if user_message_id and user_message:
|
||||
user_message['childrenIds'] = all_assistant_ids
|
||||
history_messages[user_message_id] = user_message
|
||||
|
||||
for entry in message_ids:
|
||||
target_model_id = entry['model_id']
|
||||
assistant_message_id = entry['message_id']
|
||||
for target_model_id, assistant_message_id in message_ids.items():
|
||||
if assistant_message_id:
|
||||
history_messages[assistant_message_id] = {
|
||||
'id': assistant_message_id,
|
||||
@@ -1242,7 +1713,7 @@ async def chat_completion(
|
||||
chat={
|
||||
'id': chat_id,
|
||||
'title': 'New Chat',
|
||||
'models': [entry['model_id'] for entry in message_ids],
|
||||
'models': list(message_ids.keys()),
|
||||
'history': {
|
||||
'currentId': all_assistant_ids[0] if all_assistant_ids else user_message_id,
|
||||
'messages': history_messages,
|
||||
@@ -1252,46 +1723,12 @@ async def chat_completion(
|
||||
]
|
||||
if user_message_id
|
||||
else [],
|
||||
'files': metadata.get('files') or [],
|
||||
'tags': [],
|
||||
'timestamp': int(time.time() * 1000),
|
||||
},
|
||||
folder_id=metadata.get('folder_id'),
|
||||
),
|
||||
)
|
||||
await publish_event(
|
||||
request,
|
||||
EVENTS.CHAT_CREATED,
|
||||
actor=user,
|
||||
subject_id=chat_id,
|
||||
data={'title': 'New Chat'},
|
||||
)
|
||||
if user_message_id:
|
||||
await publish_event(
|
||||
request,
|
||||
EVENTS.MESSAGE_CREATED,
|
||||
actor=user,
|
||||
subject_id=user_message_id,
|
||||
data={
|
||||
'chat_id': chat_id,
|
||||
'role': 'user',
|
||||
'content_preview': user_message.get('content', '')[:300],
|
||||
},
|
||||
)
|
||||
for entry in message_ids:
|
||||
assistant_message_id = entry.get('message_id')
|
||||
if assistant_message_id:
|
||||
await publish_event(
|
||||
request,
|
||||
EVENTS.MESSAGE_CREATED,
|
||||
actor=user,
|
||||
subject_id=assistant_message_id,
|
||||
data={
|
||||
'chat_id': chat_id,
|
||||
'role': 'assistant',
|
||||
'model': entry.get('model_id'),
|
||||
},
|
||||
)
|
||||
|
||||
# Insert chat files from user message if any
|
||||
user_message_files = user_message.get('files', [])
|
||||
@@ -1310,29 +1747,6 @@ async def chat_completion(
|
||||
except Exception as e:
|
||||
log.debug(f'Error inserting chat files: {e}')
|
||||
pass
|
||||
|
||||
if initial_title_generation is not None and all_assistant_ids:
|
||||
title_metadata = {
|
||||
**metadata,
|
||||
'message_id': all_assistant_ids[0],
|
||||
}
|
||||
event_emitter = await get_event_emitter(title_metadata, update_db=False)
|
||||
title_ctx = {
|
||||
'request': request,
|
||||
'form_data': form_data,
|
||||
'user': user,
|
||||
'metadata': title_metadata,
|
||||
'tasks': {TASKS.TITLE_GENERATION: initial_title_generation},
|
||||
'event_emitter': event_emitter,
|
||||
}
|
||||
|
||||
async def run_initial_title_generation():
|
||||
try:
|
||||
await background_tasks_handler(title_ctx)
|
||||
except Exception as e:
|
||||
log.debug(f'Error generating initial chat title: {e}')
|
||||
|
||||
asyncio.create_task(run_initial_title_generation())
|
||||
else:
|
||||
# Existing chat — verify ownership
|
||||
if not await Chats.is_chat_owner(chat_id, user.id) and user.role != 'admin':
|
||||
@@ -1341,43 +1755,14 @@ async def chat_completion(
|
||||
detail=ERROR_MESSAGES.DEFAULT(),
|
||||
)
|
||||
|
||||
user_message = metadata.get('user_message') or {}
|
||||
selected_chat_models = user_message.get('models') if isinstance(user_message, dict) else None
|
||||
if not isinstance(selected_chat_models, list) or not selected_chat_models:
|
||||
selected_chat_models = [entry.get('model_id') for entry in message_ids if entry.get('model_id')]
|
||||
|
||||
# Persist chat-level fields the frontend used to save on every message.
|
||||
# The old frontend saveChatHandler did this on every message;
|
||||
# now the backend owns persistence.
|
||||
chat_files = metadata.get('files')
|
||||
if chat_files is not None or selected_chat_models:
|
||||
existing_chat = await Chats.get_chat_by_id(chat_id)
|
||||
if existing_chat:
|
||||
updated = {**existing_chat.chat}
|
||||
if chat_files is not None:
|
||||
updated['files'] = chat_files
|
||||
if selected_chat_models:
|
||||
updated['models'] = selected_chat_models
|
||||
await Chats.update_chat_by_id(chat_id, updated)
|
||||
|
||||
# Save user message to DB
|
||||
user_message = metadata.get('user_message') or {}
|
||||
if user_message and user_message.get('id'):
|
||||
await Chats.upsert_message_to_chat_by_id_and_message_id(
|
||||
chat_id,
|
||||
user_message['id'],
|
||||
user_message,
|
||||
)
|
||||
await publish_event(
|
||||
request,
|
||||
EVENTS.MESSAGE_CREATED,
|
||||
actor=user,
|
||||
subject_id=user_message['id'],
|
||||
data={
|
||||
'chat_id': chat_id,
|
||||
'role': user_message.get('role', 'user'),
|
||||
'content_preview': user_message.get('content', '')[:300],
|
||||
},
|
||||
)
|
||||
|
||||
# Link grandparent → user message (childrenIds)
|
||||
grandparent_id = user_message.get('parentId')
|
||||
@@ -1411,7 +1796,7 @@ async def chat_completion(
|
||||
|
||||
# Save ALL assistant placeholders
|
||||
user_message_id = metadata.get('user_message_id')
|
||||
all_assistant_ids = [entry['message_id'] for entry in message_ids if entry.get('message_id')]
|
||||
all_assistant_ids = [assistant_id for assistant_id in message_ids.values() if assistant_id]
|
||||
|
||||
# Link user message → all assistant messages (childrenIds)
|
||||
if user_message_id and all_assistant_ids:
|
||||
@@ -1428,9 +1813,7 @@ async def chat_completion(
|
||||
)
|
||||
|
||||
# Save each assistant placeholder
|
||||
for entry in message_ids:
|
||||
target_model_id = entry['model_id']
|
||||
assistant_message_id = entry['message_id']
|
||||
for target_model_id, assistant_message_id in message_ids.items():
|
||||
if assistant_message_id:
|
||||
await Chats.upsert_message_to_chat_by_id_and_message_id(
|
||||
chat_id,
|
||||
@@ -1446,17 +1829,6 @@ async def chat_completion(
|
||||
'timestamp': int(time.time()),
|
||||
},
|
||||
)
|
||||
await publish_event(
|
||||
request,
|
||||
EVENTS.MESSAGE_CREATED,
|
||||
actor=user,
|
||||
subject_id=assistant_message_id,
|
||||
data={
|
||||
'chat_id': chat_id,
|
||||
'role': 'assistant',
|
||||
'model': target_model_id,
|
||||
},
|
||||
)
|
||||
|
||||
request.state.metadata = metadata
|
||||
form_data['metadata'] = metadata
|
||||
@@ -1513,9 +1885,7 @@ async def chat_completion(
|
||||
if metadata.get('chat_id') and metadata.get('message_id'):
|
||||
# Update the chat message with the error
|
||||
try:
|
||||
if not metadata.get('chat_id', '').startswith('local:') and not metadata.get(
|
||||
'chat_id', ''
|
||||
).startswith('channel:'):
|
||||
if not metadata['chat_id'].startswith('local:'):
|
||||
await Chats.upsert_message_to_chat_by_id_and_message_id(
|
||||
metadata['chat_id'],
|
||||
metadata['message_id'],
|
||||
@@ -1549,43 +1919,42 @@ async def chat_completion(
|
||||
detail=error_detail,
|
||||
)
|
||||
finally:
|
||||
# Clean up MCP clients. Each client is isolated so one
|
||||
# failure doesn't skip the rest.
|
||||
#
|
||||
# NOTE: asyncio.wait_for() / asyncio.shield() must NOT be used
|
||||
# here — they create new asyncio Tasks, which violate anyio
|
||||
# cancel-scope task-ownership rules when the MCPClient's
|
||||
# exit_stack contains anyio transport resources (streamable_http).
|
||||
# Exiting those cancel scopes from the wrong task raises
|
||||
# "Attempted to exit a cancel scope that isn't the current
|
||||
# task's current cancel scope", which propagates as a
|
||||
# BaseException through the finally block, discards the response
|
||||
# return value, and surfaces as a 500 "No response returned."
|
||||
# MCPClient.disconnect() suppresses known transport teardown errors
|
||||
# while still propagating real task cancellation.
|
||||
# MCP cleanup — MUST run in the SAME asyncio task as
|
||||
# connect() because the MCP SDK's streamablehttp_client
|
||||
# uses anyio task groups whose cancel scopes enforce
|
||||
# same-task exit. Do NOT wrap in asyncio.shield() or
|
||||
# asyncio.wait_for() — both create a new task.
|
||||
try:
|
||||
if mcp_clients := metadata.get('mcp_clients'):
|
||||
for client in reversed(list(mcp_clients.values())):
|
||||
try:
|
||||
await client.disconnect()
|
||||
except BaseException as e:
|
||||
except Exception as e:
|
||||
log.debug(f'Error disconnecting MCP client: {e}')
|
||||
except BaseException as e:
|
||||
except asyncio.CancelledError:
|
||||
# Let the client close asynchronously by GC
|
||||
pass
|
||||
except Exception as e:
|
||||
log.debug(f'Error cleaning up MCP clients: {e}')
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
# Deregister this task, then emit chat:active=false if no others remain
|
||||
try:
|
||||
chat_id = metadata.get('chat_id')
|
||||
task_id = metadata.get('task_id')
|
||||
if chat_id and task_id:
|
||||
await cleanup_task(request.app.state.redis, task_id, chat_id)
|
||||
if not await has_active_tasks(request.app.state.redis, chat_id):
|
||||
event_emitter = await get_event_emitter(metadata, update_db=False)
|
||||
if event_emitter:
|
||||
try:
|
||||
await asyncio.shield(event_emitter({'type': 'chat:active', 'data': {'active': False}}))
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
if metadata.get('chat_id'):
|
||||
|
||||
async def emit_inactive_event():
|
||||
try:
|
||||
event_emitter = await get_event_emitter(metadata, update_db=False)
|
||||
if event_emitter:
|
||||
await event_emitter({'type': 'chat:active', 'data': {'active': False}})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
# Shield the event emission so it finishes even if the main task is cancelled
|
||||
await asyncio.shield(emit_inactive_event())
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -1594,9 +1963,7 @@ async def chat_completion(
|
||||
task_ids = []
|
||||
chat_id = metadata['chat_id']
|
||||
|
||||
for idx, entry in enumerate(message_ids):
|
||||
target_model_id = entry['model_id']
|
||||
assistant_message_id = entry['message_id']
|
||||
for idx, (target_model_id, assistant_message_id) in enumerate(message_ids.items()):
|
||||
if not assistant_message_id:
|
||||
continue
|
||||
|
||||
@@ -1616,7 +1983,7 @@ async def chat_completion(
|
||||
# Resolve the model object for this specific model
|
||||
resolved_model = request.app.state.MODELS.get(target_model_id, model)
|
||||
|
||||
# Only the first model runs chat-level background tasks;
|
||||
# Only the first model runs title/tags generation;
|
||||
# subsequent models only run follow-ups.
|
||||
task_id, _ = await create_task(
|
||||
request.app.state.redis,
|
||||
@@ -1637,13 +2004,12 @@ async def chat_completion(
|
||||
),
|
||||
id=chat_id,
|
||||
)
|
||||
per_model_metadata['task_id'] = task_id
|
||||
task_ids.append(task_id)
|
||||
|
||||
# Emit chat:active=true
|
||||
if task_ids:
|
||||
event_emitter = await get_event_emitter(
|
||||
{**metadata, 'message_id': message_ids[0]['message_id']},
|
||||
{**metadata, 'message_id': list(message_ids.values())[0]},
|
||||
update_db=False,
|
||||
)
|
||||
if event_emitter:
|
||||
@@ -1656,7 +2022,7 @@ async def chat_completion(
|
||||
}
|
||||
else:
|
||||
# Legacy/direct: single model, synchronous
|
||||
metadata['message_id'] = message_ids[0]['message_id']
|
||||
metadata['message_id'] = list(message_ids.values())[0]
|
||||
return await process_chat(request, form_data, user, metadata, model, tasks)
|
||||
|
||||
|
||||
@@ -1781,7 +2147,7 @@ async def list_tasks_endpoint(request: Request, user=Depends(get_admin_user)):
|
||||
|
||||
@app.get('/api/tasks/chat/{chat_id:path}')
|
||||
async def list_tasks_by_chat_id_endpoint(request: Request, chat_id: str, user=Depends(get_verified_user)):
|
||||
if chat_id.startswith('local:') or chat_id.startswith('channel:'):
|
||||
if chat_id.startswith('local:'):
|
||||
socket_id = chat_id[len('local:') :]
|
||||
owner_id = get_user_id_from_session_pool(socket_id)
|
||||
if owner_id != user.id and user.role != 'admin':
|
||||
@@ -1799,7 +2165,7 @@ async def list_tasks_by_chat_id_endpoint(request: Request, chat_id: str, user=De
|
||||
|
||||
@app.post('/api/tasks/chat/{chat_id:path}/stop')
|
||||
async def stop_tasks_by_chat_id_endpoint(request: Request, chat_id: str, user=Depends(get_verified_user)):
|
||||
if chat_id.startswith('local:') or chat_id.startswith('channel:'):
|
||||
if chat_id.startswith('local:'):
|
||||
socket_id = chat_id[len('local:') :]
|
||||
owner_id = get_user_id_from_session_pool(socket_id)
|
||||
if owner_id != user.id and user.role != 'admin':
|
||||
@@ -1845,58 +2211,11 @@ async def get_app_config(request: Request):
|
||||
if data is not None and 'id' in data:
|
||||
user = await Users.get_user_by_id(data['id'])
|
||||
|
||||
user_count = await Users.get_num_users()
|
||||
onboarding = False
|
||||
if user is None:
|
||||
onboarding = not await Users.has_users()
|
||||
|
||||
license_metadata = getattr(app.state, 'LICENSE_METADATA', None)
|
||||
user_count = await Users.get_num_users() if license_metadata else None
|
||||
config = await Config.get_many(
|
||||
'oauth.auto_redirect',
|
||||
'ldap.enable',
|
||||
'ui.enable_signup',
|
||||
'ui.enable_login_form',
|
||||
'auth.enable_api_keys',
|
||||
'ui.enable_password_change_form',
|
||||
'direct.enable',
|
||||
'folders.enable',
|
||||
'folders.max_file_count',
|
||||
'channels.enable',
|
||||
'calendar.enable',
|
||||
'automations.enable',
|
||||
'notes.enable',
|
||||
'web.search.enable',
|
||||
'web.search.confirmation.enable',
|
||||
'web.search.confirmation.content',
|
||||
'code_execution.enable',
|
||||
'code_interpreter.enable',
|
||||
'image_generation.enable',
|
||||
'task.autocomplete.enable',
|
||||
'ui.enable_community_sharing',
|
||||
'ui.enable_message_rating',
|
||||
'ui.enable_user_webhooks',
|
||||
'users.enable_status',
|
||||
'google_drive.enable',
|
||||
'onedrive.enable',
|
||||
'memories.enable',
|
||||
'ui.default_models',
|
||||
'ui.default_pinned_models',
|
||||
'ui.prompt_suggestions',
|
||||
'code_execution.engine',
|
||||
'code_interpreter.engine',
|
||||
'audio.tts.engine',
|
||||
'audio.tts.voice',
|
||||
'audio.tts.split_on',
|
||||
'audio.stt.engine',
|
||||
'rag.file.max_size',
|
||||
'rag.file.max_count',
|
||||
'file.image_compression_width',
|
||||
'file.image_compression_height',
|
||||
'user.permissions',
|
||||
'ui.pending_user_overlay_title',
|
||||
'ui.pending_user_overlay_content',
|
||||
'ui.watermark',
|
||||
)
|
||||
if user is None:
|
||||
onboarding = user_count == 0
|
||||
|
||||
return {
|
||||
**({'onboarding': True} if onboarding else {}),
|
||||
@@ -1904,58 +2223,50 @@ async def get_app_config(request: Request):
|
||||
'name': app.state.WEBUI_NAME,
|
||||
'version': VERSION,
|
||||
'default_locale': str(DEFAULT_LOCALE),
|
||||
'oauth': {
|
||||
'providers': {name: config.get('name', name) for name, config in OAUTH_PROVIDERS.items()},
|
||||
'auto_redirect': config.get('oauth.auto_redirect'),
|
||||
},
|
||||
'oauth': {'providers': {name: config.get('name', name) for name, config in OAUTH_PROVIDERS.items()}},
|
||||
'features': {
|
||||
# --- Public: required by login/signup page pre-auth ---
|
||||
'auth': WEBUI_AUTH,
|
||||
'auth_trusted_header': bool(WEBUI_AUTH_TRUSTED_EMAIL_HEADER),
|
||||
'auth_trusted_header': bool(app.state.AUTH_TRUSTED_EMAIL_HEADER),
|
||||
'enable_signup_password_confirmation': ENABLE_SIGNUP_PASSWORD_CONFIRMATION,
|
||||
'enable_ldap': config.get('ldap.enable'),
|
||||
'enable_signup': config.get('ui.enable_signup'),
|
||||
'enable_login_form': config.get('ui.enable_login_form'),
|
||||
'enable_ldap': app.state.config.ENABLE_LDAP,
|
||||
'enable_api_keys': app.state.config.ENABLE_API_KEYS,
|
||||
'enable_signup': app.state.config.ENABLE_SIGNUP,
|
||||
'enable_login_form': app.state.config.ENABLE_LOGIN_FORM,
|
||||
'enable_password_change_form': app.state.config.ENABLE_PASSWORD_CHANGE_FORM,
|
||||
'enable_websocket': ENABLE_WEBSOCKET_SUPPORT,
|
||||
# --- Authenticated: only consumed by logged-in frontend ---
|
||||
'enable_version_update_check': ENABLE_VERSION_UPDATE_CHECK,
|
||||
'enable_public_active_users_count': ENABLE_PUBLIC_ACTIVE_USERS_COUNT,
|
||||
'enable_easter_eggs': ENABLE_EASTER_EGGS,
|
||||
**(
|
||||
{
|
||||
'enable_api_keys': config.get('auth.enable_api_keys'),
|
||||
'enable_password_change_form': config.get('ui.enable_password_change_form'),
|
||||
'enable_version_update_check': ENABLE_VERSION_UPDATE_CHECK,
|
||||
'enable_pyodide_file_persistence': ENABLE_PYODIDE_FILE_PERSISTENCE,
|
||||
'enable_public_active_users_count': ENABLE_PUBLIC_ACTIVE_USERS_COUNT,
|
||||
'enable_easter_eggs': ENABLE_EASTER_EGGS,
|
||||
'enable_direct_connections': config.get('direct.enable'),
|
||||
'enable_folders': config.get('folders.enable'),
|
||||
'folder_max_file_count': config.get('folders.max_file_count'),
|
||||
'enable_channels': config.get('channels.enable'),
|
||||
'enable_calendar': config.get('calendar.enable'),
|
||||
'enable_automations': config.get('automations.enable'),
|
||||
'enable_notes': config.get('notes.enable'),
|
||||
'enable_web_search': config.get('web.search.enable'),
|
||||
'enable_web_search_confirmation': config.get('web.search.confirmation.enable'),
|
||||
'web_search_confirmation_content': config.get('web.search.confirmation.content'),
|
||||
'enable_code_execution': config.get('code_execution.enable'),
|
||||
'enable_code_interpreter': config.get('code_interpreter.enable'),
|
||||
'enable_image_generation': config.get('image_generation.enable'),
|
||||
'enable_autocomplete_generation': config.get('task.autocomplete.enable'),
|
||||
'enable_community_sharing': config.get('ui.enable_community_sharing'),
|
||||
'enable_message_rating': config.get('ui.enable_message_rating'),
|
||||
'enable_user_webhooks': config.get('ui.enable_user_webhooks'),
|
||||
'enable_user_status': config.get('users.enable_status'),
|
||||
'enable_direct_connections': app.state.config.ENABLE_DIRECT_CONNECTIONS,
|
||||
'enable_folders': app.state.config.ENABLE_FOLDERS,
|
||||
'folder_max_file_count': app.state.config.FOLDER_MAX_FILE_COUNT,
|
||||
'enable_channels': app.state.config.ENABLE_CHANNELS,
|
||||
'enable_calendar': app.state.config.ENABLE_CALENDAR,
|
||||
'enable_automations': app.state.config.ENABLE_AUTOMATIONS,
|
||||
'enable_notes': app.state.config.ENABLE_NOTES,
|
||||
'enable_web_search': app.state.config.ENABLE_WEB_SEARCH,
|
||||
'enable_code_execution': app.state.config.ENABLE_CODE_EXECUTION,
|
||||
'enable_code_interpreter': app.state.config.ENABLE_CODE_INTERPRETER,
|
||||
'enable_image_generation': app.state.config.ENABLE_IMAGE_GENERATION,
|
||||
'enable_autocomplete_generation': app.state.config.ENABLE_AUTOCOMPLETE_GENERATION,
|
||||
'enable_community_sharing': app.state.config.ENABLE_COMMUNITY_SHARING,
|
||||
'enable_message_rating': app.state.config.ENABLE_MESSAGE_RATING,
|
||||
'enable_user_webhooks': app.state.config.ENABLE_USER_WEBHOOKS,
|
||||
'enable_user_status': app.state.config.ENABLE_USER_STATUS,
|
||||
'enable_admin_export': ENABLE_ADMIN_EXPORT,
|
||||
'enable_admin_chat_access': ENABLE_ADMIN_CHAT_ACCESS,
|
||||
'enable_admin_analytics': ENABLE_ADMIN_ANALYTICS,
|
||||
'enable_google_drive_integration': config.get('google_drive.enable'),
|
||||
'enable_onedrive_integration': config.get('onedrive.enable'),
|
||||
'enable_memories': config.get('memories.enable'),
|
||||
'enable_google_drive_integration': app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
|
||||
'enable_onedrive_integration': app.state.config.ENABLE_ONEDRIVE_INTEGRATION,
|
||||
'enable_memories': app.state.config.ENABLE_MEMORIES,
|
||||
**(
|
||||
{
|
||||
'enable_onedrive_personal': ENABLE_ONEDRIVE_PERSONAL,
|
||||
'enable_onedrive_business': ENABLE_ONEDRIVE_BUSINESS,
|
||||
}
|
||||
if config.get('onedrive.enable')
|
||||
if app.state.config.ENABLE_ONEDRIVE_INTEGRATION
|
||||
else {}
|
||||
),
|
||||
}
|
||||
@@ -1965,55 +2276,54 @@ async def get_app_config(request: Request):
|
||||
},
|
||||
**(
|
||||
{
|
||||
'default_models': config.get('ui.default_models'),
|
||||
'default_pinned_models': config.get('ui.default_pinned_models'),
|
||||
'default_prompt_suggestions': config.get('ui.prompt_suggestions'),
|
||||
**({'user_count': user_count} if user_count is not None else {}),
|
||||
'default_models': app.state.config.DEFAULT_MODELS,
|
||||
'default_pinned_models': app.state.config.DEFAULT_PINNED_MODELS,
|
||||
'default_prompt_suggestions': app.state.config.DEFAULT_PROMPT_SUGGESTIONS,
|
||||
'user_count': user_count,
|
||||
'code': {
|
||||
'engine': config.get('code_execution.engine'),
|
||||
'interpreter_engine': config.get('code_interpreter.engine'),
|
||||
'engine': app.state.config.CODE_EXECUTION_ENGINE,
|
||||
'interpreter_engine': app.state.config.CODE_INTERPRETER_ENGINE,
|
||||
},
|
||||
'audio': {
|
||||
'tts': {
|
||||
'engine': config.get('audio.tts.engine'),
|
||||
'voice': config.get('audio.tts.voice'),
|
||||
'split_on': config.get('audio.tts.split_on'),
|
||||
'engine': app.state.config.TTS_ENGINE,
|
||||
'voice': app.state.config.TTS_VOICE,
|
||||
'split_on': app.state.config.TTS_SPLIT_ON,
|
||||
},
|
||||
'stt': {
|
||||
'engine': config.get('audio.stt.engine'),
|
||||
'engine': app.state.config.STT_ENGINE,
|
||||
},
|
||||
},
|
||||
'file': {
|
||||
'max_size': config.get('rag.file.max_size'),
|
||||
'max_count': config.get('rag.file.max_count'),
|
||||
'max_size': app.state.config.FILE_MAX_SIZE,
|
||||
'max_count': app.state.config.FILE_MAX_COUNT,
|
||||
'image_compression': {
|
||||
'width': config.get('file.image_compression_width'),
|
||||
'height': config.get('file.image_compression_height'),
|
||||
'width': app.state.config.FILE_IMAGE_COMPRESSION_WIDTH,
|
||||
'height': app.state.config.FILE_IMAGE_COMPRESSION_HEIGHT,
|
||||
},
|
||||
},
|
||||
'permissions': {**(config.get('user.permissions') or {})},
|
||||
'permissions': {**app.state.config.USER_PERMISSIONS},
|
||||
'google_drive': {
|
||||
'client_id': GOOGLE_DRIVE_CLIENT_ID,
|
||||
'api_key': GOOGLE_DRIVE_API_KEY,
|
||||
'client_id': GOOGLE_DRIVE_CLIENT_ID.value,
|
||||
'api_key': GOOGLE_DRIVE_API_KEY.value,
|
||||
},
|
||||
'onedrive': {
|
||||
'client_id_personal': ONEDRIVE_CLIENT_ID_PERSONAL,
|
||||
'client_id_business': ONEDRIVE_CLIENT_ID_BUSINESS,
|
||||
'sharepoint_url': ONEDRIVE_SHAREPOINT_URL,
|
||||
'sharepoint_tenant_id': ONEDRIVE_SHAREPOINT_TENANT_ID,
|
||||
'sharepoint_url': ONEDRIVE_SHAREPOINT_URL.value,
|
||||
'sharepoint_tenant_id': ONEDRIVE_SHAREPOINT_TENANT_ID.value,
|
||||
},
|
||||
'ui': {
|
||||
'pending_user_overlay_title': config.get('ui.pending_user_overlay_title'),
|
||||
'pending_user_overlay_content': config.get('ui.pending_user_overlay_content'),
|
||||
'response_watermark': config.get('ui.watermark'),
|
||||
'iframe_csp': IFRAME_CSP,
|
||||
'pending_user_overlay_title': app.state.config.PENDING_USER_OVERLAY_TITLE,
|
||||
'pending_user_overlay_content': app.state.config.PENDING_USER_OVERLAY_CONTENT,
|
||||
'response_watermark': app.state.config.RESPONSE_WATERMARK,
|
||||
},
|
||||
'license_metadata': license_metadata,
|
||||
'license_metadata': app.state.LICENSE_METADATA,
|
||||
**(
|
||||
{
|
||||
'active_entries': user_count,
|
||||
'active_entries': app.state.USER_COUNT,
|
||||
}
|
||||
if user.role == 'admin' and user_count is not None
|
||||
if user.role == 'admin'
|
||||
else {}
|
||||
),
|
||||
}
|
||||
@@ -2022,8 +2332,8 @@ async def get_app_config(request: Request):
|
||||
**(
|
||||
{
|
||||
'ui': {
|
||||
'pending_user_overlay_title': config.get('ui.pending_user_overlay_title'),
|
||||
'pending_user_overlay_content': config.get('ui.pending_user_overlay_content'),
|
||||
'pending_user_overlay_title': app.state.config.PENDING_USER_OVERLAY_TITLE,
|
||||
'pending_user_overlay_content': app.state.config.PENDING_USER_OVERLAY_CONTENT,
|
||||
}
|
||||
}
|
||||
if user and user.role == 'pending'
|
||||
@@ -2032,11 +2342,11 @@ async def get_app_config(request: Request):
|
||||
**(
|
||||
{
|
||||
'metadata': {
|
||||
'login_footer': license_metadata.get('login_footer', ''),
|
||||
'auth_logo_position': license_metadata.get('auth_logo_position', ''),
|
||||
'login_footer': app.state.LICENSE_METADATA.get('login_footer', ''),
|
||||
'auth_logo_position': app.state.LICENSE_METADATA.get('auth_logo_position', ''),
|
||||
}
|
||||
}
|
||||
if license_metadata
|
||||
if app.state.LICENSE_METADATA
|
||||
else {}
|
||||
),
|
||||
}
|
||||
@@ -2044,115 +2354,22 @@ async def get_app_config(request: Request):
|
||||
}
|
||||
|
||||
|
||||
class EventWebhookForm(BaseModel):
|
||||
name: str | None = None
|
||||
class UrlForm(BaseModel):
|
||||
url: str
|
||||
enabled: bool = True
|
||||
events: list[str] | None = None
|
||||
targets: list[dict[str, str]] | None = None
|
||||
|
||||
|
||||
class EventWebhookUpdateForm(BaseModel):
|
||||
name: str | None = None
|
||||
url: str | None = None
|
||||
enabled: bool | None = None
|
||||
events: list[str] | None = None
|
||||
targets: list[dict[str, str]] | None = None
|
||||
|
||||
|
||||
@app.get('/api/events')
|
||||
async def get_event_catalog(user=Depends(get_admin_user)):
|
||||
@app.get('/api/webhook')
|
||||
async def get_webhook_url(user=Depends(get_admin_user)):
|
||||
return {
|
||||
'schema': VERSION,
|
||||
'events': get_event_catalog_items(),
|
||||
'url': app.state.config.WEBHOOK_URL,
|
||||
}
|
||||
|
||||
|
||||
@app.get('/api/events/webhooks')
|
||||
async def get_event_webhooks_api(user=Depends(get_admin_user)):
|
||||
return await get_event_webhooks()
|
||||
|
||||
|
||||
@app.post('/api/events/webhooks')
|
||||
async def create_event_webhook(form_data: EventWebhookForm, user=Depends(get_admin_user)):
|
||||
try:
|
||||
webhook = await upsert_event_webhook(
|
||||
{
|
||||
'name': form_data.name,
|
||||
'url': form_data.url,
|
||||
'enabled': form_data.enabled,
|
||||
'events': form_data.events,
|
||||
'targets': form_data.targets,
|
||||
}
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
|
||||
await publish_event(
|
||||
app,
|
||||
EVENTS.CONFIG_WEBHOOK_UPDATED,
|
||||
actor=user,
|
||||
subject_id=webhook['id'],
|
||||
subject_type='config',
|
||||
data={
|
||||
'action': 'created',
|
||||
'enabled': webhook.get('enabled'),
|
||||
'events': webhook.get('events'),
|
||||
'targets': webhook.get('targets'),
|
||||
},
|
||||
)
|
||||
return webhook
|
||||
|
||||
|
||||
@app.put('/api/events/webhooks/{webhook_id}')
|
||||
async def update_event_webhook(webhook_id: str, form_data: EventWebhookUpdateForm, user=Depends(get_admin_user)):
|
||||
webhooks = await get_event_webhooks()
|
||||
existing = next((webhook for webhook in webhooks if webhook.get('id') == webhook_id), None)
|
||||
if not existing:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Webhook not found')
|
||||
|
||||
try:
|
||||
webhook = await upsert_event_webhook(
|
||||
{
|
||||
**existing,
|
||||
**form_data.model_dump(exclude_unset=True),
|
||||
'id': webhook_id,
|
||||
}
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
|
||||
await publish_event(
|
||||
app,
|
||||
EVENTS.CONFIG_WEBHOOK_UPDATED,
|
||||
actor=user,
|
||||
subject_id=webhook_id,
|
||||
subject_type='config',
|
||||
data={
|
||||
'action': 'updated',
|
||||
'enabled': webhook.get('enabled'),
|
||||
'events': webhook.get('events'),
|
||||
'targets': webhook.get('targets'),
|
||||
},
|
||||
)
|
||||
return webhook
|
||||
|
||||
|
||||
@app.delete('/api/events/webhooks/{webhook_id}')
|
||||
async def delete_event_webhook_api(webhook_id: str, user=Depends(get_admin_user)):
|
||||
deleted = await delete_event_webhook(webhook_id)
|
||||
if not deleted:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Webhook not found')
|
||||
|
||||
await publish_event(
|
||||
app,
|
||||
EVENTS.CONFIG_WEBHOOK_UPDATED,
|
||||
actor=user,
|
||||
subject_id=webhook_id,
|
||||
subject_type='config',
|
||||
data={'action': 'deleted'},
|
||||
)
|
||||
return {'status': True}
|
||||
@app.post('/api/webhook')
|
||||
async def update_webhook_url(form_data: UrlForm, user=Depends(get_admin_user)):
|
||||
app.state.config.WEBHOOK_URL = form_data.url
|
||||
app.state.WEBHOOK_URL = app.state.config.WEBHOOK_URL
|
||||
return {'url': app.state.config.WEBHOOK_URL}
|
||||
|
||||
|
||||
@app.get('/api/version')
|
||||
@@ -2215,9 +2432,29 @@ async def get_current_usage(user=Depends(get_verified_user)):
|
||||
raise HTTPException(status_code=500, detail='Internal Server Error')
|
||||
|
||||
|
||||
# --- OAuth Login & Callback ---
|
||||
############################
|
||||
# OAuth Login & Callback
|
||||
############################
|
||||
|
||||
|
||||
# Initialize OAuth client manager with any MCP tool servers using OAuth 2.1
|
||||
if len(app.state.config.TOOL_SERVER_CONNECTIONS) > 0:
|
||||
for tool_server_connection in app.state.config.TOOL_SERVER_CONNECTIONS:
|
||||
if tool_server_connection.get('type', 'openapi') == 'mcp':
|
||||
server_id = tool_server_connection.get('info', {}).get('id')
|
||||
auth_type = tool_server_connection.get('auth_type', 'none')
|
||||
|
||||
if server_id and auth_type in ('oauth_2.1', 'oauth_2.1_static'):
|
||||
try:
|
||||
oauth_client_info = resolve_oauth_client_info(tool_server_connection)
|
||||
app.state.oauth_client_manager.add_client(
|
||||
f'mcp:{server_id}',
|
||||
OAuthClientInformationFull(**oauth_client_info),
|
||||
)
|
||||
except Exception as e:
|
||||
log.error(f'Error adding OAuth client for MCP tool server {server_id}: {e}')
|
||||
pass
|
||||
|
||||
try:
|
||||
if ENABLE_STAR_SESSIONS_MIDDLEWARE:
|
||||
redis_session_store = RedisStore(
|
||||
@@ -2252,10 +2489,9 @@ async def register_client(request, client_id: str) -> bool:
|
||||
connection = None
|
||||
connection_idx = None
|
||||
|
||||
tool_server_connections = await Config.get('tool_server.connections', []) or []
|
||||
for idx, conn in enumerate(tool_server_connections):
|
||||
for idx, conn in enumerate(request.app.state.config.TOOL_SERVER_CONNECTIONS or []):
|
||||
if conn.get('type', 'openapi') == server_type:
|
||||
info = conn.get('info') or {}
|
||||
info = conn.get('info', {})
|
||||
if info.get('id') == server_id:
|
||||
connection = conn
|
||||
connection_idx = idx
|
||||
@@ -2267,15 +2503,12 @@ async def register_client(request, client_id: str) -> bool:
|
||||
|
||||
server_url = connection.get('url')
|
||||
auth_type = connection.get('auth_type', 'none')
|
||||
oauth_scope = (connection.get('info') or {}).get('oauth_scope') or (connection.get('config') or {}).get(
|
||||
'oauth_scope'
|
||||
)
|
||||
oauth_server_key = (connection.get('config') or {}).get('oauth_server_key')
|
||||
|
||||
try:
|
||||
if auth_type == 'oauth_2.1_static':
|
||||
# Static credentials: rebuild from admin-provided credentials + fresh metadata
|
||||
info = connection.get('info') or {}
|
||||
info = connection.get('info', {})
|
||||
oauth_client_id = info.get('oauth_client_id') or ''
|
||||
oauth_client_secret = info.get('oauth_client_secret') or ''
|
||||
if not oauth_client_id or not oauth_client_secret:
|
||||
@@ -2293,7 +2526,6 @@ async def register_client(request, client_id: str) -> bool:
|
||||
server_url,
|
||||
oauth_client_id=oauth_client_id,
|
||||
oauth_client_secret=oauth_client_secret,
|
||||
oauth_scope=oauth_scope,
|
||||
)
|
||||
else:
|
||||
oauth_client_info = await get_oauth_client_info_with_dynamic_client_registration(
|
||||
@@ -2301,30 +2533,28 @@ async def register_client(request, client_id: str) -> bool:
|
||||
client_id,
|
||||
server_url,
|
||||
oauth_server_key,
|
||||
oauth_scope=oauth_scope,
|
||||
)
|
||||
except Exception as e:
|
||||
log.error(f'OAuth client re-registration failed for {client_id}: {e}')
|
||||
return False
|
||||
|
||||
try:
|
||||
connections = await Config.get('tool_server.connections', []) or []
|
||||
connections = request.app.state.config.TOOL_SERVER_CONNECTIONS
|
||||
connections[connection_idx] = {
|
||||
**connection,
|
||||
'info': {
|
||||
**(connection.get('info') or {}),
|
||||
**connection.get('info', {}),
|
||||
'oauth_client_info': encrypt_data(oauth_client_info.model_dump(mode='json')),
|
||||
},
|
||||
}
|
||||
await Config.upsert({'tool_server.connections': connections})
|
||||
# Re-assign the full list to trigger AppConfig.__setattr__ → PersistentConfig.save()
|
||||
# (in-place list mutation via list[idx] = ... does not trigger __setattr__)
|
||||
request.app.state.config.TOOL_SERVER_CONNECTIONS = connections
|
||||
except Exception as e:
|
||||
log.error(f'Failed to persist updated OAuth client info for tool server {client_id}: {e}')
|
||||
return False
|
||||
|
||||
oauth_client_manager.remove_client(client_id)
|
||||
oauth_client_info = OAuthClientInformationFull(
|
||||
**apply_connection_oauth_options(connection, oauth_client_info.model_dump(mode='json'))
|
||||
)
|
||||
oauth_client_manager.add_client(client_id, oauth_client_info)
|
||||
log.info(f'Re-registered OAuth client {client_id} for tool server')
|
||||
return True
|
||||
@@ -2338,8 +2568,8 @@ async def oauth_client_authorize(
|
||||
user=Depends(get_verified_user),
|
||||
):
|
||||
# ensure_valid_client_registration
|
||||
client = await oauth_client_manager.get_client(client_id)
|
||||
client_info = await oauth_client_manager.get_client_info(client_id)
|
||||
client = oauth_client_manager.get_client(client_id)
|
||||
client_info = oauth_client_manager.get_client_info(client_id)
|
||||
if client is None or client_info is None:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND)
|
||||
|
||||
@@ -2356,8 +2586,8 @@ async def oauth_client_authorize(
|
||||
detail='Failed to re-register OAuth client',
|
||||
)
|
||||
|
||||
client = await oauth_client_manager.get_client(client_id)
|
||||
client_info = await oauth_client_manager.get_client_info(client_id)
|
||||
client = oauth_client_manager.get_client(client_id)
|
||||
client_info = oauth_client_manager.get_client_info(client_id)
|
||||
if client is None or client_info is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
@@ -2393,6 +2623,12 @@ async def oauth_login(provider: str, request: Request):
|
||||
return await oauth_manager.handle_login(request, provider)
|
||||
|
||||
|
||||
# OAuth login logic is as follows:
|
||||
# 1. Attempt to find a user with matching subject ID, tied to the provider
|
||||
# 2. If OAUTH_MERGE_ACCOUNTS_BY_EMAIL is true, find a user with the email address provided via OAuth
|
||||
# - This is considered insecure in general, as OAuth providers do not always verify email addresses
|
||||
# 3. If there is no user, and ENABLE_OAUTH_SIGNUP is true, create a user
|
||||
# - Email addresses are considered unique, so we fail registration if the email address is already taken
|
||||
@app.get('/oauth/{provider}/login/callback')
|
||||
@app.get('/oauth/{provider}/callback') # Legacy endpoint
|
||||
async def oauth_login_callback(
|
||||
@@ -2401,15 +2637,6 @@ async def oauth_login_callback(
|
||||
response: Response,
|
||||
db: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
"""Handle the OAuth provider callback.
|
||||
|
||||
Resolution order:
|
||||
1. Match by subject ID bound to the provider.
|
||||
2. If ``OAUTH_MERGE_ACCOUNTS_BY_EMAIL`` is enabled, match by email
|
||||
(note: some providers do not verify email addresses).
|
||||
3. If no match and ``ENABLE_OAUTH_SIGNUP`` is enabled, create a new user
|
||||
(fails if the email is already registered).
|
||||
"""
|
||||
return await oauth_manager.handle_callback(request, provider, response, db=db)
|
||||
|
||||
|
||||
@@ -2430,11 +2657,10 @@ async def oauth_backchannel_logout(
|
||||
|
||||
@app.get('/manifest.json')
|
||||
async def get_manifest_json():
|
||||
external_pwa_manifest_url = getattr(app.state, 'EXTERNAL_PWA_MANIFEST_URL', None)
|
||||
if external_pwa_manifest_url:
|
||||
if app.state.EXTERNAL_PWA_MANIFEST_URL:
|
||||
session = await get_session()
|
||||
async with session.get(
|
||||
external_pwa_manifest_url,
|
||||
app.state.EXTERNAL_PWA_MANIFEST_URL,
|
||||
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||
) as r:
|
||||
r.raise_for_status()
|
||||
@@ -2471,44 +2697,19 @@ async def get_manifest_json():
|
||||
|
||||
@app.get('/opensearch.xml')
|
||||
async def get_opensearch_xml():
|
||||
webui_url = await Config.get('webui.url')
|
||||
xml_content = rf"""
|
||||
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">
|
||||
<ShortName>{app.state.WEBUI_NAME}</ShortName>
|
||||
<Description>Search {app.state.WEBUI_NAME}</Description>
|
||||
<InputEncoding>UTF-8</InputEncoding>
|
||||
<Image width="16" height="16" type="image/x-icon">{webui_url}/static/favicon.png</Image>
|
||||
<Url type="text/html" method="get" template="{webui_url}/?q={'{searchTerms}'}"/>
|
||||
<moz:SearchForm>{webui_url}</moz:SearchForm>
|
||||
<Image width="16" height="16" type="image/x-icon">{app.state.config.WEBUI_URL}/static/favicon.png</Image>
|
||||
<Url type="text/html" method="get" template="{app.state.config.WEBUI_URL}/?q={'{searchTerms}'}"/>
|
||||
<moz:SearchForm>{app.state.config.WEBUI_URL}</moz:SearchForm>
|
||||
</OpenSearchDescription>
|
||||
"""
|
||||
return Response(content=xml_content, media_type='application/xml')
|
||||
|
||||
|
||||
def _sync_db_ping() -> None:
|
||||
"""Verify the database is reachable with a simple SELECT 1.
|
||||
|
||||
Uses a raw connection from the engine pool instead of the thread-local
|
||||
ScopedSession. This is necessary because CommitSessionMiddleware
|
||||
deliberately skips healthcheck paths (/health, /ready, /health/db),
|
||||
so any ScopedSession opened on a healthcheck worker thread is never
|
||||
rolled back or removed. If the session ever enters an invalid state
|
||||
(e.g. after a transient connection error), it stays broken on that
|
||||
thread permanently, causing PendingRollbackError on every subsequent
|
||||
probe — exactly the failure reported in #24605.
|
||||
|
||||
A raw ``engine.connect()`` context manager obtains a fresh connection
|
||||
from the pool, executes the ping, and deterministically returns the
|
||||
connection regardless of success or failure.
|
||||
"""
|
||||
with engine.connect() as conn:
|
||||
conn.execute(text('SELECT 1'))
|
||||
|
||||
|
||||
async def async_db_ping() -> None:
|
||||
await asyncio.to_thread(_sync_db_ping)
|
||||
|
||||
|
||||
@app.get('/health')
|
||||
async def healthcheck():
|
||||
return {'status': True}
|
||||
@@ -2530,7 +2731,7 @@ async def readiness_check():
|
||||
|
||||
# Check database connectivity
|
||||
try:
|
||||
await async_db_ping()
|
||||
ScopedSession.execute(text('SELECT 1;')).all()
|
||||
except Exception as e:
|
||||
log.warning(f'Readiness check DB ping failed: {e!r}')
|
||||
raise HTTPException(
|
||||
@@ -2556,14 +2757,11 @@ async def readiness_check():
|
||||
|
||||
|
||||
@app.get('/health/db')
|
||||
async def check_db_health():
|
||||
"""Verify database connectivity by issuing a lightweight ping."""
|
||||
await async_db_ping()
|
||||
async def healthcheck_with_db():
|
||||
ScopedSession.execute(text('SELECT 1;')).all()
|
||||
return {'status': True}
|
||||
|
||||
|
||||
# --- static assets & files ---
|
||||
# Serve build-time static assets (CSS, JS, images, favicon, etc.)
|
||||
app.mount('/static', StaticFiles(directory=STATIC_DIR), name='static')
|
||||
|
||||
|
||||
@@ -2572,27 +2770,13 @@ async def serve_cache_file(
|
||||
path: str,
|
||||
user=Depends(get_verified_user),
|
||||
):
|
||||
"""Serve cached files (e.g. tool outputs) with path-traversal protection.
|
||||
|
||||
Only ``image/*``, ``audio/*``, and ``video/*`` MIME types are served inline;
|
||||
everything else gets a ``Content-Disposition: attachment`` header to prevent
|
||||
XSS from user-generated HTML stored in the cache directory.
|
||||
"""
|
||||
file_path = os.path.abspath(os.path.join(CACHE_DIR, path))
|
||||
# trailing os.sep is required: without it, a path resolving to a sibling
|
||||
# whose name starts with the cache-dir basename (e.g. cache_backup) passes
|
||||
cache_root = os.path.abspath(CACHE_DIR) + os.sep
|
||||
if not file_path.startswith(cache_root):
|
||||
# prevent path traversal
|
||||
if not file_path.startswith(os.path.abspath(CACHE_DIR)):
|
||||
raise HTTPException(status_code=404, detail='File not found')
|
||||
if not os.path.isfile(file_path):
|
||||
raise HTTPException(status_code=404, detail='File not found')
|
||||
|
||||
mime, _ = mimetypes.guess_type(file_path)
|
||||
inline_safe = mime and mime.split('/', 1)[0] in {'image', 'audio', 'video'}
|
||||
headers = {'X-Content-Type-Options': 'nosniff'}
|
||||
if not inline_safe:
|
||||
headers['Content-Disposition'] = f'attachment; filename="{os.path.basename(file_path)}"'
|
||||
return FileResponse(file_path, headers=headers)
|
||||
return FileResponse(file_path)
|
||||
|
||||
|
||||
def swagger_ui_html(*args, **kwargs):
|
||||
@@ -2609,10 +2793,6 @@ applications.get_swagger_ui_html = swagger_ui_html
|
||||
|
||||
if os.path.exists(FRONTEND_BUILD_DIR):
|
||||
mimetypes.add_type('text/javascript', '.js')
|
||||
pyodide_dir = FRONTEND_BUILD_DIR / 'pyodide'
|
||||
if os.path.exists(pyodide_dir):
|
||||
app.mount('/pyodide', CORSStaticFiles(directory=pyodide_dir), name='pyodide')
|
||||
|
||||
app.mount(
|
||||
'/',
|
||||
SPAStaticFiles(directory=FRONTEND_BUILD_DIR, html=True),
|
||||
|
||||
@@ -1,85 +1,120 @@
|
||||
from __future__ import annotations
|
||||
|
||||
# Alembic environment configuration runner.
|
||||
# Coordinates database migrations in both offline and online execution modes.
|
||||
import logging.config
|
||||
import logging
|
||||
import alembic.context
|
||||
from open_webui.env import DATABASE_PASSWORD, DATABASE_URL, LOG_FORMAT
|
||||
from open_webui.internal.db import enable_iam_token_auth, extract_ssl_params_from_url, reattach_ssl_params_to_url
|
||||
from logging.config import fileConfig
|
||||
|
||||
from alembic import context
|
||||
from open_webui.models.auths import Auth
|
||||
from open_webui.models.calendar import Calendar, CalendarEvent, CalendarEventAttendee # noqa: F401
|
||||
from sqlalchemy import create_engine, engine_from_config, pool
|
||||
from open_webui.env import DATABASE_URL, DATABASE_PASSWORD, LOG_FORMAT
|
||||
from open_webui.internal.db import extract_ssl_params_from_url, reattach_ssl_params_to_url
|
||||
from sqlalchemy import engine_from_config, pool, create_engine
|
||||
|
||||
alembic_config = alembic.context.config
|
||||
if alembic_config.config_file_name:
|
||||
logging.config.fileConfig(alembic_config.config_file_name, disable_existing_loggers=False)
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name, disable_existing_loggers=False)
|
||||
|
||||
# Re-apply JSON formatter after fileConfig replaces handlers.
|
||||
if LOG_FORMAT == 'json':
|
||||
from open_webui.env import JSONFormatter
|
||||
|
||||
for log_handler in logging.root.handlers:
|
||||
log_handler.setFormatter(JSONFormatter())
|
||||
migration_metadata = Auth.metadata
|
||||
target_db_url = DATABASE_URL
|
||||
base_url, ssl_query_params = extract_ssl_params_from_url(target_db_url)
|
||||
if ssl_query_params:
|
||||
target_db_url = reattach_ssl_params_to_url(base_url, ssl_query_params)
|
||||
if target_db_url:
|
||||
alembic_config.set_main_option('sqlalchemy.url', target_db_url.replace('%', '%%'))
|
||||
for handler in logging.root.handlers:
|
||||
handler.setFormatter(JSONFormatter())
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
target_metadata = Auth.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
DB_URL = DATABASE_URL
|
||||
|
||||
# Normalize SSL query params for psycopg2 (Alembic uses psycopg2 for sync migrations).
|
||||
url_without_ssl, ssl_params = extract_ssl_params_from_url(DB_URL)
|
||||
DB_URL = reattach_ssl_params_to_url(url_without_ssl, ssl_params) if ssl_params else DB_URL
|
||||
|
||||
if DB_URL:
|
||||
config.set_main_option('sqlalchemy.url', DB_URL.replace('%', '%%'))
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
"""Execute Alembic migrations in offline mode (outputs raw SQL DDL)."""
|
||||
db_connection_url = alembic_config.get_main_option('sqlalchemy.url')
|
||||
alembic.context.configure(
|
||||
url=db_connection_url,
|
||||
target_metadata=migration_metadata,
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option('sqlalchemy.url')
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={'paramstyle': 'named'},
|
||||
)
|
||||
with alembic.context.begin_transaction():
|
||||
alembic.context.run_migrations()
|
||||
|
||||
|
||||
def _get_engine_connectable():
|
||||
"""Build the database engine based on target URL and authentication credentials."""
|
||||
if target_db_url and target_db_url.startswith('sqlite+sqlcipher://'):
|
||||
if not DATABASE_PASSWORD or not DATABASE_PASSWORD.strip():
|
||||
raise ValueError('DATABASE_PASSWORD is required when using sqlite+sqlcipher:// URLs')
|
||||
raw_db_path = target_db_url.replace('sqlite+sqlcipher://', '')
|
||||
if raw_db_path.startswith('/'):
|
||||
raw_db_path = raw_db_path[1:]
|
||||
|
||||
def _sqlite_cipher_creator():
|
||||
import sqlcipher3
|
||||
|
||||
cipher_conn = sqlcipher3.connect(raw_db_path, check_same_thread=False)
|
||||
cipher_conn.execute(f"PRAGMA key = '{DATABASE_PASSWORD}'")
|
||||
return cipher_conn
|
||||
|
||||
return create_engine('sqlite://', creator=_sqlite_cipher_creator, echo=False)
|
||||
return engine_from_config(
|
||||
alembic_config.get_section(alembic_config.config_ini_section, {}),
|
||||
prefix='sqlalchemy.',
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
"""Execute migrations against a live database connection."""
|
||||
live_connectable = _get_engine_connectable()
|
||||
enable_iam_token_auth(live_connectable)
|
||||
with live_connectable.connect() as live_connection:
|
||||
alembic.context.configure(
|
||||
connection=live_connection,
|
||||
target_metadata=migration_metadata,
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
# Handle SQLCipher URLs
|
||||
if DB_URL and DB_URL.startswith('sqlite+sqlcipher://'):
|
||||
if not DATABASE_PASSWORD or DATABASE_PASSWORD.strip() == '':
|
||||
raise ValueError('DATABASE_PASSWORD is required when using sqlite+sqlcipher:// URLs')
|
||||
|
||||
# Extract database path from SQLCipher URL
|
||||
db_path = DB_URL.replace('sqlite+sqlcipher://', '')
|
||||
if db_path.startswith('/'):
|
||||
db_path = db_path[1:] # Remove leading slash for relative paths
|
||||
|
||||
# Create a custom creator function that uses sqlcipher3
|
||||
def create_sqlcipher_connection():
|
||||
import sqlcipher3
|
||||
|
||||
conn = sqlcipher3.connect(db_path, check_same_thread=False)
|
||||
conn.execute(f"PRAGMA key = '{DATABASE_PASSWORD}'")
|
||||
return conn
|
||||
|
||||
connectable = create_engine(
|
||||
'sqlite://', # Dummy URL since we're using creator
|
||||
creator=create_sqlcipher_connection,
|
||||
echo=False,
|
||||
)
|
||||
with alembic.context.begin_transaction():
|
||||
alembic.context.run_migrations()
|
||||
else:
|
||||
# Standard database connection (existing logic)
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix='sqlalchemy.',
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(connection=connection, target_metadata=target_metadata)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
# Alembic execution entrypoint branch
|
||||
if alembic.context.is_offline_mode():
|
||||
run_migrations_offline() # run in offline mode
|
||||
if not alembic.context.is_offline_mode():
|
||||
run_migrations_online() # run in online mode
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
"""Alembic migration utilities."""
|
||||
|
||||
from alembic import op # noqa: E402 — alembic runtime context
|
||||
from sqlalchemy import inspect # metadata inspection
|
||||
from alembic import op
|
||||
from sqlalchemy import Inspector
|
||||
|
||||
|
||||
# --- database helper functions ---
|
||||
def get_existing_tables() -> set[str]:
|
||||
"""Return table names already present in the database."""
|
||||
conn = op.get_bind()
|
||||
return set(inspect(conn).get_table_names())
|
||||
def get_existing_tables():
|
||||
con = op.get_bind()
|
||||
inspector = Inspector.from_engine(con)
|
||||
tables = set(inspector.get_table_names())
|
||||
return tables
|
||||
|
||||
|
||||
def get_revision_id() -> str:
|
||||
"""Generate a short random revision identifier."""
|
||||
def get_revision_id():
|
||||
import uuid
|
||||
|
||||
return uuid.uuid4().hex[:12]
|
||||
return str(uuid.uuid4()).replace('-', '')[:12]
|
||||
|
||||
@@ -6,8 +6,8 @@ Create Date: 2025-08-13 03:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = '018012973d35'
|
||||
down_revision = 'd31026856c01'
|
||||
@@ -16,34 +16,18 @@ depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
|
||||
def _idx_exists(table, idx_name):
|
||||
return any(i['name'] == idx_name for i in inspector.get_indexes(table))
|
||||
|
||||
# Chat table indexes
|
||||
if not _idx_exists('chat', 'folder_id_idx'):
|
||||
op.create_index('folder_id_idx', 'chat', ['folder_id'])
|
||||
if not _idx_exists('chat', 'user_id_pinned_idx'):
|
||||
op.create_index('user_id_pinned_idx', 'chat', ['user_id', 'pinned'])
|
||||
if not _idx_exists('chat', 'user_id_archived_idx'):
|
||||
op.create_index('user_id_archived_idx', 'chat', ['user_id', 'archived'])
|
||||
if not _idx_exists('chat', 'updated_at_user_id_idx'):
|
||||
op.create_index('updated_at_user_id_idx', 'chat', ['updated_at', 'user_id'])
|
||||
if not _idx_exists('chat', 'folder_id_user_id_idx'):
|
||||
op.create_index('folder_id_user_id_idx', 'chat', ['folder_id', 'user_id'])
|
||||
op.create_index('folder_id_idx', 'chat', ['folder_id'])
|
||||
op.create_index('user_id_pinned_idx', 'chat', ['user_id', 'pinned'])
|
||||
op.create_index('user_id_archived_idx', 'chat', ['user_id', 'archived'])
|
||||
op.create_index('updated_at_user_id_idx', 'chat', ['updated_at', 'user_id'])
|
||||
op.create_index('folder_id_user_id_idx', 'chat', ['folder_id', 'user_id'])
|
||||
|
||||
# Tag table index
|
||||
if not _idx_exists('tag', 'user_id_idx'):
|
||||
op.create_index('user_id_idx', 'tag', ['user_id'])
|
||||
op.create_index('user_id_idx', 'tag', ['user_id'])
|
||||
|
||||
# Function table index (only if is_global column exists — added by a later migration)
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
func_cols = {c['name'] for c in inspector.get_columns('function')}
|
||||
if 'is_global' in func_cols and not _idx_exists('function', 'is_global_idx'):
|
||||
op.create_index('is_global_idx', 'function', ['is_global'])
|
||||
# Function table index
|
||||
op.create_index('is_global_idx', 'function', ['is_global'])
|
||||
|
||||
|
||||
def downgrade():
|
||||
|
||||
@@ -6,12 +6,12 @@ Create Date: 2024-10-09 21:02:35.241684
|
||||
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.sql import table, select, update, column
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
from sqlalchemy.sql import column, select, table, update
|
||||
|
||||
import json
|
||||
|
||||
revision = '1af9b942657b'
|
||||
down_revision = '242a2047eae0'
|
||||
@@ -93,11 +93,8 @@ def upgrade():
|
||||
conn.execute(update_stmt)
|
||||
|
||||
# Add columns `pinned` and `meta` to 'chat'
|
||||
chat_columns = {c['name'] for c in inspector.get_columns('chat')}
|
||||
if 'pinned' not in chat_columns:
|
||||
op.add_column('chat', sa.Column('pinned', sa.Boolean(), nullable=True))
|
||||
if 'meta' not in chat_columns:
|
||||
op.add_column('chat', sa.Column('meta', sa.JSON(), nullable=False, server_default='{}'))
|
||||
op.add_column('chat', sa.Column('pinned', sa.Boolean(), nullable=True))
|
||||
op.add_column('chat', sa.Column('meta', sa.JSON(), nullable=False, server_default='{}'))
|
||||
|
||||
chatidtag = table('chatidtag', column('chat_id', sa.String()), column('tag_name', sa.String()))
|
||||
chat = table(
|
||||
|
||||
@@ -6,11 +6,11 @@ Create Date: 2024-10-09 21:02:35.241684
|
||||
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.sql import select, table, update
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.sql import table, select, update
|
||||
|
||||
import json
|
||||
|
||||
revision = '242a2047eae0'
|
||||
down_revision = '6a39f3d8e55c'
|
||||
@@ -47,41 +47,34 @@ def upgrade():
|
||||
# If the column is already JSON, no need to do anything
|
||||
pass
|
||||
|
||||
# Step 3: Migrate data from 'old_chat' to 'chat' (only if old_chat exists)
|
||||
# Re-check columns after potential rename above
|
||||
current_cols = {c['name'] for c in sa.inspect(conn).get_columns('chat')}
|
||||
if 'old_chat' in current_cols:
|
||||
chat_table = table(
|
||||
'chat',
|
||||
sa.Column('id', sa.String(), primary_key=True),
|
||||
sa.Column('old_chat', sa.Text()),
|
||||
sa.Column('chat', sa.JSON()),
|
||||
)
|
||||
# Step 3: Migrate data from 'old_chat' to 'chat'
|
||||
chat_table = table(
|
||||
'chat',
|
||||
sa.Column('id', sa.String(), primary_key=True),
|
||||
sa.Column('old_chat', sa.Text()),
|
||||
sa.Column('chat', sa.JSON()),
|
||||
)
|
||||
|
||||
# - Selecting all data from the table
|
||||
connection = op.get_bind()
|
||||
results = connection.execute(select(chat_table.c.id, chat_table.c.old_chat))
|
||||
for row in results:
|
||||
try:
|
||||
# Convert text JSON to actual JSON object, assuming the text is in JSON format
|
||||
json_data = json.loads(row.old_chat)
|
||||
except json.JSONDecodeError:
|
||||
json_data = None # Handle cases where the text cannot be converted to JSON
|
||||
# - Selecting all data from the table
|
||||
connection = op.get_bind()
|
||||
results = connection.execute(select(chat_table.c.id, chat_table.c.old_chat))
|
||||
for row in results:
|
||||
try:
|
||||
# Convert text JSON to actual JSON object, assuming the text is in JSON format
|
||||
json_data = json.loads(row.old_chat)
|
||||
except json.JSONDecodeError:
|
||||
json_data = None # Handle cases where the text cannot be converted to JSON
|
||||
|
||||
connection.execute(sa.update(chat_table).where(chat_table.c.id == row.id).values(chat=json_data))
|
||||
connection.execute(sa.update(chat_table).where(chat_table.c.id == row.id).values(chat=json_data))
|
||||
|
||||
# Step 4: Drop 'old_chat' column
|
||||
print("Dropping 'old_chat' column")
|
||||
op.drop_column('chat', 'old_chat')
|
||||
# Step 4: Drop 'old_chat' column
|
||||
print("Dropping 'old_chat' column")
|
||||
op.drop_column('chat', 'old_chat')
|
||||
|
||||
|
||||
def downgrade():
|
||||
conn = op.get_bind()
|
||||
columns = {col['name'] for col in sa.inspect(conn).get_columns('chat')}
|
||||
|
||||
# Step 1: Add 'old_chat' column back as Text
|
||||
if 'old_chat' not in columns:
|
||||
op.add_column('chat', sa.Column('old_chat', sa.Text(), nullable=True))
|
||||
op.add_column('chat', sa.Column('old_chat', sa.Text(), nullable=True))
|
||||
|
||||
# Step 2: Convert 'chat' JSON data back to text and store in 'old_chat'
|
||||
chat_table = table(
|
||||
@@ -91,14 +84,14 @@ def downgrade():
|
||||
sa.Column('old_chat', sa.Text()),
|
||||
)
|
||||
|
||||
if 'chat' in columns:
|
||||
results = conn.execute(select(chat_table.c.id, chat_table.c.chat))
|
||||
for row in results:
|
||||
text_data = json.dumps(row.chat) if row.chat is not None else None
|
||||
conn.execute(sa.update(chat_table).where(chat_table.c.id == row.id).values(old_chat=text_data))
|
||||
connection = op.get_bind()
|
||||
results = connection.execute(select(chat_table.c.id, chat_table.c.chat))
|
||||
for row in results:
|
||||
text_data = json.dumps(row.chat) if row.chat is not None else None
|
||||
connection.execute(sa.update(chat_table).where(chat_table.c.id == row.id).values(old_chat=text_data))
|
||||
|
||||
# Step 3: Remove the new 'chat' JSON column
|
||||
op.drop_column('chat', 'chat')
|
||||
# Step 3: Remove the new 'chat' JSON column
|
||||
op.drop_column('chat', 'chat')
|
||||
|
||||
# Step 4: Rename 'old_chat' back to 'chat'
|
||||
op.alter_column('chat', 'old_chat', new_column_name='chat', existing_type=sa.Text())
|
||||
|
||||
+56
-69
@@ -8,9 +8,9 @@ Create Date: 2025-11-27 03:07:56.200231
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import open_webui.internal.db
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import open_webui.internal.db
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '2f1211949ecc'
|
||||
@@ -20,76 +20,63 @@ depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
|
||||
# New columns to be added to channel_member table
|
||||
cm_cols = {c['name'] for c in inspector.get_columns('channel_member')}
|
||||
if 'status' not in cm_cols:
|
||||
op.add_column('channel_member', sa.Column('status', sa.Text(), nullable=True))
|
||||
if 'is_active' not in cm_cols:
|
||||
op.add_column(
|
||||
'channel_member',
|
||||
sa.Column(
|
||||
'is_active',
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
default=True,
|
||||
server_default=sa.sql.expression.true(),
|
||||
),
|
||||
)
|
||||
if 'is_channel_muted' not in cm_cols:
|
||||
op.add_column(
|
||||
'channel_member',
|
||||
sa.Column(
|
||||
'is_channel_muted',
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
default=False,
|
||||
server_default=sa.sql.expression.false(),
|
||||
),
|
||||
)
|
||||
if 'is_channel_pinned' not in cm_cols:
|
||||
op.add_column(
|
||||
'channel_member',
|
||||
sa.Column(
|
||||
'is_channel_pinned',
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
default=False,
|
||||
server_default=sa.sql.expression.false(),
|
||||
),
|
||||
)
|
||||
if 'data' not in cm_cols:
|
||||
op.add_column('channel_member', sa.Column('data', sa.JSON(), nullable=True))
|
||||
if 'meta' not in cm_cols:
|
||||
op.add_column('channel_member', sa.Column('meta', sa.JSON(), nullable=True))
|
||||
if 'joined_at' not in cm_cols:
|
||||
op.add_column('channel_member', sa.Column('joined_at', sa.BigInteger(), nullable=False))
|
||||
if 'left_at' not in cm_cols:
|
||||
op.add_column('channel_member', sa.Column('left_at', sa.BigInteger(), nullable=True))
|
||||
if 'last_read_at' not in cm_cols:
|
||||
op.add_column('channel_member', sa.Column('last_read_at', sa.BigInteger(), nullable=True))
|
||||
if 'updated_at' not in cm_cols:
|
||||
op.add_column('channel_member', sa.Column('updated_at', sa.BigInteger(), nullable=True))
|
||||
op.add_column('channel_member', sa.Column('status', sa.Text(), nullable=True))
|
||||
op.add_column(
|
||||
'channel_member',
|
||||
sa.Column(
|
||||
'is_active',
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
default=True,
|
||||
server_default=sa.sql.expression.true(),
|
||||
),
|
||||
)
|
||||
|
||||
op.add_column(
|
||||
'channel_member',
|
||||
sa.Column(
|
||||
'is_channel_muted',
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
default=False,
|
||||
server_default=sa.sql.expression.false(),
|
||||
),
|
||||
)
|
||||
op.add_column(
|
||||
'channel_member',
|
||||
sa.Column(
|
||||
'is_channel_pinned',
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
default=False,
|
||||
server_default=sa.sql.expression.false(),
|
||||
),
|
||||
)
|
||||
|
||||
op.add_column('channel_member', sa.Column('data', sa.JSON(), nullable=True))
|
||||
op.add_column('channel_member', sa.Column('meta', sa.JSON(), nullable=True))
|
||||
|
||||
op.add_column('channel_member', sa.Column('joined_at', sa.BigInteger(), nullable=False))
|
||||
op.add_column('channel_member', sa.Column('left_at', sa.BigInteger(), nullable=True))
|
||||
|
||||
op.add_column('channel_member', sa.Column('last_read_at', sa.BigInteger(), nullable=True))
|
||||
|
||||
op.add_column('channel_member', sa.Column('updated_at', sa.BigInteger(), nullable=True))
|
||||
|
||||
# New columns to be added to message table
|
||||
msg_cols = {c['name'] for c in inspector.get_columns('message')}
|
||||
if 'is_pinned' not in msg_cols:
|
||||
op.add_column(
|
||||
'message',
|
||||
sa.Column(
|
||||
'is_pinned',
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
default=False,
|
||||
server_default=sa.sql.expression.false(),
|
||||
),
|
||||
)
|
||||
if 'pinned_at' not in msg_cols:
|
||||
op.add_column('message', sa.Column('pinned_at', sa.BigInteger(), nullable=True))
|
||||
if 'pinned_by' not in msg_cols:
|
||||
op.add_column('message', sa.Column('pinned_by', sa.Text(), nullable=True))
|
||||
op.add_column(
|
||||
'message',
|
||||
sa.Column(
|
||||
'is_pinned',
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
default=False,
|
||||
server_default=sa.sql.expression.false(),
|
||||
),
|
||||
)
|
||||
op.add_column('message', sa.Column('pinned_at', sa.BigInteger(), nullable=True))
|
||||
op.add_column('message', sa.Column('pinned_by', sa.Text(), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
@@ -6,11 +6,11 @@ Create Date: 2026-01-23 17:15:00.000000
|
||||
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from typing import Sequence, Union
|
||||
import uuid
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision: str = '374d2f66af06'
|
||||
down_revision: Union[str, None] = 'c440947495f3'
|
||||
@@ -20,167 +20,150 @@ depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
existing_tables = set(inspector.get_table_names())
|
||||
|
||||
# If the final state already exists (prompt has 'id' PK + prompt_history exists),
|
||||
# the migration completed successfully on a prior run — nothing to do.
|
||||
if 'prompt_history' in existing_tables and 'prompt_new' not in existing_tables:
|
||||
# prompt_history exists and prompt_new was already renamed → done
|
||||
prompt_cols = {c['name'] for c in inspector.get_columns('prompt')}
|
||||
if 'id' in prompt_cols and 'version_id' in prompt_cols:
|
||||
return
|
||||
# Step 1: Read existing data from OLD table (schema likely command as PK)
|
||||
# We use batch_alter previously, but we want to move to new table.
|
||||
# We need to assume the OLD structure.
|
||||
|
||||
# Step 1: Read existing data from OLD table (schema: command as PK)
|
||||
# Only read if the old-schema prompt table still exists (has 'command' but no 'version_id')
|
||||
existing_prompts = []
|
||||
if 'prompt' in existing_tables and 'prompt_new' not in existing_tables:
|
||||
prompt_cols = {c['name'] for c in inspector.get_columns('prompt')}
|
||||
if 'command' in prompt_cols and 'version_id' not in prompt_cols:
|
||||
old_prompt_table = sa.table(
|
||||
'prompt',
|
||||
sa.column('command', sa.Text()),
|
||||
sa.column('user_id', sa.Text()),
|
||||
sa.column('title', sa.Text()),
|
||||
sa.column('content', sa.Text()),
|
||||
sa.column('timestamp', sa.BigInteger()),
|
||||
sa.column('access_control', sa.JSON()),
|
||||
old_prompt_table = sa.table(
|
||||
'prompt',
|
||||
sa.column('command', sa.Text()),
|
||||
sa.column('user_id', sa.Text()),
|
||||
sa.column('title', sa.Text()),
|
||||
sa.column('content', sa.Text()),
|
||||
sa.column('timestamp', sa.BigInteger()),
|
||||
sa.column('access_control', sa.JSON()),
|
||||
)
|
||||
|
||||
# Check if table exists/read data
|
||||
try:
|
||||
existing_prompts = conn.execute(
|
||||
sa.select(
|
||||
old_prompt_table.c.command,
|
||||
old_prompt_table.c.user_id,
|
||||
old_prompt_table.c.title,
|
||||
old_prompt_table.c.content,
|
||||
old_prompt_table.c.timestamp,
|
||||
old_prompt_table.c.access_control,
|
||||
)
|
||||
try:
|
||||
existing_prompts = conn.execute(
|
||||
sa.select(
|
||||
old_prompt_table.c.command,
|
||||
old_prompt_table.c.user_id,
|
||||
old_prompt_table.c.title,
|
||||
old_prompt_table.c.content,
|
||||
old_prompt_table.c.timestamp,
|
||||
old_prompt_table.c.access_control,
|
||||
)
|
||||
).fetchall()
|
||||
except Exception:
|
||||
existing_prompts = []
|
||||
).fetchall()
|
||||
except Exception:
|
||||
# Fallback if table doesn't exist (new install)
|
||||
existing_prompts = []
|
||||
|
||||
# Step 2: Create new prompt table with 'id' as PRIMARY KEY (if not already created)
|
||||
if 'prompt_new' not in existing_tables:
|
||||
op.create_table(
|
||||
'prompt_new',
|
||||
sa.Column('id', sa.Text(), primary_key=True),
|
||||
sa.Column('command', sa.String(), unique=True, index=True),
|
||||
sa.Column('user_id', sa.String(), nullable=False),
|
||||
sa.Column('name', sa.Text(), nullable=False),
|
||||
sa.Column('content', sa.Text(), nullable=False),
|
||||
sa.Column('data', sa.JSON(), nullable=True),
|
||||
sa.Column('meta', sa.JSON(), nullable=True),
|
||||
sa.Column('access_control', sa.JSON(), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False, server_default='1'),
|
||||
sa.Column('version_id', sa.Text(), nullable=True),
|
||||
sa.Column('tags', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=False),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=False),
|
||||
)
|
||||
# Step 2: Create new prompt table with 'id' as PRIMARY KEY
|
||||
op.create_table(
|
||||
'prompt_new',
|
||||
sa.Column('id', sa.Text(), primary_key=True),
|
||||
sa.Column('command', sa.String(), unique=True, index=True),
|
||||
sa.Column('user_id', sa.String(), nullable=False),
|
||||
sa.Column('name', sa.Text(), nullable=False),
|
||||
sa.Column('content', sa.Text(), nullable=False),
|
||||
sa.Column('data', sa.JSON(), nullable=True),
|
||||
sa.Column('meta', sa.JSON(), nullable=True),
|
||||
sa.Column('access_control', sa.JSON(), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False, server_default='1'),
|
||||
sa.Column('version_id', sa.Text(), nullable=True),
|
||||
sa.Column('tags', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=False),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=False),
|
||||
)
|
||||
|
||||
# Step 3: Create prompt_history table (if not already created)
|
||||
if 'prompt_history' not in existing_tables:
|
||||
op.create_table(
|
||||
'prompt_history',
|
||||
sa.Column('id', sa.Text(), primary_key=True),
|
||||
sa.Column('prompt_id', sa.Text(), nullable=False, index=True),
|
||||
sa.Column('parent_id', sa.Text(), nullable=True),
|
||||
sa.Column('snapshot', sa.JSON(), nullable=False),
|
||||
sa.Column('user_id', sa.Text(), nullable=False),
|
||||
sa.Column('commit_message', sa.Text(), nullable=True),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=False),
|
||||
)
|
||||
# Step 3: Create prompt_history table
|
||||
op.create_table(
|
||||
'prompt_history',
|
||||
sa.Column('id', sa.Text(), primary_key=True),
|
||||
sa.Column('prompt_id', sa.Text(), nullable=False, index=True),
|
||||
sa.Column('parent_id', sa.Text(), nullable=True),
|
||||
sa.Column('snapshot', sa.JSON(), nullable=False),
|
||||
sa.Column('user_id', sa.Text(), nullable=False),
|
||||
sa.Column('commit_message', sa.Text(), nullable=True),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=False),
|
||||
)
|
||||
|
||||
# Step 4: Migrate data (only if we have old data to migrate)
|
||||
if existing_prompts:
|
||||
prompt_new_table = sa.table(
|
||||
'prompt_new',
|
||||
sa.column('id', sa.Text()),
|
||||
sa.column('command', sa.String()),
|
||||
sa.column('user_id', sa.String()),
|
||||
sa.column('name', sa.Text()),
|
||||
sa.column('content', sa.Text()),
|
||||
sa.column('data', sa.JSON()),
|
||||
sa.column('meta', sa.JSON()),
|
||||
sa.column('access_control', sa.JSON()),
|
||||
sa.column('is_active', sa.Boolean()),
|
||||
sa.column('version_id', sa.Text()),
|
||||
sa.column('tags', sa.JSON()),
|
||||
sa.column('created_at', sa.BigInteger()),
|
||||
sa.column('updated_at', sa.BigInteger()),
|
||||
)
|
||||
# Step 4: Migrate data
|
||||
prompt_new_table = sa.table(
|
||||
'prompt_new',
|
||||
sa.column('id', sa.Text()),
|
||||
sa.column('command', sa.String()),
|
||||
sa.column('user_id', sa.String()),
|
||||
sa.column('name', sa.Text()),
|
||||
sa.column('content', sa.Text()),
|
||||
sa.column('data', sa.JSON()),
|
||||
sa.column('meta', sa.JSON()),
|
||||
sa.column('access_control', sa.JSON()),
|
||||
sa.column('is_active', sa.Boolean()),
|
||||
sa.column('version_id', sa.Text()),
|
||||
sa.column('tags', sa.JSON()),
|
||||
sa.column('created_at', sa.BigInteger()),
|
||||
sa.column('updated_at', sa.BigInteger()),
|
||||
)
|
||||
|
||||
prompt_history_table = sa.table(
|
||||
'prompt_history',
|
||||
sa.column('id', sa.Text()),
|
||||
sa.column('prompt_id', sa.Text()),
|
||||
sa.column('parent_id', sa.Text()),
|
||||
sa.column('snapshot', sa.JSON()),
|
||||
sa.column('user_id', sa.Text()),
|
||||
sa.column('commit_message', sa.Text()),
|
||||
sa.column('created_at', sa.BigInteger()),
|
||||
)
|
||||
prompt_history_table = sa.table(
|
||||
'prompt_history',
|
||||
sa.column('id', sa.Text()),
|
||||
sa.column('prompt_id', sa.Text()),
|
||||
sa.column('parent_id', sa.Text()),
|
||||
sa.column('snapshot', sa.JSON()),
|
||||
sa.column('user_id', sa.Text()),
|
||||
sa.column('commit_message', sa.Text()),
|
||||
sa.column('created_at', sa.BigInteger()),
|
||||
)
|
||||
|
||||
for row in existing_prompts:
|
||||
command = row[0]
|
||||
user_id = row[1]
|
||||
title = row[2]
|
||||
content = row[3]
|
||||
timestamp = row[4]
|
||||
access_control = row[5]
|
||||
for row in existing_prompts:
|
||||
command = row[0]
|
||||
user_id = row[1]
|
||||
title = row[2]
|
||||
content = row[3]
|
||||
timestamp = row[4]
|
||||
access_control = row[5]
|
||||
|
||||
new_uuid = str(uuid.uuid4())
|
||||
history_uuid = str(uuid.uuid4())
|
||||
clean_command = command[1:] if command and command.startswith('/') else command
|
||||
new_uuid = str(uuid.uuid4())
|
||||
history_uuid = str(uuid.uuid4())
|
||||
clean_command = command[1:] if command and command.startswith('/') else command
|
||||
|
||||
# Insert into prompt_new
|
||||
conn.execute(
|
||||
sa.insert(prompt_new_table).values(
|
||||
id=new_uuid,
|
||||
command=clean_command,
|
||||
user_id=user_id,
|
||||
name=title,
|
||||
content=content,
|
||||
data={},
|
||||
meta={},
|
||||
access_control=access_control,
|
||||
is_active=True,
|
||||
version_id=history_uuid,
|
||||
tags=[],
|
||||
created_at=timestamp,
|
||||
updated_at=timestamp,
|
||||
)
|
||||
# Insert into prompt_new
|
||||
conn.execute(
|
||||
sa.insert(prompt_new_table).values(
|
||||
id=new_uuid,
|
||||
command=clean_command,
|
||||
user_id=user_id,
|
||||
name=title,
|
||||
content=content,
|
||||
data={},
|
||||
meta={},
|
||||
access_control=access_control,
|
||||
is_active=True,
|
||||
version_id=history_uuid,
|
||||
tags=[],
|
||||
created_at=timestamp,
|
||||
updated_at=timestamp,
|
||||
)
|
||||
)
|
||||
|
||||
# Create initial history entry
|
||||
conn.execute(
|
||||
sa.insert(prompt_history_table).values(
|
||||
id=history_uuid,
|
||||
prompt_id=new_uuid,
|
||||
parent_id=None,
|
||||
snapshot={
|
||||
'name': title,
|
||||
'content': content,
|
||||
'command': clean_command,
|
||||
'data': {},
|
||||
'meta': {},
|
||||
'access_control': access_control,
|
||||
},
|
||||
user_id=user_id,
|
||||
commit_message=None,
|
||||
created_at=timestamp,
|
||||
)
|
||||
# Create initial history entry
|
||||
conn.execute(
|
||||
sa.insert(prompt_history_table).values(
|
||||
id=history_uuid,
|
||||
prompt_id=new_uuid,
|
||||
parent_id=None,
|
||||
snapshot={
|
||||
'name': title,
|
||||
'content': content,
|
||||
'command': clean_command,
|
||||
'data': {},
|
||||
'meta': {},
|
||||
'access_control': access_control,
|
||||
},
|
||||
user_id=user_id,
|
||||
commit_message=None,
|
||||
created_at=timestamp,
|
||||
)
|
||||
)
|
||||
|
||||
# Step 5: Replace old table with new one (only if prompt_new exists)
|
||||
# Re-check tables after potential creation above
|
||||
inspector.clear_cache()
|
||||
current_tables = set(inspector.get_table_names())
|
||||
if 'prompt_new' in current_tables:
|
||||
if 'prompt' in current_tables:
|
||||
op.drop_table('prompt')
|
||||
op.rename_table('prompt_new', 'prompt')
|
||||
# Step 5: Replace old table with new one
|
||||
op.drop_table('prompt')
|
||||
op.rename_table('prompt_new', 'prompt')
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
@@ -6,8 +6,8 @@ Create Date: 2024-12-30 03:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = '3781e22d8b01'
|
||||
down_revision = '7826ab40b532'
|
||||
@@ -16,50 +16,38 @@ depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
existing_tables = set(inspector.get_table_names())
|
||||
|
||||
# Add 'type' column to the 'channel' table
|
||||
channel_cols = {c['name'] for c in inspector.get_columns('channel')}
|
||||
if 'type' not in channel_cols:
|
||||
op.add_column(
|
||||
'channel',
|
||||
sa.Column(
|
||||
'type',
|
||||
sa.Text(),
|
||||
nullable=True,
|
||||
),
|
||||
)
|
||||
op.add_column(
|
||||
'channel',
|
||||
sa.Column(
|
||||
'type',
|
||||
sa.Text(),
|
||||
nullable=True,
|
||||
),
|
||||
)
|
||||
|
||||
# Add 'parent_id' column to the 'message' table for threads
|
||||
message_cols = {c['name'] for c in inspector.get_columns('message')}
|
||||
if 'parent_id' not in message_cols:
|
||||
op.add_column(
|
||||
'message',
|
||||
sa.Column('parent_id', sa.Text(), nullable=True),
|
||||
)
|
||||
op.add_column(
|
||||
'message',
|
||||
sa.Column('parent_id', sa.Text(), nullable=True),
|
||||
)
|
||||
|
||||
if 'message_reaction' not in existing_tables:
|
||||
op.create_table(
|
||||
'message_reaction',
|
||||
sa.Column('id', sa.Text(), nullable=False, primary_key=True, unique=True), # Unique reaction ID
|
||||
sa.Column('user_id', sa.Text(), nullable=False), # User who reacted
|
||||
sa.Column('message_id', sa.Text(), nullable=False), # Message that was reacted to
|
||||
sa.Column('name', sa.Text(), nullable=False), # Reaction name (e.g. "thumbs_up")
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=True), # Timestamp of when the reaction was added
|
||||
)
|
||||
op.create_table(
|
||||
'message_reaction',
|
||||
sa.Column('id', sa.Text(), nullable=False, primary_key=True, unique=True), # Unique reaction ID
|
||||
sa.Column('user_id', sa.Text(), nullable=False), # User who reacted
|
||||
sa.Column('message_id', sa.Text(), nullable=False), # Message that was reacted to
|
||||
sa.Column('name', sa.Text(), nullable=False), # Reaction name (e.g. "thumbs_up")
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=True), # Timestamp of when the reaction was added
|
||||
)
|
||||
|
||||
if 'channel_member' not in existing_tables:
|
||||
op.create_table(
|
||||
'channel_member',
|
||||
sa.Column(
|
||||
'id', sa.Text(), nullable=False, primary_key=True, unique=True
|
||||
), # Record ID for the membership row
|
||||
sa.Column('channel_id', sa.Text(), nullable=False), # Associated channel
|
||||
sa.Column('user_id', sa.Text(), nullable=False), # Associated user
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=True), # Timestamp of when the user joined the channel
|
||||
)
|
||||
op.create_table(
|
||||
'channel_member',
|
||||
sa.Column('id', sa.Text(), nullable=False, primary_key=True, unique=True), # Record ID for the membership row
|
||||
sa.Column('channel_id', sa.Text(), nullable=False), # Associated channel
|
||||
sa.Column('user_id', sa.Text(), nullable=False), # Associated user
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=True), # Timestamp of when the user joined the channel
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
|
||||
@@ -6,13 +6,13 @@ Create Date: 2025-11-17 03:45:25.123939
|
||||
|
||||
"""
|
||||
|
||||
import json
|
||||
import time
|
||||
import uuid
|
||||
import time
|
||||
import json
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '37f288994c47'
|
||||
@@ -22,13 +22,6 @@ depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
existing_tables = set(inspector.get_table_names())
|
||||
|
||||
if 'group_member' in existing_tables:
|
||||
return # Already created — skip everything
|
||||
|
||||
# 1. Create new table
|
||||
op.create_table(
|
||||
'group_member',
|
||||
|
||||
@@ -8,8 +8,8 @@ Create Date: 2025-09-08 14:19:59.583921
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '38d63c18f30f'
|
||||
@@ -19,39 +19,50 @@ depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Ensure 'id' column in 'user' table is unique and primary key (ForeignKey constraint)
|
||||
inspector = sa.inspect(op.get_bind())
|
||||
existing_tables = set(inspector.get_table_names())
|
||||
columns = inspector.get_columns('user')
|
||||
|
||||
# ── Create oauth_session table (idempotent) ───────────────────────
|
||||
if 'oauth_session' not in existing_tables:
|
||||
op.create_table(
|
||||
'oauth_session',
|
||||
sa.Column('id', sa.Text(), primary_key=True, nullable=False, unique=True),
|
||||
sa.Column(
|
||||
'user_id',
|
||||
sa.Text(),
|
||||
sa.ForeignKey('user.id', ondelete='CASCADE'),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column('provider', sa.Text(), nullable=False),
|
||||
sa.Column('token', sa.Text(), nullable=False),
|
||||
sa.Column('expires_at', sa.BigInteger(), nullable=False),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=False),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=False),
|
||||
)
|
||||
pk_columns = inspector.get_pk_constraint('user')['constrained_columns']
|
||||
id_column = next((col for col in columns if col['name'] == 'id'), None)
|
||||
|
||||
# Create indexes (idempotent — no-ops when table was just created
|
||||
# with the columns above, and safe to call if indexes already exist).
|
||||
existing_indexes = (
|
||||
{idx['name'] for idx in inspector.get_indexes('oauth_session')} if 'oauth_session' in existing_tables else set()
|
||||
if id_column and not id_column.get('unique', False):
|
||||
unique_constraints = inspector.get_unique_constraints('user')
|
||||
unique_columns = {tuple(u['column_names']) for u in unique_constraints}
|
||||
|
||||
with op.batch_alter_table('user') as batch_op:
|
||||
# If primary key is wrong, drop it
|
||||
if pk_columns and pk_columns != ['id']:
|
||||
batch_op.drop_constraint(inspector.get_pk_constraint('user')['name'], type_='primary')
|
||||
|
||||
# Add unique constraint if missing
|
||||
if ('id',) not in unique_columns:
|
||||
batch_op.create_unique_constraint('uq_user_id', ['id'])
|
||||
|
||||
# Re-create correct primary key
|
||||
batch_op.create_primary_key('pk_user_id', ['id'])
|
||||
|
||||
# Create oauth_session table
|
||||
op.create_table(
|
||||
'oauth_session',
|
||||
sa.Column('id', sa.Text(), primary_key=True, nullable=False, unique=True),
|
||||
sa.Column(
|
||||
'user_id',
|
||||
sa.Text(),
|
||||
sa.ForeignKey('user.id', ondelete='CASCADE'),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column('provider', sa.Text(), nullable=False),
|
||||
sa.Column('token', sa.Text(), nullable=False),
|
||||
sa.Column('expires_at', sa.BigInteger(), nullable=False),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=False),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=False),
|
||||
)
|
||||
|
||||
if 'idx_oauth_session_user_id' not in existing_indexes:
|
||||
op.create_index('idx_oauth_session_user_id', 'oauth_session', ['user_id'])
|
||||
if 'idx_oauth_session_expires_at' not in existing_indexes:
|
||||
op.create_index('idx_oauth_session_expires_at', 'oauth_session', ['expires_at'])
|
||||
if 'idx_oauth_session_user_provider' not in existing_indexes:
|
||||
op.create_index('idx_oauth_session_user_provider', 'oauth_session', ['user_id', 'provider'])
|
||||
# Create indexes for better performance
|
||||
op.create_index('idx_oauth_session_user_id', 'oauth_session', ['user_id'])
|
||||
op.create_index('idx_oauth_session_expires_at', 'oauth_session', ['expires_at'])
|
||||
op.create_index('idx_oauth_session_user_provider', 'oauth_session', ['user_id', 'provider'])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
@@ -6,12 +6,12 @@ Create Date: 2024-10-09 21:02:35.241684
|
||||
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.sql import table, select, update, column
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
from sqlalchemy.sql import column, select, table, update
|
||||
|
||||
import json
|
||||
|
||||
revision = '3ab32c4b8f59'
|
||||
down_revision = '1af9b942657b'
|
||||
|
||||
@@ -8,8 +8,8 @@ Create Date: 2025-08-21 02:07:18.078283
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '3af16a1c9fb6'
|
||||
@@ -19,18 +19,10 @@ depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
user_cols = {c['name'] for c in inspector.get_columns('user')}
|
||||
|
||||
if 'username' not in user_cols:
|
||||
op.add_column('user', sa.Column('username', sa.String(length=50), nullable=True))
|
||||
if 'bio' not in user_cols:
|
||||
op.add_column('user', sa.Column('bio', sa.Text(), nullable=True))
|
||||
if 'gender' not in user_cols:
|
||||
op.add_column('user', sa.Column('gender', sa.Text(), nullable=True))
|
||||
if 'date_of_birth' not in user_cols:
|
||||
op.add_column('user', sa.Column('date_of_birth', sa.Date(), nullable=True))
|
||||
op.add_column('user', sa.Column('username', sa.String(length=50), nullable=True))
|
||||
op.add_column('user', sa.Column('bio', sa.Text(), nullable=True))
|
||||
op.add_column('user', sa.Column('gender', sa.Text(), nullable=True))
|
||||
op.add_column('user', sa.Column('date_of_birth', sa.Date(), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
"""add knowledge_directory table
|
||||
|
||||
Revision ID: 3c9b0ca343fd
|
||||
Revises: a0b1c2d3e4f5
|
||||
Create Date: 2026-05-13 21:58:40.832482
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '3c9b0ca343fd'
|
||||
down_revision: Union[str, None] = 'a0b1c2d3e4f5'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
existing_tables = set(inspector.get_table_names())
|
||||
|
||||
if 'knowledge_directory' not in existing_tables:
|
||||
# Create knowledge_directory table
|
||||
op.create_table(
|
||||
'knowledge_directory',
|
||||
sa.Column('id', sa.Text(), nullable=False),
|
||||
sa.Column('knowledge_id', sa.Text(), nullable=False),
|
||||
sa.Column('parent_id', sa.Text(), nullable=True),
|
||||
sa.Column('name', sa.Text(), nullable=False),
|
||||
sa.Column('user_id', sa.Text(), nullable=False),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=False),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['knowledge_id'], ['knowledge.id'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['parent_id'], ['knowledge_directory.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint(
|
||||
'knowledge_id', 'parent_id', 'name', name='uq_knowledge_directory_knowledge_parent_name'
|
||||
),
|
||||
)
|
||||
op.create_index('ix_knowledge_directory_knowledge_id', 'knowledge_directory', ['knowledge_id'])
|
||||
op.create_index('ix_knowledge_directory_parent_id', 'knowledge_directory', ['parent_id'])
|
||||
|
||||
# Add directory_id column to knowledge_file
|
||||
kf_cols = {c['name'] for c in inspector.get_columns('knowledge_file')}
|
||||
if 'directory_id' not in kf_cols:
|
||||
with op.batch_alter_table('knowledge_file') as batch:
|
||||
batch.add_column(sa.Column('directory_id', sa.Text(), nullable=True))
|
||||
batch.create_foreign_key(
|
||||
'fk_knowledge_file_directory_id',
|
||||
'knowledge_directory',
|
||||
['directory_id'],
|
||||
['id'],
|
||||
ondelete='SET NULL',
|
||||
)
|
||||
batch.create_index('ix_knowledge_file_directory_id', ['directory_id'])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Remove directory_id from knowledge_file
|
||||
with op.batch_alter_table('knowledge_file') as batch:
|
||||
batch.drop_index('ix_knowledge_file_directory_id')
|
||||
batch.drop_constraint('fk_knowledge_file_directory_id', type_='foreignkey')
|
||||
batch.drop_column('directory_id')
|
||||
|
||||
# Drop knowledge_directory table
|
||||
op.drop_index('ix_knowledge_directory_parent_id', table_name='knowledge_directory')
|
||||
op.drop_index('ix_knowledge_directory_knowledge_id', table_name='knowledge_directory')
|
||||
op.drop_table('knowledge_directory')
|
||||
@@ -6,15 +6,16 @@ Create Date: 2025-12-02 06:54:19.401334
|
||||
|
||||
"""
|
||||
|
||||
import json
|
||||
import time
|
||||
import uuid
|
||||
from typing import Sequence, Union
|
||||
|
||||
import open_webui.internal.db
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
import open_webui.internal.db
|
||||
|
||||
import time
|
||||
import json
|
||||
import uuid
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '3e0e00844bb0'
|
||||
@@ -24,13 +25,6 @@ depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
existing_tables = set(inspector.get_table_names())
|
||||
|
||||
if 'knowledge_file' in existing_tables:
|
||||
return # Already created — skip everything
|
||||
|
||||
op.create_table(
|
||||
'knowledge_file',
|
||||
sa.Column('id', sa.Text(), primary_key=True),
|
||||
|
||||
-584
@@ -1,584 +0,0 @@
|
||||
"""reshape config to per key rows
|
||||
|
||||
Revision ID: 3ff2c63645b8
|
||||
Revises: 461111b60977
|
||||
Create Date: 2026-06-17 00:50:51.477073
|
||||
|
||||
"""
|
||||
|
||||
import json
|
||||
import time
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '3ff2c63645b8'
|
||||
down_revision: Union[str, None] = '461111b60977'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
# Maps every dot-notation blob path to its legacy env/config key name.
|
||||
# Built from the legacy persistent config declarations in config.py.
|
||||
BLOB_PATH_TO_KEY = {
|
||||
'audio.stt.allowed_extensions': 'AUDIO_STT_ALLOWED_EXTENSIONS',
|
||||
'audio.stt.azure.api_key': 'AUDIO_STT_AZURE_API_KEY',
|
||||
'audio.stt.azure.base_url': 'AUDIO_STT_AZURE_BASE_URL',
|
||||
'audio.stt.azure.locales': 'AUDIO_STT_AZURE_LOCALES',
|
||||
'audio.stt.azure.max_speakers': 'AUDIO_STT_AZURE_MAX_SPEAKERS',
|
||||
'audio.stt.azure.region': 'AUDIO_STT_AZURE_REGION',
|
||||
'audio.stt.deepgram.api_key': 'DEEPGRAM_API_KEY',
|
||||
'audio.stt.engine': 'AUDIO_STT_ENGINE',
|
||||
'audio.stt.mistral.api_base_url': 'AUDIO_STT_MISTRAL_API_BASE_URL',
|
||||
'audio.stt.mistral.api_key': 'AUDIO_STT_MISTRAL_API_KEY',
|
||||
'audio.stt.mistral.use_chat_completions': 'AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS',
|
||||
'audio.stt.model': 'AUDIO_STT_MODEL',
|
||||
'audio.stt.openai.api_base_url': 'AUDIO_STT_OPENAI_API_BASE_URL',
|
||||
'audio.stt.openai.api_key': 'AUDIO_STT_OPENAI_API_KEY',
|
||||
'audio.stt.supported_content_types': 'AUDIO_STT_SUPPORTED_CONTENT_TYPES',
|
||||
'audio.stt.whisper_model': 'WHISPER_MODEL',
|
||||
'audio.tts.api_key': 'AUDIO_TTS_API_KEY',
|
||||
'audio.tts.azure.speech_base_url': 'AUDIO_TTS_AZURE_SPEECH_BASE_URL',
|
||||
'audio.tts.azure.speech_output_format': 'AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT',
|
||||
'audio.tts.azure.speech_region': 'AUDIO_TTS_AZURE_SPEECH_REGION',
|
||||
'audio.tts.engine': 'AUDIO_TTS_ENGINE',
|
||||
'audio.tts.mistral.api_base_url': 'AUDIO_TTS_MISTRAL_API_BASE_URL',
|
||||
'audio.tts.mistral.api_key': 'AUDIO_TTS_MISTRAL_API_KEY',
|
||||
'audio.tts.model': 'AUDIO_TTS_MODEL',
|
||||
'audio.tts.openai.api_base_url': 'AUDIO_TTS_OPENAI_API_BASE_URL',
|
||||
'audio.tts.openai.api_key': 'AUDIO_TTS_OPENAI_API_KEY',
|
||||
'audio.tts.openai.params': 'AUDIO_TTS_OPENAI_PARAMS',
|
||||
'audio.tts.split_on': 'AUDIO_TTS_SPLIT_ON',
|
||||
'audio.tts.voice': 'AUDIO_TTS_VOICE',
|
||||
'auth.admin.email': 'ADMIN_EMAIL',
|
||||
'auth.admin.show': 'SHOW_ADMIN_DETAILS',
|
||||
'auth.api_key.allowed_endpoints': 'API_KEYS_ALLOWED_ENDPOINTS',
|
||||
'auth.api_key.endpoint_restrictions': 'ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS',
|
||||
'auth.enable_api_keys': 'ENABLE_API_KEYS',
|
||||
'auth.jwt_expiry': 'JWT_EXPIRES_IN',
|
||||
'automations.enable': 'ENABLE_AUTOMATIONS',
|
||||
'automations.max_count': 'AUTOMATION_MAX_COUNT',
|
||||
'automations.min_interval': 'AUTOMATION_MIN_INTERVAL',
|
||||
'calendar.enable': 'ENABLE_CALENDAR',
|
||||
'channels.enable': 'ENABLE_CHANNELS',
|
||||
'code_execution.enable': 'ENABLE_CODE_EXECUTION',
|
||||
'code_execution.engine': 'CODE_EXECUTION_ENGINE',
|
||||
'code_execution.jupyter.auth': 'CODE_EXECUTION_JUPYTER_AUTH',
|
||||
'code_execution.jupyter.auth_password': 'CODE_EXECUTION_JUPYTER_AUTH_PASSWORD',
|
||||
'code_execution.jupyter.auth_token': 'CODE_EXECUTION_JUPYTER_AUTH_TOKEN',
|
||||
'code_execution.jupyter.timeout': 'CODE_EXECUTION_JUPYTER_TIMEOUT',
|
||||
'code_execution.jupyter.url': 'CODE_EXECUTION_JUPYTER_URL',
|
||||
'code_interpreter.enable': 'ENABLE_CODE_INTERPRETER',
|
||||
'code_interpreter.engine': 'CODE_INTERPRETER_ENGINE',
|
||||
'code_interpreter.jupyter.auth': 'CODE_INTERPRETER_JUPYTER_AUTH',
|
||||
'code_interpreter.jupyter.auth_password': 'CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD',
|
||||
'code_interpreter.jupyter.auth_token': 'CODE_INTERPRETER_JUPYTER_AUTH_TOKEN',
|
||||
'code_interpreter.jupyter.timeout': 'CODE_INTERPRETER_JUPYTER_TIMEOUT',
|
||||
'code_interpreter.jupyter.url': 'CODE_INTERPRETER_JUPYTER_URL',
|
||||
'code_interpreter.prompt_template': 'CODE_INTERPRETER_PROMPT_TEMPLATE',
|
||||
'direct.enable': 'ENABLE_DIRECT_CONNECTIONS',
|
||||
'evaluation.arena.enable': 'ENABLE_EVALUATION_ARENA_MODELS',
|
||||
'evaluation.arena.models': 'EVALUATION_ARENA_MODELS',
|
||||
'file.image_compression_height': 'FILE_IMAGE_COMPRESSION_HEIGHT',
|
||||
'file.image_compression_width': 'FILE_IMAGE_COMPRESSION_WIDTH',
|
||||
'folders.enable': 'ENABLE_FOLDERS',
|
||||
'folders.max_file_count': 'FOLDER_MAX_FILE_COUNT',
|
||||
'google_drive.api_key': 'GOOGLE_DRIVE_API_KEY',
|
||||
'google_drive.client_id': 'GOOGLE_DRIVE_CLIENT_ID',
|
||||
'google_drive.enable': 'ENABLE_GOOGLE_DRIVE_INTEGRATION',
|
||||
'image_generation.automatic1111.api_auth': 'AUTOMATIC1111_API_AUTH',
|
||||
'image_generation.automatic1111.api_params': 'AUTOMATIC1111_PARAMS',
|
||||
'image_generation.automatic1111.base_url': 'AUTOMATIC1111_BASE_URL',
|
||||
'image_generation.comfyui.api_key': 'COMFYUI_API_KEY',
|
||||
'image_generation.comfyui.base_url': 'COMFYUI_BASE_URL',
|
||||
'image_generation.comfyui.nodes': 'COMFYUI_WORKFLOW_NODES',
|
||||
'image_generation.comfyui.workflow': 'COMFYUI_WORKFLOW',
|
||||
'image_generation.enable': 'ENABLE_IMAGE_GENERATION',
|
||||
'image_generation.engine': 'IMAGE_GENERATION_ENGINE',
|
||||
'image_generation.gemini.api_base_url': 'IMAGES_GEMINI_API_BASE_URL',
|
||||
'image_generation.gemini.api_key': 'IMAGES_GEMINI_API_KEY',
|
||||
'image_generation.gemini.endpoint_method': 'IMAGES_GEMINI_ENDPOINT_METHOD',
|
||||
'image_generation.model': 'IMAGE_GENERATION_MODEL',
|
||||
'image_generation.openai.api_base_url': 'IMAGES_OPENAI_API_BASE_URL',
|
||||
'image_generation.openai.api_key': 'IMAGES_OPENAI_API_KEY',
|
||||
'image_generation.openai.api_version': 'IMAGES_OPENAI_API_VERSION',
|
||||
'image_generation.openai.params': 'IMAGES_OPENAI_API_PARAMS',
|
||||
'image_generation.prompt.enable': 'ENABLE_IMAGE_PROMPT_GENERATION',
|
||||
'image_generation.size': 'IMAGE_SIZE',
|
||||
'image_generation.steps': 'IMAGE_STEPS',
|
||||
'images.edit.comfyui.api_key': 'IMAGES_EDIT_COMFYUI_API_KEY',
|
||||
'images.edit.comfyui.base_url': 'IMAGES_EDIT_COMFYUI_BASE_URL',
|
||||
'images.edit.comfyui.nodes': 'IMAGES_EDIT_COMFYUI_WORKFLOW_NODES',
|
||||
'images.edit.comfyui.workflow': 'IMAGES_EDIT_COMFYUI_WORKFLOW',
|
||||
'images.edit.enable': 'ENABLE_IMAGE_EDIT',
|
||||
'images.edit.engine': 'IMAGE_EDIT_ENGINE',
|
||||
'images.edit.gemini.api_base_url': 'IMAGES_EDIT_GEMINI_API_BASE_URL',
|
||||
'images.edit.gemini.api_key': 'IMAGES_EDIT_GEMINI_API_KEY',
|
||||
'images.edit.model': 'IMAGE_EDIT_MODEL',
|
||||
'images.edit.openai.api_base_url': 'IMAGES_EDIT_OPENAI_API_BASE_URL',
|
||||
'images.edit.openai.api_key': 'IMAGES_EDIT_OPENAI_API_KEY',
|
||||
'images.edit.openai.api_version': 'IMAGES_EDIT_OPENAI_API_VERSION',
|
||||
'images.edit.size': 'IMAGE_EDIT_SIZE',
|
||||
'ldap.enable': 'ENABLE_LDAP',
|
||||
'ldap.group.enable_creation': 'ENABLE_LDAP_GROUP_CREATION',
|
||||
'ldap.group.enable_management': 'ENABLE_LDAP_GROUP_MANAGEMENT',
|
||||
'ldap.server.app_dn': 'LDAP_APP_DN',
|
||||
'ldap.server.app_password': 'LDAP_APP_PASSWORD',
|
||||
'ldap.server.attribute_for_groups': 'LDAP_ATTRIBUTE_FOR_GROUPS',
|
||||
'ldap.server.attribute_for_mail': 'LDAP_ATTRIBUTE_FOR_MAIL',
|
||||
'ldap.server.attribute_for_username': 'LDAP_ATTRIBUTE_FOR_USERNAME',
|
||||
'ldap.server.ca_cert_file': 'LDAP_CA_CERT_FILE',
|
||||
'ldap.server.ciphers': 'LDAP_CIPHERS',
|
||||
'ldap.server.host': 'LDAP_SERVER_HOST',
|
||||
'ldap.server.label': 'LDAP_SERVER_LABEL',
|
||||
'ldap.server.port': 'LDAP_SERVER_PORT',
|
||||
'ldap.server.search_filter': 'LDAP_SEARCH_FILTER',
|
||||
'ldap.server.use_tls': 'LDAP_USE_TLS',
|
||||
'ldap.server.users_dn': 'LDAP_SEARCH_BASE',
|
||||
'ldap.server.validate_cert': 'LDAP_VALIDATE_CERT',
|
||||
'memories.enable': 'ENABLE_MEMORIES',
|
||||
'models.base_models_cache': 'ENABLE_BASE_MODELS_CACHE',
|
||||
'models.default_metadata': 'DEFAULT_MODEL_METADATA',
|
||||
'models.default_params': 'DEFAULT_MODEL_PARAMS',
|
||||
'notes.enable': 'ENABLE_NOTES',
|
||||
# OAuth — direct paths
|
||||
'oauth.admin_roles': 'OAUTH_ADMIN_ROLES',
|
||||
'oauth.allowed_domains': 'OAUTH_ALLOWED_DOMAINS',
|
||||
'oauth.allowed_roles': 'OAUTH_ALLOWED_ROLES',
|
||||
'oauth.audience': 'OAUTH_AUDIENCE',
|
||||
'oauth.auto_redirect': 'OAUTH_AUTO_REDIRECT',
|
||||
'oauth.blocked_groups': 'OAUTH_BLOCKED_GROUPS',
|
||||
'oauth.client.timeout': 'OAUTH_CLIENT_TIMEOUT',
|
||||
'oauth.enable_group_creation': 'ENABLE_OAUTH_GROUP_CREATION',
|
||||
'oauth.enable_group_mapping': 'ENABLE_OAUTH_GROUP_MANAGEMENT',
|
||||
'oauth.enable_role_mapping': 'ENABLE_OAUTH_ROLE_MANAGEMENT',
|
||||
'oauth.enable_signup': 'ENABLE_OAUTH_SIGNUP',
|
||||
'oauth.group_default_share': 'OAUTH_GROUP_DEFAULT_SHARE',
|
||||
'oauth.merge_accounts_by_email': 'OAUTH_MERGE_ACCOUNTS_BY_EMAIL',
|
||||
'oauth.refresh_token_include_scope': 'OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE',
|
||||
'oauth.roles_claim': 'OAUTH_ROLES_CLAIM',
|
||||
'oauth.update_email_on_login': 'OAUTH_UPDATE_EMAIL_ON_LOGIN',
|
||||
'oauth.update_name_on_login': 'OAUTH_UPDATE_NAME_ON_LOGIN',
|
||||
'oauth.update_picture_on_login': 'OAUTH_UPDATE_PICTURE_ON_LOGIN',
|
||||
# OAuth — generic provider paths
|
||||
'oauth.client_id': 'OAUTH_CLIENT_ID',
|
||||
'oauth.client_secret': 'OAUTH_CLIENT_SECRET',
|
||||
'oauth.code_challenge_method': 'OAUTH_CODE_CHALLENGE_METHOD',
|
||||
'oauth.email_claim': 'OAUTH_EMAIL_CLAIM',
|
||||
'oauth.end_session_endpoint': 'OPENID_END_SESSION_ENDPOINT',
|
||||
'oauth.group_claim': 'OAUTH_GROUP_CLAIM',
|
||||
'oauth.picture_claim': 'OAUTH_PICTURE_CLAIM',
|
||||
'oauth.provider_name': 'OAUTH_PROVIDER_NAME',
|
||||
'oauth.provider_url': 'OPENID_PROVIDER_URL',
|
||||
'oauth.redirect_uri': 'OPENID_REDIRECT_URI',
|
||||
'oauth.scopes': 'OAUTH_SCOPES',
|
||||
'oauth.sub_claim': 'OAUTH_SUB_CLAIM',
|
||||
'oauth.timeout': 'OAUTH_TIMEOUT',
|
||||
'oauth.token_endpoint_auth_method': 'OAUTH_TOKEN_ENDPOINT_AUTH_METHOD',
|
||||
'oauth.username_claim': 'OAUTH_USERNAME_CLAIM',
|
||||
# OAuth — OIDC nested paths (flattened)
|
||||
'oauth.oidc.avatar_claim': 'OAUTH_PICTURE_CLAIM',
|
||||
'oauth.oidc.client_id': 'OAUTH_CLIENT_ID',
|
||||
'oauth.oidc.client_secret': 'OAUTH_CLIENT_SECRET',
|
||||
'oauth.oidc.code_challenge_method': 'OAUTH_CODE_CHALLENGE_METHOD',
|
||||
'oauth.oidc.email_claim': 'OAUTH_EMAIL_CLAIM',
|
||||
'oauth.oidc.end_session_endpoint': 'OPENID_END_SESSION_ENDPOINT',
|
||||
'oauth.oidc.group_claim': 'OAUTH_GROUP_CLAIM', # renamed from OAUTH_GROUPS_CLAIM
|
||||
'oauth.oidc.oauth_timeout': 'OAUTH_TIMEOUT',
|
||||
'oauth.oidc.provider_name': 'OAUTH_PROVIDER_NAME',
|
||||
'oauth.oidc.provider_url': 'OPENID_PROVIDER_URL',
|
||||
'oauth.oidc.redirect_uri': 'OPENID_REDIRECT_URI',
|
||||
'oauth.oidc.scopes': 'OAUTH_SCOPES',
|
||||
'oauth.oidc.sub_claim': 'OAUTH_SUB_CLAIM',
|
||||
'oauth.oidc.token_endpoint_auth_method': 'OAUTH_TOKEN_ENDPOINT_AUTH_METHOD',
|
||||
'oauth.oidc.username_claim': 'OAUTH_USERNAME_CLAIM',
|
||||
# OAuth — provider-specific
|
||||
'oauth.feishu.client_id': 'FEISHU_CLIENT_ID',
|
||||
'oauth.feishu.client_secret': 'FEISHU_CLIENT_SECRET',
|
||||
'oauth.feishu.redirect_uri': 'FEISHU_REDIRECT_URI',
|
||||
'oauth.feishu.scope': 'FEISHU_OAUTH_SCOPE',
|
||||
'oauth.github.client_id': 'GITHUB_CLIENT_ID',
|
||||
'oauth.github.client_secret': 'GITHUB_CLIENT_SECRET',
|
||||
'oauth.github.redirect_uri': 'GITHUB_CLIENT_REDIRECT_URI',
|
||||
'oauth.github.scope': 'GITHUB_CLIENT_SCOPE',
|
||||
'oauth.google.client_id': 'GOOGLE_CLIENT_ID',
|
||||
'oauth.google.client_secret': 'GOOGLE_CLIENT_SECRET',
|
||||
'oauth.google.redirect_uri': 'GOOGLE_REDIRECT_URI',
|
||||
'oauth.google.scope': 'GOOGLE_OAUTH_SCOPE',
|
||||
'oauth.microsoft.client_id': 'MICROSOFT_CLIENT_ID',
|
||||
'oauth.microsoft.client_secret': 'MICROSOFT_CLIENT_SECRET',
|
||||
'oauth.microsoft.login_base_url': 'MICROSOFT_CLIENT_LOGIN_BASE_URL',
|
||||
'oauth.microsoft.picture_url': 'MICROSOFT_CLIENT_PICTURE_URL',
|
||||
'oauth.microsoft.redirect_uri': 'MICROSOFT_REDIRECT_URI',
|
||||
'oauth.microsoft.scope': 'MICROSOFT_OAUTH_SCOPE',
|
||||
'oauth.microsoft.tenant_id': 'MICROSOFT_CLIENT_TENANT_ID',
|
||||
# Ollama / OpenAI
|
||||
'ollama.api_configs': 'OLLAMA_API_CONFIGS',
|
||||
'ollama.base_urls': 'OLLAMA_BASE_URLS',
|
||||
'ollama.enable': 'ENABLE_OLLAMA_API',
|
||||
'onedrive.enable': 'ENABLE_ONEDRIVE_INTEGRATION',
|
||||
'onedrive.sharepoint_tenant_id': 'ONEDRIVE_SHAREPOINT_TENANT_ID',
|
||||
'onedrive.sharepoint_url': 'ONEDRIVE_SHAREPOINT_URL',
|
||||
'openai.api_base_urls': 'OPENAI_API_BASE_URLS',
|
||||
'openai.api_configs': 'OPENAI_API_CONFIGS',
|
||||
'openai.api_keys': 'OPENAI_API_KEYS',
|
||||
'openai.enable': 'ENABLE_OPENAI_API',
|
||||
# RAG
|
||||
'rag.content_extraction_engine': 'CONTENT_EXTRACTION_ENGINE',
|
||||
'rag.datalab_marker_use_llm': 'DATALAB_MARKER_USE_LLM',
|
||||
'rag.mistral_ocr_api_base_url': 'MISTRAL_OCR_API_BASE_URL',
|
||||
'rag.azure_openai.api_key': 'RAG_AZURE_OPENAI_API_KEY',
|
||||
'rag.azure_openai.api_version': 'RAG_AZURE_OPENAI_API_VERSION',
|
||||
'rag.azure_openai.base_url': 'RAG_AZURE_OPENAI_BASE_URL',
|
||||
'rag.bypass_embedding_and_retrieval': 'BYPASS_EMBEDDING_AND_RETRIEVAL',
|
||||
'rag.chunk_min_size_target': 'CHUNK_MIN_SIZE_TARGET',
|
||||
'rag.chunk_overlap': 'CHUNK_OVERLAP',
|
||||
'rag.chunk_size': 'CHUNK_SIZE',
|
||||
'rag.datalab_marker_additional_config': 'DATALAB_MARKER_ADDITIONAL_CONFIG',
|
||||
'rag.datalab_marker_api_base_url': 'DATALAB_MARKER_API_BASE_URL',
|
||||
'rag.datalab_marker_api_key': 'DATALAB_MARKER_API_KEY',
|
||||
'rag.datalab_marker_disable_image_extraction': 'DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION',
|
||||
'rag.datalab_marker_force_ocr': 'DATALAB_MARKER_FORCE_OCR',
|
||||
'rag.datalab_marker_format_lines': 'DATALAB_MARKER_FORMAT_LINES',
|
||||
'rag.datalab_marker_output_format': 'DATALAB_MARKER_OUTPUT_FORMAT',
|
||||
'rag.datalab_marker_paginate': 'DATALAB_MARKER_PAGINATE',
|
||||
'rag.datalab_marker_skip_cache': 'DATALAB_MARKER_SKIP_CACHE',
|
||||
'rag.datalab_marker_strip_existing_ocr': 'DATALAB_MARKER_STRIP_EXISTING_OCR',
|
||||
'rag.docling_api_key': 'DOCLING_API_KEY',
|
||||
'rag.docling_params': 'DOCLING_PARAMS',
|
||||
'rag.docling_server_url': 'DOCLING_SERVER_URL',
|
||||
'rag.document_intelligence_endpoint': 'DOCUMENT_INTELLIGENCE_ENDPOINT',
|
||||
'rag.document_intelligence_key': 'DOCUMENT_INTELLIGENCE_KEY',
|
||||
'rag.document_intelligence_model': 'DOCUMENT_INTELLIGENCE_MODEL',
|
||||
'rag.embedding_batch_size': 'RAG_EMBEDDING_BATCH_SIZE',
|
||||
'rag.embedding_concurrent_requests': 'RAG_EMBEDDING_CONCURRENT_REQUESTS',
|
||||
'rag.embedding_engine': 'RAG_EMBEDDING_ENGINE',
|
||||
'rag.embedding_model': 'RAG_EMBEDDING_MODEL',
|
||||
'rag.enable_async_embedding': 'ENABLE_ASYNC_EMBEDDING',
|
||||
'rag.enable_hybrid_search': 'ENABLE_RAG_HYBRID_SEARCH',
|
||||
'rag.enable_hybrid_search_enriched_texts': 'ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS',
|
||||
'rag.enable_markdown_header_text_splitter': 'ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER',
|
||||
'rag.external_document_loader_api_key': 'EXTERNAL_DOCUMENT_LOADER_API_KEY',
|
||||
'rag.external_document_loader_url': 'EXTERNAL_DOCUMENT_LOADER_URL',
|
||||
'rag.external_reranker_api_key': 'RAG_EXTERNAL_RERANKER_API_KEY',
|
||||
'rag.external_reranker_timeout': 'RAG_EXTERNAL_RERANKER_TIMEOUT',
|
||||
'rag.external_reranker_url': 'RAG_EXTERNAL_RERANKER_URL',
|
||||
'rag.file.allowed_extensions': 'RAG_ALLOWED_FILE_EXTENSIONS',
|
||||
'rag.file.max_count': 'RAG_FILE_MAX_COUNT',
|
||||
'rag.file.max_size': 'RAG_FILE_MAX_SIZE',
|
||||
'rag.full_context': 'RAG_FULL_CONTEXT',
|
||||
'rag.hybrid_bm25_weight': 'RAG_HYBRID_BM25_WEIGHT',
|
||||
'rag.mineru_api_key': 'MINERU_API_KEY',
|
||||
'rag.mineru_api_mode': 'MINERU_API_MODE',
|
||||
'rag.mineru_api_timeout': 'MINERU_API_TIMEOUT',
|
||||
'rag.mineru_api_url': 'MINERU_API_URL',
|
||||
'rag.mineru_file_extensions': 'MINERU_FILE_EXTENSIONS',
|
||||
'rag.mineru_params': 'MINERU_PARAMS',
|
||||
'rag.mistral_ocr_api_key': 'MISTRAL_OCR_API_KEY',
|
||||
'rag.ollama.key': 'RAG_OLLAMA_API_KEY',
|
||||
'rag.ollama.url': 'RAG_OLLAMA_BASE_URL',
|
||||
'rag.openai_api_base_url': 'RAG_OPENAI_API_BASE_URL',
|
||||
'rag.openai_api_key': 'RAG_OPENAI_API_KEY',
|
||||
'rag.paddleocr_vl_base_url': 'PADDLEOCR_VL_BASE_URL',
|
||||
'rag.paddleocr_vl_token': 'PADDLEOCR_VL_TOKEN',
|
||||
'rag.pdf_extract_images': 'PDF_EXTRACT_IMAGES',
|
||||
'rag.pdf_loader_mode': 'PDF_LOADER_MODE',
|
||||
'rag.relevance_threshold': 'RAG_RELEVANCE_THRESHOLD',
|
||||
'rag.reranking_batch_size': 'RAG_RERANKING_BATCH_SIZE',
|
||||
'rag.reranking_engine': 'RAG_RERANKING_ENGINE',
|
||||
'rag.reranking_model': 'RAG_RERANKING_MODEL',
|
||||
'rag.template': 'RAG_TEMPLATE',
|
||||
'rag.text_splitter': 'RAG_TEXT_SPLITTER',
|
||||
'rag.tika_server_url': 'TIKA_SERVER_URL',
|
||||
'rag.tiktoken_encoding_name': 'TIKTOKEN_ENCODING_NAME',
|
||||
'rag.top_k': 'RAG_TOP_K',
|
||||
'rag.top_k_reranker': 'RAG_TOP_K_RERANKER',
|
||||
# RAG — Web
|
||||
'rag.web.fetch.max_content_length': 'WEB_FETCH_MAX_CONTENT_LENGTH',
|
||||
'rag.web.loader.concurrent_requests': 'WEB_LOADER_CONCURRENT_REQUESTS',
|
||||
'rag.web.loader.engine': 'WEB_LOADER_ENGINE',
|
||||
'rag.web.loader.external_web_loader_api_key': 'EXTERNAL_WEB_LOADER_API_KEY',
|
||||
'rag.web.loader.external_web_loader_url': 'EXTERNAL_WEB_LOADER_URL',
|
||||
'rag.web.loader.firecrawl_api_key': 'FIRECRAWL_API_KEY',
|
||||
'rag.web.loader.firecrawl_api_url': 'FIRECRAWL_API_BASE_URL',
|
||||
'rag.web.loader.firecrawl_timeout': 'FIRECRAWL_TIMEOUT',
|
||||
'rag.web.loader.playwright_timeout': 'PLAYWRIGHT_TIMEOUT',
|
||||
'rag.web.loader.playwright_ws_url': 'PLAYWRIGHT_WS_URL',
|
||||
'rag.web.loader.ssl_verification': 'ENABLE_WEB_LOADER_SSL_VERIFICATION',
|
||||
'rag.web.loader.timeout': 'WEB_LOADER_TIMEOUT',
|
||||
'rag.web.search.azure_ai_search_api_key': 'AZURE_AI_SEARCH_API_KEY',
|
||||
'rag.web.search.azure_ai_search_endpoint': 'AZURE_AI_SEARCH_ENDPOINT',
|
||||
'rag.web.search.azure_ai_search_index_name': 'AZURE_AI_SEARCH_INDEX_NAME',
|
||||
'rag.web.search.bing_search_v7_endpoint': 'BING_SEARCH_V7_ENDPOINT',
|
||||
'rag.web.search.bing_search_v7_subscription_key': 'BING_SEARCH_V7_SUBSCRIPTION_KEY',
|
||||
'rag.web.search.bocha_search_api_key': 'BOCHA_SEARCH_API_KEY',
|
||||
'rag.web.search.brave_search_api_key': 'BRAVE_SEARCH_API_KEY',
|
||||
'rag.web.search.brave_search_context_tokens': 'BRAVE_SEARCH_CONTEXT_TOKENS',
|
||||
'rag.web.search.bypass_embedding_and_retrieval': 'BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL',
|
||||
'rag.web.search.bypass_web_loader': 'BYPASS_WEB_SEARCH_WEB_LOADER',
|
||||
'rag.web.search.concurrent_requests': 'WEB_SEARCH_CONCURRENT_REQUESTS',
|
||||
'rag.web.search.ddgs_backend': 'DDGS_BACKEND',
|
||||
'rag.web.search.domain.filter_list': 'WEB_SEARCH_DOMAIN_FILTER_LIST',
|
||||
'rag.web.search.enable': 'ENABLE_WEB_SEARCH',
|
||||
'rag.web.search.engine': 'WEB_SEARCH_ENGINE',
|
||||
'rag.web.search.exa_api_key': 'EXA_API_KEY',
|
||||
'rag.web.search.external_web_search_api_key': 'EXTERNAL_WEB_SEARCH_API_KEY',
|
||||
'rag.web.search.external_web_search_url': 'EXTERNAL_WEB_SEARCH_URL',
|
||||
'rag.web.search.google_pse_api_key': 'GOOGLE_PSE_API_KEY',
|
||||
'rag.web.search.google_pse_engine_id': 'GOOGLE_PSE_ENGINE_ID',
|
||||
'rag.web.search.jina_api_base_url': 'JINA_API_BASE_URL',
|
||||
'rag.web.search.jina_api_key': 'JINA_API_KEY',
|
||||
'rag.web.search.kagi_search_api_key': 'KAGI_SEARCH_API_KEY',
|
||||
'rag.web.search.linkup_api_key': 'LINKUP_API_KEY',
|
||||
'rag.web.search.linkup_search_params': 'LINKUP_SEARCH_PARAMS',
|
||||
'rag.web.search.mojeek_search_api_key': 'MOJEEK_SEARCH_API_KEY',
|
||||
'rag.web.search.ollama_cloud_api_key': 'OLLAMA_CLOUD_WEB_SEARCH_API_KEY',
|
||||
'rag.web.search.perplexity_api_key': 'PERPLEXITY_API_KEY',
|
||||
'rag.web.search.perplexity_model': 'PERPLEXITY_MODEL',
|
||||
'rag.web.search.perplexity_search_api_url': 'PERPLEXITY_SEARCH_API_URL',
|
||||
'rag.web.search.perplexity_search_context_usage': 'PERPLEXITY_SEARCH_CONTEXT_USAGE',
|
||||
'rag.web.search.result_count': 'WEB_SEARCH_RESULT_COUNT',
|
||||
'rag.web.search.searchapi_api_key': 'SEARCHAPI_API_KEY',
|
||||
'rag.web.search.searchapi_engine': 'SEARCHAPI_ENGINE',
|
||||
'rag.web.search.searxng_language': 'SEARXNG_LANGUAGE',
|
||||
'rag.web.search.searxng_query_url': 'SEARXNG_QUERY_URL',
|
||||
'rag.web.search.serpapi_api_key': 'SERPAPI_API_KEY',
|
||||
'rag.web.search.serpapi_engine': 'SERPAPI_ENGINE',
|
||||
'rag.web.search.serper_api_key': 'SERPER_API_KEY',
|
||||
'rag.web.search.serply_api_key': 'SERPLY_API_KEY',
|
||||
'rag.web.search.serpstack_api_key': 'SERPSTACK_API_KEY',
|
||||
'rag.web.search.serpstack_https': 'SERPSTACK_HTTPS',
|
||||
'rag.web.search.sougou_api_sid': 'SOUGOU_API_SID',
|
||||
'rag.web.search.sougou_api_sk': 'SOUGOU_API_SK',
|
||||
'rag.web.search.tavily_api_key': 'TAVILY_API_KEY',
|
||||
'rag.web.search.tavily_extract_depth': 'TAVILY_EXTRACT_DEPTH',
|
||||
'rag.web.search.trust_env': 'WEB_SEARCH_TRUST_ENV',
|
||||
'rag.web.search.yacy_password': 'YACY_PASSWORD',
|
||||
'rag.web.search.yacy_query_url': 'YACY_QUERY_URL',
|
||||
'rag.web.search.yacy_username': 'YACY_USERNAME',
|
||||
'rag.web.search.yandex_web_search_api_key': 'YANDEX_WEB_SEARCH_API_KEY',
|
||||
'rag.web.search.yandex_web_search_config': 'YANDEX_WEB_SEARCH_CONFIG',
|
||||
'rag.web.search.yandex_web_search_url': 'YANDEX_WEB_SEARCH_URL',
|
||||
'rag.web.search.youcom_api_key': 'YOUCOM_API_KEY',
|
||||
'rag.youtube_loader_language': 'YOUTUBE_LOADER_LANGUAGE',
|
||||
'rag.youtube_loader_proxy_url': 'YOUTUBE_LOADER_PROXY_URL',
|
||||
# Tasks
|
||||
'task.autocomplete.enable': 'ENABLE_AUTOCOMPLETE_GENERATION',
|
||||
'task.autocomplete.input_max_length': 'AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH',
|
||||
'task.autocomplete.prompt_template': 'AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE',
|
||||
'task.follow_up.enable': 'ENABLE_FOLLOW_UP_GENERATION',
|
||||
'task.follow_up.prompt_template': 'FOLLOW_UP_GENERATION_PROMPT_TEMPLATE',
|
||||
'task.image.prompt_template': 'IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE',
|
||||
'task.model.default': 'TASK_MODEL',
|
||||
'task.model.external': 'TASK_MODEL_EXTERNAL',
|
||||
'task.query.prompt_template': 'QUERY_GENERATION_PROMPT_TEMPLATE',
|
||||
'task.query.retrieval.enable': 'ENABLE_RETRIEVAL_QUERY_GENERATION',
|
||||
'task.query.search.enable': 'ENABLE_SEARCH_QUERY_GENERATION',
|
||||
'task.tags.enable': 'ENABLE_TAGS_GENERATION',
|
||||
'task.tags.prompt_template': 'TAGS_GENERATION_PROMPT_TEMPLATE',
|
||||
'task.title.enable': 'ENABLE_TITLE_GENERATION',
|
||||
'task.title.prompt_template': 'TITLE_GENERATION_PROMPT_TEMPLATE',
|
||||
'task.tools.prompt_template': 'TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE',
|
||||
'task.voice.prompt.enable': 'ENABLE_VOICE_MODE_PROMPT',
|
||||
'task.voice.prompt_template': 'VOICE_MODE_PROMPT_TEMPLATE',
|
||||
# Misc
|
||||
'terminal_server.connections': 'TERMINAL_SERVER_CONNECTIONS',
|
||||
'tool_server.connections': 'TOOL_SERVER_CONNECTIONS',
|
||||
'ui.banners': 'WEBUI_BANNERS',
|
||||
'ui.default_group_id': 'DEFAULT_GROUP_ID',
|
||||
'ui.default_locale': 'DEFAULT_LOCALE',
|
||||
'ui.default_models': 'DEFAULT_MODELS',
|
||||
'ui.default_pinned_models': 'DEFAULT_PINNED_MODELS',
|
||||
'ui.default_user_role': 'DEFAULT_USER_ROLE',
|
||||
'ui.enable_community_sharing': 'ENABLE_COMMUNITY_SHARING',
|
||||
'ui.enable_login_form': 'ENABLE_LOGIN_FORM',
|
||||
'ui.enable_message_rating': 'ENABLE_MESSAGE_RATING',
|
||||
'ui.enable_password_change_form': 'ENABLE_PASSWORD_CHANGE_FORM',
|
||||
'ui.enable_signup': 'ENABLE_SIGNUP',
|
||||
'ui.enable_user_webhooks': 'ENABLE_USER_WEBHOOKS',
|
||||
'ui.model_order_list': 'MODEL_ORDER_LIST',
|
||||
'ui.pending_user_overlay_content': 'PENDING_USER_OVERLAY_CONTENT',
|
||||
'ui.pending_user_overlay_title': 'PENDING_USER_OVERLAY_TITLE',
|
||||
'ui.prompt_suggestions': 'DEFAULT_PROMPT_SUGGESTIONS',
|
||||
'ui.watermark': 'RESPONSE_WATERMARK',
|
||||
'user.permissions': 'USER_PERMISSIONS',
|
||||
'users.enable_status': 'ENABLE_USER_STATUS',
|
||||
'webhook_url': 'WEBHOOK_URL',
|
||||
'webui.url': 'WEBUI_URL',
|
||||
}
|
||||
|
||||
|
||||
STORAGE_KEY_REWRITES = {
|
||||
'oauth.refresh_token_include_scope': 'oauth.refresh_token.include_scope',
|
||||
'rag.openai_api_base_url': 'rag.openai.api_base_url',
|
||||
'rag.openai_api_key': 'rag.openai.api_key',
|
||||
'rag.ollama.url': 'rag.ollama.base_url',
|
||||
'rag.ollama.key': 'rag.ollama.api_key',
|
||||
'oauth.oidc.avatar_claim': 'oauth.picture_claim',
|
||||
'oauth.oidc.client_id': 'oauth.client_id',
|
||||
'oauth.oidc.client_secret': 'oauth.client_secret',
|
||||
'oauth.oidc.code_challenge_method': 'oauth.code_challenge_method',
|
||||
'oauth.oidc.email_claim': 'oauth.email_claim',
|
||||
'oauth.oidc.end_session_endpoint': 'oauth.end_session_endpoint',
|
||||
'oauth.oidc.group_claim': 'oauth.group_claim',
|
||||
'oauth.oidc.oauth_timeout': 'oauth.timeout',
|
||||
'oauth.oidc.provider_name': 'oauth.provider_name',
|
||||
'oauth.oidc.provider_url': 'oauth.provider_url',
|
||||
'oauth.oidc.redirect_uri': 'oauth.redirect_uri',
|
||||
'oauth.oidc.scopes': 'oauth.scopes',
|
||||
'oauth.oidc.sub_claim': 'oauth.sub_claim',
|
||||
'oauth.oidc.token_endpoint_auth_method': 'oauth.token_endpoint_auth_method',
|
||||
'oauth.oidc.username_claim': 'oauth.username_claim',
|
||||
}
|
||||
|
||||
|
||||
LEGACY_KEY_TO_STORAGE_KEY = {
|
||||
legacy_key: STORAGE_KEY_REWRITES.get(blob_path, blob_path) for blob_path, legacy_key in BLOB_PATH_TO_KEY.items()
|
||||
}
|
||||
|
||||
|
||||
def _walk_blob(data: dict, prefix: str = '') -> dict:
|
||||
"""Recursively walk a nested config blob, preserving known config values.
|
||||
|
||||
Some config values are intentionally dictionaries, e.g. OPENAI_API_CONFIGS
|
||||
and OLLAMA_API_CONFIGS. Once the current path is a known config key, keep
|
||||
that value intact instead of flattening its internals into orphaned rows.
|
||||
"""
|
||||
result = {}
|
||||
for key, value in data.items():
|
||||
path = f'{prefix}{key}' if not prefix else f'{prefix}.{key}'
|
||||
if path in BLOB_PATH_TO_KEY or path in LEGACY_KEY_TO_STORAGE_KEY:
|
||||
result[path] = value
|
||||
elif isinstance(value, dict):
|
||||
result.update(_walk_blob(value, path))
|
||||
else:
|
||||
result[path] = value
|
||||
return result
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Reshape config from single-row JSON blob to per-key rows."""
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
table_names = set(inspector.get_table_names())
|
||||
config_columns = (
|
||||
{column['name'] for column in inspector.get_columns('config')} if 'config' in table_names else set()
|
||||
)
|
||||
has_old_config = {'id', 'data'}.issubset(config_columns)
|
||||
has_new_config = {'key', 'value'}.issubset(config_columns)
|
||||
|
||||
# Ad-hoc table reference for reading the old schema
|
||||
old_config = sa.table(
|
||||
'config',
|
||||
sa.column('id', sa.Integer),
|
||||
sa.column('data', sa.JSON),
|
||||
)
|
||||
|
||||
# 1. Read existing blob
|
||||
blob_data = {}
|
||||
if has_old_config:
|
||||
try:
|
||||
result = conn.execute(sa.select(old_config.c.data).order_by(old_config.c.id.desc()).limit(1))
|
||||
row = result.fetchone()
|
||||
if row and row[0]:
|
||||
raw = row[0]
|
||||
blob_data = json.loads(raw) if isinstance(raw, str) else raw
|
||||
except Exception:
|
||||
pass # Table might be partially migrated or empty
|
||||
|
||||
# 2. Preserve old blob table for rollback/inspection, then create per-key table.
|
||||
if has_old_config:
|
||||
if 'config_old' in table_names:
|
||||
op.drop_table('config_old')
|
||||
op.rename_table('config', 'config_old')
|
||||
|
||||
# 3. Create new per-key table
|
||||
new_config = (
|
||||
sa.table(
|
||||
'config',
|
||||
sa.column('key', sa.Text),
|
||||
sa.column('value', sa.JSON()),
|
||||
sa.column('updated_at', sa.BigInteger),
|
||||
)
|
||||
if has_new_config
|
||||
else op.create_table(
|
||||
'config',
|
||||
sa.Column('key', sa.Text(), primary_key=True),
|
||||
sa.Column('value', sa.JSON(), nullable=False),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=True),
|
||||
)
|
||||
)
|
||||
|
||||
# 4. Flatten blob and insert per-key rows
|
||||
if blob_data:
|
||||
flat = _walk_blob(blob_data)
|
||||
|
||||
# Keep stable dot-notation paths as the database keys.
|
||||
# Known legacy env-style keys are rewritten to their dotted keys; unknown
|
||||
# keys are still copied so custom/future config is not silently lost.
|
||||
rows = {}
|
||||
for blob_path, value in flat.items():
|
||||
if blob_path in BLOB_PATH_TO_KEY:
|
||||
storage_key = STORAGE_KEY_REWRITES.get(blob_path, blob_path)
|
||||
elif blob_path in LEGACY_KEY_TO_STORAGE_KEY:
|
||||
storage_key = LEGACY_KEY_TO_STORAGE_KEY[blob_path]
|
||||
else:
|
||||
storage_key = STORAGE_KEY_REWRITES.get(blob_path, blob_path)
|
||||
|
||||
if storage_key not in rows:
|
||||
rows[storage_key] = value
|
||||
|
||||
# Batch insert via SQLAlchemy table reference
|
||||
if rows:
|
||||
now = int(time.time())
|
||||
op.bulk_insert(
|
||||
new_config,
|
||||
[{'key': k, 'value': v, 'updated_at': now} for k, v in rows.items()],
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Restore preserved old single-row config table when available."""
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
table_names = set(inspector.get_table_names())
|
||||
|
||||
if 'config_old' in table_names:
|
||||
if 'config' in table_names:
|
||||
op.drop_table('config')
|
||||
op.rename_table('config_old', 'config')
|
||||
return
|
||||
|
||||
config_columns = (
|
||||
{column['name'] for column in inspector.get_columns('config')} if 'config' in table_names else set()
|
||||
)
|
||||
has_per_key_config = {'key', 'value'}.issubset(config_columns)
|
||||
|
||||
blob_data = {}
|
||||
if has_per_key_config:
|
||||
config = sa.table(
|
||||
'config',
|
||||
sa.column('key', sa.Text),
|
||||
sa.column('value', sa.JSON),
|
||||
)
|
||||
for key, value in conn.execute(sa.select(config.c.key, config.c.value)):
|
||||
blob_data[key] = json.loads(value) if isinstance(value, str) else value
|
||||
op.drop_table('config')
|
||||
|
||||
if 'config' in table_names and not has_per_key_config:
|
||||
return
|
||||
|
||||
old_config = op.create_table(
|
||||
'config',
|
||||
sa.Column('id', sa.Integer(), primary_key=True),
|
||||
sa.Column('data', sa.JSON(), nullable=False),
|
||||
sa.Column('version', sa.Integer(), nullable=False, server_default='0'),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.func.now()),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||
)
|
||||
|
||||
if blob_data:
|
||||
op.bulk_insert(old_config, [{'data': blob_data, 'version': 0}])
|
||||
@@ -1,40 +0,0 @@
|
||||
"""add memory path and meta
|
||||
|
||||
Revision ID: 42e2978c7933
|
||||
Revises: 7b3f2a9c1d4e
|
||||
Create Date: 2026-06-29 05:35:50.565887
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision: str = '42e2978c7933'
|
||||
down_revision: Union[str, None] = '7b3f2a9c1d4e'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
columns = {column['name'] for column in inspector.get_columns('memory')}
|
||||
|
||||
if 'path' not in columns:
|
||||
op.add_column('memory', sa.Column('path', sa.Text(), nullable=True))
|
||||
if 'meta' not in columns:
|
||||
op.add_column('memory', sa.Column('meta', sa.JSON(), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
columns = {column['name'] for column in inspector.get_columns('memory')}
|
||||
|
||||
if 'meta' in columns:
|
||||
op.drop_column('memory', 'meta')
|
||||
if 'path' in columns:
|
||||
op.drop_column('memory', 'path')
|
||||
-74
@@ -1,74 +0,0 @@
|
||||
"""add missing primary keys to legacy peewee tables
|
||||
|
||||
Revision ID: 461111b60977
|
||||
Revises: 3c9b0ca343fd
|
||||
Create Date: 2026-05-14 04:38:14.000000
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '461111b60977'
|
||||
down_revision: Union[str, None] = '3c9b0ca343fd'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
# Tables bootstrapped by the old Peewee migration layer that may have
|
||||
# UNIQUE(id) but no PRIMARY KEY constraint. Fresh Alembic installs
|
||||
# already have correct PKs from 7e5b5dc7342b_init.py.
|
||||
# 'tag' uses a composite PK since the same tag name can exist for multiple users.
|
||||
LEGACY_TABLES = {
|
||||
'auth': ['id'],
|
||||
'chat': ['id'],
|
||||
'chatidtag': ['id'],
|
||||
'document': ['id'],
|
||||
'file': ['id'],
|
||||
'function': ['id'],
|
||||
'memory': ['id'],
|
||||
'model': ['id'],
|
||||
'prompt': ['id'],
|
||||
'tag': ['id', 'user_id'],
|
||||
'tool': ['id'],
|
||||
'user': ['id'],
|
||||
}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
existing_tables = set(inspector.get_table_names())
|
||||
|
||||
for table_name, pk_columns in LEGACY_TABLES.items():
|
||||
if table_name not in existing_tables:
|
||||
continue
|
||||
|
||||
pk = inspector.get_pk_constraint(table_name)
|
||||
pk_cols = pk.get('constrained_columns', [])
|
||||
|
||||
# Already has the correct PK — nothing to do
|
||||
if sorted(pk_cols) == sorted(pk_columns):
|
||||
continue
|
||||
|
||||
# Check that all PK columns exist
|
||||
columns = {c['name'] for c in inspector.get_columns(table_name)}
|
||||
if not all(c in columns for c in pk_columns):
|
||||
continue
|
||||
|
||||
print(f"Promoting UNIQUE(id) -> PRIMARY KEY({', '.join(pk_columns)}) for '{table_name}'")
|
||||
|
||||
conn.execute(sa.text(f'DROP TABLE IF EXISTS _alembic_tmp_{table_name}'))
|
||||
with op.batch_alter_table(table_name) as batch_op:
|
||||
# Drop existing PK if any (e.g. on wrong column)
|
||||
if pk_cols and pk.get('name'):
|
||||
batch_op.drop_constraint(pk['name'], type_='primary')
|
||||
|
||||
batch_op.create_primary_key(f'pk_{table_name}', pk_columns)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Downgrade is a no-op — we don't want to remove PKs
|
||||
pass
|
||||
+5
-21
@@ -6,8 +6,8 @@ Create Date: 2024-10-23 03:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = '4ace53fd72c8'
|
||||
down_revision = 'af906e964978'
|
||||
@@ -16,18 +16,6 @@ depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
columns = {c['name']: c for c in inspector.get_columns('folder')}
|
||||
|
||||
created_at_col = columns.get('created_at')
|
||||
if not created_at_col:
|
||||
return
|
||||
|
||||
# Only convert if still DateTime — skip if already BigInteger
|
||||
if isinstance(created_at_col['type'], sa.BigInteger):
|
||||
return
|
||||
|
||||
# Perform safe alterations using batch operation
|
||||
with op.batch_alter_table('folder', schema=None) as batch_op:
|
||||
# Step 1: Remove server defaults for created_at and updated_at
|
||||
@@ -60,24 +48,20 @@ def upgrade():
|
||||
|
||||
|
||||
def downgrade():
|
||||
# Convert columns back to DateTime and restore defaults. Mirrors the
|
||||
# upgrade's postgresql_using cast — without it, Postgres can't
|
||||
# auto-cast BigInteger → timestamp and aborts with DatatypeMismatch.
|
||||
# Downgrade: Convert columns back to DateTime and restore defaults
|
||||
with op.batch_alter_table('folder', schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
'created_at',
|
||||
type_=sa.DateTime(),
|
||||
existing_type=sa.BigInteger(),
|
||||
existing_nullable=False,
|
||||
server_default=sa.func.now(),
|
||||
postgresql_using='to_timestamp(created_at)::timestamp without time zone',
|
||||
server_default=sa.func.now(), # Restoring server default on downgrade
|
||||
)
|
||||
batch_op.alter_column(
|
||||
'updated_at',
|
||||
type_=sa.DateTime(),
|
||||
existing_type=sa.BigInteger(),
|
||||
existing_nullable=False,
|
||||
server_default=sa.func.now(),
|
||||
onupdate=sa.func.now(),
|
||||
postgresql_using='to_timestamp(updated_at)::timestamp without time zone',
|
||||
server_default=sa.func.now(), # Restoring server default on downgrade
|
||||
onupdate=sa.func.now(), # Restoring onupdate behavior if it was there
|
||||
)
|
||||
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
"""add context summary to chat message
|
||||
|
||||
Revision ID: 4c5ce3d2f27f
|
||||
Revises: 3ff2c63645b8
|
||||
Create Date: 2026-06-18 23:48:08.310063
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '4c5ce3d2f27f'
|
||||
down_revision: Union[str, None] = '3ff2c63645b8'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
columns = {column['name'] for column in inspector.get_columns('chat_message')}
|
||||
|
||||
if 'context_summary' not in columns:
|
||||
op.add_column('chat_message', sa.Column('context_summary', sa.Text(), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
columns = {column['name'] for column in inspector.get_columns('chat_message')}
|
||||
|
||||
if 'context_summary' in columns:
|
||||
op.drop_column('chat_message', 'context_summary')
|
||||
@@ -1,88 +0,0 @@
|
||||
"""add pinned_note table
|
||||
|
||||
Revision ID: 4de81c2a3af1
|
||||
Revises: 56359461a091
|
||||
Create Date: 2026-05-09 04:29:27.651341
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import open_webui.internal.db
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '4de81c2a3af1'
|
||||
down_revision: Union[str, None] = '56359461a091'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import insert, select, update
|
||||
from sqlalchemy.sql import column, table
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
existing_tables = set(inspector.get_table_names())
|
||||
|
||||
if 'pinned_note' not in existing_tables:
|
||||
op.create_table(
|
||||
'pinned_note',
|
||||
sa.Column('id', sa.Text(), nullable=False),
|
||||
sa.Column('user_id', sa.Text(), nullable=False),
|
||||
sa.Column('note_id', sa.Text(), sa.ForeignKey('note.id', ondelete='CASCADE'), nullable=False),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('user_id', 'note_id', name='uq_pinned_note'),
|
||||
)
|
||||
|
||||
note_table = table('note', column('id', sa.Text), column('user_id', sa.Text), column('is_pinned', sa.Boolean))
|
||||
|
||||
pinned_note_table = table(
|
||||
'pinned_note',
|
||||
column('id', sa.Text),
|
||||
column('user_id', sa.Text),
|
||||
column('note_id', sa.Text),
|
||||
column('created_at', sa.BigInteger),
|
||||
)
|
||||
|
||||
notes = conn.execute(
|
||||
select(note_table.c.id, note_table.c.user_id).where(note_table.c.is_pinned == True)
|
||||
).fetchall()
|
||||
|
||||
if notes:
|
||||
now = int(time.time_ns())
|
||||
conn.execute(
|
||||
insert(pinned_note_table),
|
||||
[
|
||||
{'id': str(uuid.uuid4()), 'user_id': note[1], 'note_id': note[0], 'created_at': now}
|
||||
for note in notes
|
||||
],
|
||||
)
|
||||
|
||||
with op.batch_alter_table('note', schema=None) as batch_op:
|
||||
batch_op.drop_column('is_pinned')
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
with op.batch_alter_table('note', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('is_pinned', sa.Boolean(), nullable=True))
|
||||
|
||||
conn = op.get_bind()
|
||||
|
||||
note_table = table('note', column('id', sa.Text), column('is_pinned', sa.Boolean))
|
||||
|
||||
pinned_note_table = table('pinned_note', column('note_id', sa.Text))
|
||||
|
||||
notes = conn.execute(select(pinned_note_table.c.note_id)).fetchall()
|
||||
|
||||
for note in notes:
|
||||
conn.execute(update(note_table).where(note_table.c.id == note[0]).values(is_pinned=True))
|
||||
|
||||
op.drop_table('pinned_note')
|
||||
@@ -8,8 +8,9 @@ Create Date: 2026-04-19 16:20:58.162045
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '56359461a091'
|
||||
@@ -18,86 +19,58 @@ branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def _index_exists(inspector, index_name, table_name):
|
||||
"""Check if an index already exists on the given table."""
|
||||
indexes = inspector.get_indexes(table_name)
|
||||
return any(idx['name'] == index_name for idx in indexes)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
tables = inspector.get_table_names()
|
||||
op.create_table(
|
||||
'calendar',
|
||||
sa.Column('id', sa.Text(), nullable=False),
|
||||
sa.Column('user_id', sa.Text(), nullable=False),
|
||||
sa.Column('name', sa.Text(), nullable=False),
|
||||
sa.Column('color', sa.Text(), nullable=True),
|
||||
sa.Column('is_default', sa.Boolean(), nullable=False),
|
||||
sa.Column('data', sa.JSON(), nullable=True),
|
||||
sa.Column('meta', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=False),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
)
|
||||
op.create_index('ix_calendar_user', 'calendar', ['user_id'], unique=False)
|
||||
|
||||
if 'calendar' not in tables:
|
||||
op.create_table(
|
||||
'calendar',
|
||||
sa.Column('id', sa.Text(), nullable=False),
|
||||
sa.Column('user_id', sa.Text(), nullable=False),
|
||||
sa.Column('name', sa.Text(), nullable=False),
|
||||
sa.Column('color', sa.Text(), nullable=True),
|
||||
sa.Column('is_default', sa.Boolean(), nullable=False),
|
||||
sa.Column('data', sa.JSON(), nullable=True),
|
||||
sa.Column('meta', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=False),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
)
|
||||
op.create_table(
|
||||
'calendar_event',
|
||||
sa.Column('id', sa.Text(), nullable=False),
|
||||
sa.Column('calendar_id', sa.Text(), nullable=False),
|
||||
sa.Column('user_id', sa.Text(), nullable=False),
|
||||
sa.Column('title', sa.Text(), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('start_at', sa.BigInteger(), nullable=False),
|
||||
sa.Column('end_at', sa.BigInteger(), nullable=True),
|
||||
sa.Column('all_day', sa.Boolean(), nullable=False),
|
||||
sa.Column('rrule', sa.Text(), nullable=True),
|
||||
sa.Column('color', sa.Text(), nullable=True),
|
||||
sa.Column('location', sa.Text(), nullable=True),
|
||||
sa.Column('data', sa.JSON(), nullable=True),
|
||||
sa.Column('meta', sa.JSON(), nullable=True),
|
||||
sa.Column('is_cancelled', sa.Boolean(), nullable=False),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=False),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
)
|
||||
op.create_index('ix_calendar_event_calendar', 'calendar_event', ['calendar_id', 'start_at'], unique=False)
|
||||
op.create_index('ix_calendar_event_user_date', 'calendar_event', ['user_id', 'start_at'], unique=False)
|
||||
|
||||
inspector.clear_cache()
|
||||
if 'calendar' in inspector.get_table_names():
|
||||
if not _index_exists(inspector, 'ix_calendar_user', 'calendar'):
|
||||
op.create_index('ix_calendar_user', 'calendar', ['user_id'], unique=False)
|
||||
|
||||
if 'calendar_event' not in tables:
|
||||
op.create_table(
|
||||
'calendar_event',
|
||||
sa.Column('id', sa.Text(), nullable=False),
|
||||
sa.Column('calendar_id', sa.Text(), nullable=False),
|
||||
sa.Column('user_id', sa.Text(), nullable=False),
|
||||
sa.Column('title', sa.Text(), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('start_at', sa.BigInteger(), nullable=False),
|
||||
sa.Column('end_at', sa.BigInteger(), nullable=True),
|
||||
sa.Column('all_day', sa.Boolean(), nullable=False),
|
||||
sa.Column('rrule', sa.Text(), nullable=True),
|
||||
sa.Column('color', sa.Text(), nullable=True),
|
||||
sa.Column('location', sa.Text(), nullable=True),
|
||||
sa.Column('data', sa.JSON(), nullable=True),
|
||||
sa.Column('meta', sa.JSON(), nullable=True),
|
||||
sa.Column('is_cancelled', sa.Boolean(), nullable=False),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=False),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
)
|
||||
|
||||
inspector.clear_cache()
|
||||
if 'calendar_event' in inspector.get_table_names():
|
||||
if not _index_exists(inspector, 'ix_calendar_event_calendar', 'calendar_event'):
|
||||
op.create_index('ix_calendar_event_calendar', 'calendar_event', ['calendar_id', 'start_at'], unique=False)
|
||||
if not _index_exists(inspector, 'ix_calendar_event_user_date', 'calendar_event'):
|
||||
op.create_index('ix_calendar_event_user_date', 'calendar_event', ['user_id', 'start_at'], unique=False)
|
||||
|
||||
if 'calendar_event_attendee' not in tables:
|
||||
op.create_table(
|
||||
'calendar_event_attendee',
|
||||
sa.Column('id', sa.Text(), nullable=False),
|
||||
sa.Column('event_id', sa.Text(), nullable=False),
|
||||
sa.Column('user_id', sa.Text(), nullable=False),
|
||||
sa.Column('status', sa.Text(), nullable=False),
|
||||
sa.Column('meta', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=False),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('event_id', 'user_id', name='uq_event_attendee'),
|
||||
)
|
||||
|
||||
inspector.clear_cache()
|
||||
if 'calendar_event_attendee' in inspector.get_table_names():
|
||||
if not _index_exists(inspector, 'ix_calendar_event_attendee_user', 'calendar_event_attendee'):
|
||||
op.create_index(
|
||||
'ix_calendar_event_attendee_user', 'calendar_event_attendee', ['user_id', 'status'], unique=False
|
||||
)
|
||||
op.create_table(
|
||||
'calendar_event_attendee',
|
||||
sa.Column('id', sa.Text(), nullable=False),
|
||||
sa.Column('event_id', sa.Text(), nullable=False),
|
||||
sa.Column('user_id', sa.Text(), nullable=False),
|
||||
sa.Column('status', sa.Text(), nullable=False),
|
||||
sa.Column('meta', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=False),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('event_id', 'user_id', name='uq_event_attendee'),
|
||||
)
|
||||
op.create_index('ix_calendar_event_attendee_user', 'calendar_event_attendee', ['user_id', 'status'], unique=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
@@ -6,8 +6,8 @@ Create Date: 2024-12-22 03:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = '57c599a3cb57'
|
||||
down_revision = '922e7a387820'
|
||||
@@ -16,36 +16,30 @@ depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
existing_tables = set(inspector.get_table_names())
|
||||
op.create_table(
|
||||
'channel',
|
||||
sa.Column('id', sa.Text(), nullable=False, primary_key=True, unique=True),
|
||||
sa.Column('user_id', sa.Text()),
|
||||
sa.Column('name', sa.Text()),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('data', sa.JSON(), nullable=True),
|
||||
sa.Column('meta', sa.JSON(), nullable=True),
|
||||
sa.Column('access_control', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=True),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=True),
|
||||
)
|
||||
|
||||
if 'channel' not in existing_tables:
|
||||
op.create_table(
|
||||
'channel',
|
||||
sa.Column('id', sa.Text(), nullable=False, primary_key=True, unique=True),
|
||||
sa.Column('user_id', sa.Text()),
|
||||
sa.Column('name', sa.Text()),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('data', sa.JSON(), nullable=True),
|
||||
sa.Column('meta', sa.JSON(), nullable=True),
|
||||
sa.Column('access_control', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=True),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=True),
|
||||
)
|
||||
|
||||
if 'message' not in existing_tables:
|
||||
op.create_table(
|
||||
'message',
|
||||
sa.Column('id', sa.Text(), nullable=False, primary_key=True, unique=True),
|
||||
sa.Column('user_id', sa.Text()),
|
||||
sa.Column('channel_id', sa.Text(), nullable=True),
|
||||
sa.Column('content', sa.Text()),
|
||||
sa.Column('data', sa.JSON(), nullable=True),
|
||||
sa.Column('meta', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=True),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=True),
|
||||
)
|
||||
op.create_table(
|
||||
'message',
|
||||
sa.Column('id', sa.Text(), nullable=False, primary_key=True, unique=True),
|
||||
sa.Column('user_id', sa.Text()),
|
||||
sa.Column('channel_id', sa.Text(), nullable=True),
|
||||
sa.Column('content', sa.Text()),
|
||||
sa.Column('data', sa.JSON(), nullable=True),
|
||||
sa.Column('meta', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=True),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=True),
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
|
||||
@@ -8,9 +8,9 @@ Create Date: 2025-12-10 15:11:39.424601
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import open_webui.internal.db
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import open_webui.internal.db
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '6283dc0e4d8d'
|
||||
@@ -20,38 +20,31 @@ depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
existing_tables = set(inspector.get_table_names())
|
||||
|
||||
if 'channel_file' not in existing_tables:
|
||||
op.create_table(
|
||||
'channel_file',
|
||||
sa.Column('id', sa.Text(), primary_key=True),
|
||||
sa.Column('user_id', sa.Text(), nullable=False),
|
||||
sa.Column(
|
||||
'channel_id',
|
||||
sa.Text(),
|
||||
sa.ForeignKey('channel.id', ondelete='CASCADE'),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
'file_id',
|
||||
sa.Text(),
|
||||
sa.ForeignKey('file.id', ondelete='CASCADE'),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=False),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=False),
|
||||
# indexes
|
||||
sa.Index('ix_channel_file_channel_id', 'channel_id'),
|
||||
sa.Index('ix_channel_file_file_id', 'file_id'),
|
||||
sa.Index('ix_channel_file_user_id', 'user_id'),
|
||||
# unique constraints
|
||||
sa.UniqueConstraint(
|
||||
'channel_id', 'file_id', name='uq_channel_file_channel_file'
|
||||
), # prevent duplicate entries
|
||||
)
|
||||
op.create_table(
|
||||
'channel_file',
|
||||
sa.Column('id', sa.Text(), primary_key=True),
|
||||
sa.Column('user_id', sa.Text(), nullable=False),
|
||||
sa.Column(
|
||||
'channel_id',
|
||||
sa.Text(),
|
||||
sa.ForeignKey('channel.id', ondelete='CASCADE'),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
'file_id',
|
||||
sa.Text(),
|
||||
sa.ForeignKey('file.id', ondelete='CASCADE'),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=False),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=False),
|
||||
# indexes
|
||||
sa.Index('ix_channel_file_channel_id', 'channel_id'),
|
||||
sa.Index('ix_channel_file_file_id', 'file_id'),
|
||||
sa.Index('ix_channel_file_user_id', 'user_id'),
|
||||
# unique constraints
|
||||
sa.UniqueConstraint('channel_id', 'file_id', name='uq_channel_file_channel_file'), # prevent duplicate entries
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
@@ -6,11 +6,10 @@ Create Date: 2024-10-01 14:02:35.241684
|
||||
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.sql import column, select, table
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.sql import table, column, select
|
||||
import json
|
||||
|
||||
revision = '6a39f3d8e55c'
|
||||
down_revision = 'c0fbf31ca0db'
|
||||
@@ -19,67 +18,62 @@ depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
existing_tables = set(inspector.get_table_names())
|
||||
# Creating the 'knowledge' table
|
||||
print('Creating knowledge table')
|
||||
knowledge_table = op.create_table(
|
||||
'knowledge',
|
||||
sa.Column('id', sa.Text(), primary_key=True),
|
||||
sa.Column('user_id', sa.Text(), nullable=False),
|
||||
sa.Column('name', sa.Text(), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('data', sa.JSON(), nullable=True),
|
||||
sa.Column('meta', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=False),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=True),
|
||||
)
|
||||
|
||||
if 'knowledge' not in existing_tables:
|
||||
# Creating the 'knowledge' table
|
||||
print('Creating knowledge table')
|
||||
knowledge_table = op.create_table(
|
||||
'knowledge',
|
||||
sa.Column('id', sa.Text(), primary_key=True),
|
||||
sa.Column('user_id', sa.Text(), nullable=False),
|
||||
sa.Column('name', sa.Text(), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('data', sa.JSON(), nullable=True),
|
||||
sa.Column('meta', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=False),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=True),
|
||||
print('Migrating data from document table to knowledge table')
|
||||
# Representation of the existing 'document' table
|
||||
document_table = table(
|
||||
'document',
|
||||
column('collection_name', sa.String()),
|
||||
column('user_id', sa.String()),
|
||||
column('name', sa.String()),
|
||||
column('title', sa.Text()),
|
||||
column('content', sa.Text()),
|
||||
column('timestamp', sa.BigInteger()),
|
||||
)
|
||||
|
||||
# Select all from existing document table
|
||||
documents = op.get_bind().execute(
|
||||
select(
|
||||
document_table.c.collection_name,
|
||||
document_table.c.user_id,
|
||||
document_table.c.name,
|
||||
document_table.c.title,
|
||||
document_table.c.content,
|
||||
document_table.c.timestamp,
|
||||
)
|
||||
)
|
||||
|
||||
print('Migrating data from document table to knowledge table')
|
||||
# Representation of the existing 'document' table
|
||||
document_table = table(
|
||||
'document',
|
||||
column('collection_name', sa.String()),
|
||||
column('user_id', sa.String()),
|
||||
column('name', sa.String()),
|
||||
column('title', sa.Text()),
|
||||
column('content', sa.Text()),
|
||||
column('timestamp', sa.BigInteger()),
|
||||
)
|
||||
|
||||
# Select all from existing document table
|
||||
documents = conn.execute(
|
||||
select(
|
||||
document_table.c.collection_name,
|
||||
document_table.c.user_id,
|
||||
document_table.c.name,
|
||||
document_table.c.title,
|
||||
document_table.c.content,
|
||||
document_table.c.timestamp,
|
||||
# Insert data into knowledge table from document table
|
||||
for doc in documents:
|
||||
op.get_bind().execute(
|
||||
knowledge_table.insert().values(
|
||||
id=doc.collection_name,
|
||||
user_id=doc.user_id,
|
||||
description=doc.name,
|
||||
meta={
|
||||
'legacy': True,
|
||||
'document': True,
|
||||
'tags': json.loads(doc.content or '{}').get('tags', []),
|
||||
},
|
||||
name=doc.title,
|
||||
created_at=doc.timestamp,
|
||||
updated_at=doc.timestamp, # using created_at for both created_at and updated_at in project
|
||||
)
|
||||
)
|
||||
|
||||
# Insert data into knowledge table from document table
|
||||
for doc in documents:
|
||||
conn.execute(
|
||||
knowledge_table.insert().values(
|
||||
id=doc.collection_name,
|
||||
user_id=doc.user_id,
|
||||
description=doc.name,
|
||||
meta={
|
||||
'legacy': True,
|
||||
'document': True,
|
||||
'tags': json.loads(doc.content or '{}').get('tags', []),
|
||||
},
|
||||
name=doc.title,
|
||||
created_at=doc.timestamp,
|
||||
updated_at=doc.timestamp,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_table('knowledge')
|
||||
|
||||
@@ -6,8 +6,8 @@ Create Date: 2024-12-23 03:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = '7826ab40b532'
|
||||
down_revision = '57c599a3cb57'
|
||||
@@ -16,15 +16,10 @@ depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
file_cols = {c['name'] for c in inspector.get_columns('file')}
|
||||
|
||||
if 'access_control' not in file_cols:
|
||||
op.add_column(
|
||||
'file',
|
||||
sa.Column('access_control', sa.JSON(), nullable=True),
|
||||
)
|
||||
op.add_column(
|
||||
'file',
|
||||
sa.Column('access_control', sa.JSON(), nullable=True),
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
"""add memory type
|
||||
|
||||
Revision ID: 7b3f2a9c1d4e
|
||||
Revises: 4c5ce3d2f27f
|
||||
Create Date: 2026-06-25 00:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision: str = '7b3f2a9c1d4e'
|
||||
down_revision: Union[str, None] = '4c5ce3d2f27f'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
columns = {column['name'] for column in inspector.get_columns('memory')}
|
||||
indexes = {index['name'] for index in inspector.get_indexes('memory')}
|
||||
|
||||
if 'type' not in columns:
|
||||
op.add_column('memory', sa.Column('type', sa.String(), server_default='context', nullable=False))
|
||||
|
||||
if 'ix_memory_type' not in indexes:
|
||||
op.create_index('ix_memory_type', 'memory', ['type'])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
columns = {column['name'] for column in inspector.get_columns('memory')}
|
||||
indexes = {index['name'] for index in inspector.get_indexes('memory')}
|
||||
|
||||
if 'ix_memory_type' in indexes:
|
||||
op.drop_index('ix_memory_type', table_name='memory')
|
||||
|
||||
if 'type' in columns:
|
||||
op.drop_column('memory', 'type')
|
||||
@@ -1,34 +1,44 @@
|
||||
# Initial bootstrap migration version.
|
||||
# Revision ID: 7e5b5dc7342b
|
||||
# Revises: (none)
|
||||
# Created on: 2024-06-24 13:15:33.808998
|
||||
from __future__ import annotations
|
||||
from typing import Sequence
|
||||
import open_webui.internal.db # noqa: F401
|
||||
"""init
|
||||
|
||||
Revision ID: 7e5b5dc7342b
|
||||
Revises:
|
||||
Create Date: 2024-06-24 13:15:33.808998
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
import open_webui.internal.db
|
||||
from open_webui.internal.db import JSONField
|
||||
from open_webui.migrations.util import get_existing_tables
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '7e5b5dc7342b'
|
||||
down_revision: str | None = None
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
# Initial schema table declarations
|
||||
_INITIAL_TABLES: list[tuple[str, list[sa.Column], list]] = [
|
||||
(
|
||||
'auth',
|
||||
[
|
||||
down_revision: Union[str, None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
existing_tables = set(get_existing_tables())
|
||||
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
if 'auth' not in existing_tables:
|
||||
op.create_table(
|
||||
'auth',
|
||||
sa.Column('id', sa.String(), nullable=False),
|
||||
sa.Column('email', sa.String(), nullable=True),
|
||||
sa.Column('password', sa.Text(), nullable=True),
|
||||
sa.Column('active', sa.Boolean(), nullable=True),
|
||||
],
|
||||
[sa.PrimaryKeyConstraint('id')],
|
||||
),
|
||||
(
|
||||
'chat',
|
||||
[
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
)
|
||||
|
||||
if 'chat' not in existing_tables:
|
||||
op.create_table(
|
||||
'chat',
|
||||
sa.Column('id', sa.String(), nullable=False),
|
||||
sa.Column('user_id', sa.String(), nullable=True),
|
||||
sa.Column('title', sa.Text(), nullable=True),
|
||||
@@ -37,23 +47,24 @@ _INITIAL_TABLES: list[tuple[str, list[sa.Column], list]] = [
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=True),
|
||||
sa.Column('share_id', sa.Text(), nullable=True),
|
||||
sa.Column('archived', sa.Boolean(), nullable=True),
|
||||
],
|
||||
[sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('share_id')],
|
||||
),
|
||||
(
|
||||
'chatidtag',
|
||||
[
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('share_id'),
|
||||
)
|
||||
|
||||
if 'chatidtag' not in existing_tables:
|
||||
op.create_table(
|
||||
'chatidtag',
|
||||
sa.Column('id', sa.String(), nullable=False),
|
||||
sa.Column('tag_name', sa.String(), nullable=True),
|
||||
sa.Column('chat_id', sa.String(), nullable=True),
|
||||
sa.Column('user_id', sa.String(), nullable=True),
|
||||
sa.Column('timestamp', sa.BigInteger(), nullable=True),
|
||||
],
|
||||
[sa.PrimaryKeyConstraint('id')],
|
||||
),
|
||||
(
|
||||
'document',
|
||||
[
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
)
|
||||
|
||||
if 'document' not in existing_tables:
|
||||
op.create_table(
|
||||
'document',
|
||||
sa.Column('collection_name', sa.String(), nullable=False),
|
||||
sa.Column('name', sa.String(), nullable=True),
|
||||
sa.Column('title', sa.Text(), nullable=True),
|
||||
@@ -61,23 +72,24 @@ _INITIAL_TABLES: list[tuple[str, list[sa.Column], list]] = [
|
||||
sa.Column('content', sa.Text(), nullable=True),
|
||||
sa.Column('user_id', sa.String(), nullable=True),
|
||||
sa.Column('timestamp', sa.BigInteger(), nullable=True),
|
||||
],
|
||||
[sa.PrimaryKeyConstraint('collection_name'), sa.UniqueConstraint('name')],
|
||||
),
|
||||
(
|
||||
'file',
|
||||
[
|
||||
sa.PrimaryKeyConstraint('collection_name'),
|
||||
sa.UniqueConstraint('name'),
|
||||
)
|
||||
|
||||
if 'file' not in existing_tables:
|
||||
op.create_table(
|
||||
'file',
|
||||
sa.Column('id', sa.String(), nullable=False),
|
||||
sa.Column('user_id', sa.String(), nullable=True),
|
||||
sa.Column('filename', sa.Text(), nullable=True),
|
||||
sa.Column('meta', JSONField(), nullable=True),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=True),
|
||||
],
|
||||
[sa.PrimaryKeyConstraint('id')],
|
||||
),
|
||||
(
|
||||
'function',
|
||||
[
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
)
|
||||
|
||||
if 'function' not in existing_tables:
|
||||
op.create_table(
|
||||
'function',
|
||||
sa.Column('id', sa.String(), nullable=False),
|
||||
sa.Column('user_id', sa.String(), nullable=True),
|
||||
sa.Column('name', sa.Text(), nullable=True),
|
||||
@@ -89,23 +101,23 @@ _INITIAL_TABLES: list[tuple[str, list[sa.Column], list]] = [
|
||||
sa.Column('is_global', sa.Boolean(), nullable=True),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=True),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=True),
|
||||
],
|
||||
[sa.PrimaryKeyConstraint('id')],
|
||||
),
|
||||
(
|
||||
'memory',
|
||||
[
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
)
|
||||
|
||||
if 'memory' not in existing_tables:
|
||||
op.create_table(
|
||||
'memory',
|
||||
sa.Column('id', sa.String(), nullable=False),
|
||||
sa.Column('user_id', sa.String(), nullable=True),
|
||||
sa.Column('content', sa.Text(), nullable=True),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=True),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=True),
|
||||
],
|
||||
[sa.PrimaryKeyConstraint('id')],
|
||||
),
|
||||
(
|
||||
'model',
|
||||
[
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
)
|
||||
|
||||
if 'model' not in existing_tables:
|
||||
op.create_table(
|
||||
'model',
|
||||
sa.Column('id', sa.Text(), nullable=False),
|
||||
sa.Column('user_id', sa.Text(), nullable=True),
|
||||
sa.Column('base_model_id', sa.Text(), nullable=True),
|
||||
@@ -114,33 +126,33 @@ _INITIAL_TABLES: list[tuple[str, list[sa.Column], list]] = [
|
||||
sa.Column('meta', JSONField(), nullable=True),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=True),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=True),
|
||||
],
|
||||
[sa.PrimaryKeyConstraint('id')],
|
||||
),
|
||||
(
|
||||
'prompt',
|
||||
[
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
)
|
||||
|
||||
if 'prompt' not in existing_tables:
|
||||
op.create_table(
|
||||
'prompt',
|
||||
sa.Column('command', sa.String(), nullable=False),
|
||||
sa.Column('user_id', sa.String(), nullable=True),
|
||||
sa.Column('title', sa.Text(), nullable=True),
|
||||
sa.Column('content', sa.Text(), nullable=True),
|
||||
sa.Column('timestamp', sa.BigInteger(), nullable=True),
|
||||
],
|
||||
[sa.PrimaryKeyConstraint('command')],
|
||||
),
|
||||
(
|
||||
'tag',
|
||||
[
|
||||
sa.PrimaryKeyConstraint('command'),
|
||||
)
|
||||
|
||||
if 'tag' not in existing_tables:
|
||||
op.create_table(
|
||||
'tag',
|
||||
sa.Column('id', sa.String(), nullable=False),
|
||||
sa.Column('name', sa.String(), nullable=True),
|
||||
sa.Column('user_id', sa.String(), nullable=True),
|
||||
sa.Column('data', sa.Text(), nullable=True),
|
||||
],
|
||||
[sa.PrimaryKeyConstraint('id')],
|
||||
),
|
||||
(
|
||||
'tool',
|
||||
[
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
)
|
||||
|
||||
if 'tool' not in existing_tables:
|
||||
op.create_table(
|
||||
'tool',
|
||||
sa.Column('id', sa.String(), nullable=False),
|
||||
sa.Column('user_id', sa.String(), nullable=True),
|
||||
sa.Column('name', sa.Text(), nullable=True),
|
||||
@@ -150,12 +162,12 @@ _INITIAL_TABLES: list[tuple[str, list[sa.Column], list]] = [
|
||||
sa.Column('valves', JSONField(), nullable=True),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=True),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=True),
|
||||
],
|
||||
[sa.PrimaryKeyConstraint('id')],
|
||||
),
|
||||
(
|
||||
'user',
|
||||
[
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
)
|
||||
|
||||
if 'user' not in existing_tables:
|
||||
op.create_table(
|
||||
'user',
|
||||
sa.Column('id', sa.String(), nullable=False),
|
||||
sa.Column('name', sa.String(), nullable=True),
|
||||
sa.Column('email', sa.String(), nullable=True),
|
||||
@@ -168,25 +180,25 @@ _INITIAL_TABLES: list[tuple[str, list[sa.Column], list]] = [
|
||||
sa.Column('settings', JSONField(), nullable=True),
|
||||
sa.Column('info', JSONField(), nullable=True),
|
||||
sa.Column('oauth_sub', sa.Text(), nullable=True),
|
||||
],
|
||||
[
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('api_key'),
|
||||
sa.UniqueConstraint('oauth_sub'),
|
||||
],
|
||||
),
|
||||
]
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
# --- migration execution ---
|
||||
def upgrade() -> None: # deploy initial schema tables
|
||||
existing_tables = set(get_existing_tables())
|
||||
for name, columns, constraints in _INITIAL_TABLES:
|
||||
if name not in existing_tables:
|
||||
op.create_table(name, *columns, *constraints)
|
||||
|
||||
|
||||
# --- rollback function ---
|
||||
def downgrade() -> None: # rollback initial schema tables
|
||||
for table_name, _, _ in reversed(_INITIAL_TABLES):
|
||||
op.drop_table(table_name)
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('user')
|
||||
op.drop_table('tool')
|
||||
op.drop_table('tag')
|
||||
op.drop_table('prompt')
|
||||
op.drop_table('model')
|
||||
op.drop_table('memory')
|
||||
op.drop_table('function')
|
||||
op.drop_table('file')
|
||||
op.drop_table('document')
|
||||
op.drop_table('chatidtag')
|
||||
op.drop_table('chat')
|
||||
op.drop_table('auth')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
+12
-19
@@ -8,9 +8,9 @@ Create Date: 2025-12-10 16:07:58.001282
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import open_webui.internal.db
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import open_webui.internal.db
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '81cc2ce44d79'
|
||||
@@ -20,27 +20,20 @@ depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
|
||||
# Add message_id column to channel_file table
|
||||
cf_cols = {c['name'] for c in inspector.get_columns('channel_file')}
|
||||
if 'message_id' not in cf_cols:
|
||||
with op.batch_alter_table('channel_file', schema=None) as batch_op:
|
||||
batch_op.add_column(
|
||||
sa.Column(
|
||||
'message_id',
|
||||
sa.Text(),
|
||||
sa.ForeignKey('message.id', ondelete='CASCADE', name='fk_channel_file_message_id'),
|
||||
nullable=True,
|
||||
)
|
||||
with op.batch_alter_table('channel_file', schema=None) as batch_op:
|
||||
batch_op.add_column(
|
||||
sa.Column(
|
||||
'message_id',
|
||||
sa.Text(),
|
||||
sa.ForeignKey('message.id', ondelete='CASCADE', name='fk_channel_file_message_id'),
|
||||
nullable=True,
|
||||
)
|
||||
)
|
||||
|
||||
# Add data column to knowledge table
|
||||
k_cols = {c['name'] for c in inspector.get_columns('knowledge')}
|
||||
if 'data' not in k_cols:
|
||||
with op.batch_alter_table('knowledge', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('data', sa.JSON(), nullable=True))
|
||||
with op.batch_alter_table('knowledge', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('data', sa.JSON(), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
@@ -6,13 +6,13 @@ Create Date: 2026-02-01 04:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
import time
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -56,13 +56,6 @@ def _flush_batch(conn, table, batch):
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
existing_tables = set(inspector.get_table_names())
|
||||
|
||||
if 'chat_message' in existing_tables:
|
||||
return # Already created — skip everything
|
||||
|
||||
# Step 1: Create table
|
||||
op.create_table(
|
||||
'chat_message',
|
||||
@@ -92,6 +85,8 @@ def upgrade() -> None:
|
||||
op.create_index('chat_message_user_created_idx', 'chat_message', ['user_id', 'created_at'])
|
||||
|
||||
# Step 2: Backfill from existing chats
|
||||
conn = op.get_bind()
|
||||
|
||||
chat_table = sa.table(
|
||||
'chat',
|
||||
sa.column('id', sa.Text()),
|
||||
|
||||
+35
-44
@@ -8,9 +8,9 @@ Create Date: 2025-11-30 06:33:38.790341
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import open_webui.internal.db
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import open_webui.internal.db
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '90ef40d4714e'
|
||||
@@ -20,53 +20,42 @@ depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
existing_tables = set(inspector.get_table_names())
|
||||
|
||||
# Update 'channel' table
|
||||
channel_cols = {c['name'] for c in inspector.get_columns('channel')}
|
||||
if 'is_private' not in channel_cols:
|
||||
op.add_column('channel', sa.Column('is_private', sa.Boolean(), nullable=True))
|
||||
if 'archived_at' not in channel_cols:
|
||||
op.add_column('channel', sa.Column('archived_at', sa.BigInteger(), nullable=True))
|
||||
if 'archived_by' not in channel_cols:
|
||||
op.add_column('channel', sa.Column('archived_by', sa.Text(), nullable=True))
|
||||
if 'deleted_at' not in channel_cols:
|
||||
op.add_column('channel', sa.Column('deleted_at', sa.BigInteger(), nullable=True))
|
||||
if 'deleted_by' not in channel_cols:
|
||||
op.add_column('channel', sa.Column('deleted_by', sa.Text(), nullable=True))
|
||||
if 'updated_by' not in channel_cols:
|
||||
op.add_column('channel', sa.Column('updated_by', sa.Text(), nullable=True))
|
||||
op.add_column('channel', sa.Column('is_private', sa.Boolean(), nullable=True))
|
||||
|
||||
op.add_column('channel', sa.Column('archived_at', sa.BigInteger(), nullable=True))
|
||||
op.add_column('channel', sa.Column('archived_by', sa.Text(), nullable=True))
|
||||
|
||||
op.add_column('channel', sa.Column('deleted_at', sa.BigInteger(), nullable=True))
|
||||
op.add_column('channel', sa.Column('deleted_by', sa.Text(), nullable=True))
|
||||
|
||||
op.add_column('channel', sa.Column('updated_by', sa.Text(), nullable=True))
|
||||
|
||||
# Update 'channel_member' table
|
||||
cm_cols = {c['name'] for c in inspector.get_columns('channel_member')}
|
||||
if 'role' not in cm_cols:
|
||||
op.add_column('channel_member', sa.Column('role', sa.Text(), nullable=True))
|
||||
if 'invited_by' not in cm_cols:
|
||||
op.add_column('channel_member', sa.Column('invited_by', sa.Text(), nullable=True))
|
||||
if 'invited_at' not in cm_cols:
|
||||
op.add_column('channel_member', sa.Column('invited_at', sa.BigInteger(), nullable=True))
|
||||
op.add_column('channel_member', sa.Column('role', sa.Text(), nullable=True))
|
||||
op.add_column('channel_member', sa.Column('invited_by', sa.Text(), nullable=True))
|
||||
op.add_column('channel_member', sa.Column('invited_at', sa.BigInteger(), nullable=True))
|
||||
|
||||
# Create 'channel_webhook' table
|
||||
if 'channel_webhook' not in existing_tables:
|
||||
op.create_table(
|
||||
'channel_webhook',
|
||||
sa.Column('id', sa.Text(), primary_key=True, unique=True, nullable=False),
|
||||
sa.Column('user_id', sa.Text(), nullable=False),
|
||||
sa.Column(
|
||||
'channel_id',
|
||||
sa.Text(),
|
||||
sa.ForeignKey('channel.id', ondelete='CASCADE'),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column('name', sa.Text(), nullable=False),
|
||||
sa.Column('profile_image_url', sa.Text(), nullable=True),
|
||||
sa.Column('token', sa.Text(), nullable=False),
|
||||
sa.Column('last_used_at', sa.BigInteger(), nullable=True),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=False),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=False),
|
||||
)
|
||||
op.create_table(
|
||||
'channel_webhook',
|
||||
sa.Column('id', sa.Text(), primary_key=True, unique=True, nullable=False),
|
||||
sa.Column('user_id', sa.Text(), nullable=False),
|
||||
sa.Column(
|
||||
'channel_id',
|
||||
sa.Text(),
|
||||
sa.ForeignKey('channel.id', ondelete='CASCADE'),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column('name', sa.Text(), nullable=False),
|
||||
sa.Column('profile_image_url', sa.Text(), nullable=True),
|
||||
sa.Column('token', sa.Text(), nullable=False),
|
||||
sa.Column('last_used_at', sa.BigInteger(), nullable=True),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=False),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=False),
|
||||
)
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
@@ -85,3 +74,5 @@ def downgrade() -> None:
|
||||
|
||||
# Drop 'channel_webhook' table
|
||||
op.drop_table('channel_webhook')
|
||||
|
||||
pass
|
||||
|
||||
@@ -6,8 +6,8 @@ Create Date: 2024-11-14 03:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = '922e7a387820'
|
||||
down_revision = '4ace53fd72c8'
|
||||
@@ -16,55 +16,70 @@ depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
existing_tables = set(inspector.get_table_names())
|
||||
|
||||
if 'group' not in existing_tables:
|
||||
op.create_table(
|
||||
'group',
|
||||
sa.Column('id', sa.Text(), nullable=False, primary_key=True, unique=True),
|
||||
sa.Column('user_id', sa.Text(), nullable=True),
|
||||
sa.Column('name', sa.Text(), nullable=True),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('data', sa.JSON(), nullable=True),
|
||||
sa.Column('meta', sa.JSON(), nullable=True),
|
||||
sa.Column('permissions', sa.JSON(), nullable=True),
|
||||
sa.Column('user_ids', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=True),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=True),
|
||||
)
|
||||
op.create_table(
|
||||
'group',
|
||||
sa.Column('id', sa.Text(), nullable=False, primary_key=True, unique=True),
|
||||
sa.Column('user_id', sa.Text(), nullable=True),
|
||||
sa.Column('name', sa.Text(), nullable=True),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('data', sa.JSON(), nullable=True),
|
||||
sa.Column('meta', sa.JSON(), nullable=True),
|
||||
sa.Column('permissions', sa.JSON(), nullable=True),
|
||||
sa.Column('user_ids', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=True),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=True),
|
||||
)
|
||||
|
||||
# Add 'access_control' column to 'model' table
|
||||
model_cols = {c['name'] for c in inspector.get_columns('model')}
|
||||
if 'access_control' not in model_cols:
|
||||
op.add_column('model', sa.Column('access_control', sa.JSON(), nullable=True))
|
||||
if 'is_active' not in model_cols:
|
||||
op.add_column(
|
||||
'model',
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False, server_default=sa.sql.expression.true()),
|
||||
)
|
||||
op.add_column(
|
||||
'model',
|
||||
sa.Column('access_control', sa.JSON(), nullable=True),
|
||||
)
|
||||
|
||||
# Add 'is_active' column to 'model' table
|
||||
op.add_column(
|
||||
'model',
|
||||
sa.Column(
|
||||
'is_active',
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
server_default=sa.sql.expression.true(),
|
||||
),
|
||||
)
|
||||
|
||||
# Add 'access_control' column to 'knowledge' table
|
||||
knowledge_cols = {c['name'] for c in inspector.get_columns('knowledge')}
|
||||
if 'access_control' not in knowledge_cols:
|
||||
op.add_column('knowledge', sa.Column('access_control', sa.JSON(), nullable=True))
|
||||
op.add_column(
|
||||
'knowledge',
|
||||
sa.Column('access_control', sa.JSON(), nullable=True),
|
||||
)
|
||||
|
||||
# Add 'access_control' column to 'prompt' table
|
||||
prompt_cols = {c['name'] for c in inspector.get_columns('prompt')}
|
||||
if 'access_control' not in prompt_cols:
|
||||
op.add_column('prompt', sa.Column('access_control', sa.JSON(), nullable=True))
|
||||
op.add_column(
|
||||
'prompt',
|
||||
sa.Column('access_control', sa.JSON(), nullable=True),
|
||||
)
|
||||
|
||||
# Add 'access_control' column to 'tools' table
|
||||
tool_cols = {c['name'] for c in inspector.get_columns('tool')}
|
||||
if 'access_control' not in tool_cols:
|
||||
op.add_column('tool', sa.Column('access_control', sa.JSON(), nullable=True))
|
||||
op.add_column(
|
||||
'tool',
|
||||
sa.Column('access_control', sa.JSON(), nullable=True),
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_table('group')
|
||||
|
||||
# Drop 'access_control' column from 'model' table
|
||||
op.drop_column('model', 'access_control')
|
||||
|
||||
# Drop 'is_active' column from 'model' table
|
||||
op.drop_column('model', 'is_active')
|
||||
|
||||
# Drop 'access_control' column from 'knowledge' table
|
||||
op.drop_column('knowledge', 'access_control')
|
||||
|
||||
# Drop 'access_control' column from 'prompt' table
|
||||
op.drop_column('prompt', 'access_control')
|
||||
|
||||
# Drop 'access_control' column from 'tools' table
|
||||
op.drop_column('tool', 'access_control')
|
||||
|
||||
@@ -6,8 +6,8 @@ Create Date: 2025-05-03 03:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = '9f0c9cd09105'
|
||||
down_revision = '3781e22d8b01'
|
||||
@@ -16,22 +16,17 @@ depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
existing_tables = set(inspector.get_table_names())
|
||||
|
||||
if 'note' not in existing_tables:
|
||||
op.create_table(
|
||||
'note',
|
||||
sa.Column('id', sa.Text(), nullable=False, primary_key=True, unique=True),
|
||||
sa.Column('user_id', sa.Text(), nullable=True),
|
||||
sa.Column('title', sa.Text(), nullable=True),
|
||||
sa.Column('data', sa.JSON(), nullable=True),
|
||||
sa.Column('meta', sa.JSON(), nullable=True),
|
||||
sa.Column('access_control', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=True),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=True),
|
||||
)
|
||||
op.create_table(
|
||||
'note',
|
||||
sa.Column('id', sa.Text(), nullable=False, primary_key=True, unique=True),
|
||||
sa.Column('user_id', sa.Text(), nullable=True),
|
||||
sa.Column('title', sa.Text(), nullable=True),
|
||||
sa.Column('data', sa.JSON(), nullable=True),
|
||||
sa.Column('meta', sa.JSON(), nullable=True),
|
||||
sa.Column('access_control', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=True),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=True),
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
"""Add memory user_id index
|
||||
|
||||
Revision ID: a0b1c2d3e4f5
|
||||
Revises: 4de81c2a3af1
|
||||
Create Date: 2025-09-15 03:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision = 'a0b1c2d3e4f5'
|
||||
down_revision = '4de81c2a3af1'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
existing_indexes = {idx['name'] for idx in inspector.get_indexes('memory')}
|
||||
|
||||
if 'ix_memory_user_id' not in existing_indexes:
|
||||
op.create_index('ix_memory_user_id', 'memory', ['user_id'])
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_index('ix_memory_user_id', table_name='memory')
|
||||
@@ -8,8 +8,9 @@ Create Date: 2026-02-11 09:30:00.000000
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from open_webui.migrations.util import get_existing_tables
|
||||
|
||||
revision: str = 'a1b2c3d4e5f6'
|
||||
|
||||
+3
-9
@@ -8,8 +8,8 @@ Create Date: 2026-03-29 22:15:00.000000
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'a3dd5bedd151'
|
||||
@@ -19,14 +19,8 @@ depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
columns = [col['name'] for col in inspector.get_columns('chat')]
|
||||
|
||||
if 'tasks' not in columns:
|
||||
op.add_column('chat', sa.Column('tasks', sa.JSON(), nullable=True))
|
||||
if 'summary' not in columns:
|
||||
op.add_column('chat', sa.Column('summary', sa.Text(), nullable=True))
|
||||
op.add_column('chat', sa.Column('tasks', sa.JSON(), nullable=True))
|
||||
op.add_column('chat', sa.Column('summary', sa.Text(), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
+8
-10
@@ -8,8 +8,8 @@ Create Date: 2025-09-27 02:24:18.058455
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'a5c220713937'
|
||||
@@ -19,18 +19,16 @@ depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
msg_cols = {c['name'] for c in inspector.get_columns('message')}
|
||||
|
||||
# Add 'reply_to_id' column to the 'message' table for replying to messages
|
||||
if 'reply_to_id' not in msg_cols:
|
||||
op.add_column(
|
||||
'message',
|
||||
sa.Column('reply_to_id', sa.Text(), nullable=True),
|
||||
)
|
||||
op.add_column(
|
||||
'message',
|
||||
sa.Column('reply_to_id', sa.Text(), nullable=True),
|
||||
)
|
||||
pass
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Remove 'reply_to_id' column from the 'message' table
|
||||
op.drop_column('message', 'reply_to_id')
|
||||
|
||||
pass
|
||||
|
||||
@@ -6,8 +6,8 @@ Create Date: 2024-10-20 17:02:35.241684
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# Revision identifiers, used by Alembic.
|
||||
revision = 'af906e964978'
|
||||
@@ -17,28 +17,23 @@ depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
existing_tables = set(inspector.get_table_names())
|
||||
|
||||
if 'feedback' not in existing_tables:
|
||||
# ### Create feedback table ###
|
||||
op.create_table(
|
||||
'feedback',
|
||||
sa.Column('id', sa.Text(), primary_key=True), # Unique identifier for each feedback (TEXT type)
|
||||
sa.Column('user_id', sa.Text(), nullable=True), # ID of the user providing the feedback (TEXT type)
|
||||
sa.Column('version', sa.BigInteger(), default=0), # Version of feedback (BIGINT type)
|
||||
sa.Column('type', sa.Text(), nullable=True), # Type of feedback (TEXT type)
|
||||
sa.Column('data', sa.JSON(), nullable=True), # Feedback data (JSON type)
|
||||
sa.Column('meta', sa.JSON(), nullable=True), # Metadata for feedback (JSON type)
|
||||
sa.Column('snapshot', sa.JSON(), nullable=True), # snapshot data for feedback (JSON type)
|
||||
sa.Column(
|
||||
'created_at', sa.BigInteger(), nullable=False
|
||||
), # Feedback creation timestamp (BIGINT representing epoch)
|
||||
sa.Column(
|
||||
'updated_at', sa.BigInteger(), nullable=False
|
||||
), # Feedback update timestamp (BIGINT representing epoch)
|
||||
)
|
||||
# ### Create feedback table ###
|
||||
op.create_table(
|
||||
'feedback',
|
||||
sa.Column('id', sa.Text(), primary_key=True), # Unique identifier for each feedback (TEXT type)
|
||||
sa.Column('user_id', sa.Text(), nullable=True), # ID of the user providing the feedback (TEXT type)
|
||||
sa.Column('version', sa.BigInteger(), default=0), # Version of feedback (BIGINT type)
|
||||
sa.Column('type', sa.Text(), nullable=True), # Type of feedback (TEXT type)
|
||||
sa.Column('data', sa.JSON(), nullable=True), # Feedback data (JSON type)
|
||||
sa.Column('meta', sa.JSON(), nullable=True), # Metadata for feedback (JSON type)
|
||||
sa.Column('snapshot', sa.JSON(), nullable=True), # snapshot data for feedback (JSON type)
|
||||
sa.Column(
|
||||
'created_at', sa.BigInteger(), nullable=False
|
||||
), # Feedback creation timestamp (BIGINT representing epoch)
|
||||
sa.Column(
|
||||
'updated_at', sa.BigInteger(), nullable=False
|
||||
), # Feedback update timestamp (BIGINT representing epoch)
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
|
||||
@@ -6,13 +6,15 @@ Create Date: 2025-11-28 04:55:31.737538
|
||||
|
||||
"""
|
||||
|
||||
import json
|
||||
import time
|
||||
from typing import Sequence, Union
|
||||
|
||||
import open_webui.internal.db
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
import open_webui.internal.db
|
||||
import json
|
||||
import time
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'b10670c03dd5'
|
||||
@@ -20,49 +22,20 @@ down_revision: Union[str, None] = '2f1211949ecc'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
# ── Ad-hoc table references for Core DML ─────────────────────────────────
|
||||
# These are lightweight table() / column() references used only inside this
|
||||
# migration for SELECT / UPDATE / INSERT — they do NOT create or alter
|
||||
# anything on disk.
|
||||
|
||||
_user = sa.table(
|
||||
'user',
|
||||
sa.column('id', sa.Text),
|
||||
sa.column('oauth_sub', sa.Text),
|
||||
sa.column('oauth', sa.JSON),
|
||||
sa.column('api_key', sa.Text),
|
||||
sa.column('info', sa.Text),
|
||||
sa.column('settings', sa.Text),
|
||||
)
|
||||
|
||||
_api_key = sa.table(
|
||||
'api_key',
|
||||
sa.column('id', sa.Text),
|
||||
sa.column('user_id', sa.Text),
|
||||
sa.column('key', sa.Text),
|
||||
sa.column('created_at', sa.BigInteger),
|
||||
sa.column('updated_at', sa.BigInteger),
|
||||
)
|
||||
|
||||
|
||||
def _drop_sqlite_indexes_for_column(table_name, column_name, conn):
|
||||
"""
|
||||
SQLite requires manual removal of any user-created indexes referencing
|
||||
a column before ALTER TABLE ... DROP COLUMN can succeed.
|
||||
|
||||
NOTE: PRAGMAs have no Core equivalent — raw text is unavoidable here.
|
||||
SQLite requires manual removal of any indexes referencing a column
|
||||
before ALTER TABLE ... DROP COLUMN can succeed.
|
||||
"""
|
||||
indexes = conn.execute(sa.text(f"PRAGMA index_list('{table_name}')")).fetchall()
|
||||
|
||||
for idx in indexes:
|
||||
index_name = idx[1] # index name
|
||||
# Skip system-managed autoindexes (PK / UNIQUE constraints) — they
|
||||
# cannot be dropped directly and will disappear when the column is
|
||||
# removed via batch_alter_table.
|
||||
if index_name.startswith('sqlite_autoindex_'):
|
||||
continue
|
||||
# Get indexed columns
|
||||
idx_info = conn.execute(sa.text(f"PRAGMA index_info('{index_name}')")).fetchall()
|
||||
indexed_cols = [row[2] for row in idx_info]
|
||||
|
||||
indexed_cols = [row[2] for row in idx_info] # col names
|
||||
if column_name in indexed_cols:
|
||||
conn.execute(sa.text(f'DROP INDEX IF EXISTS {index_name}'))
|
||||
|
||||
@@ -71,30 +44,33 @@ def _convert_column_to_json(table: str, column: str):
|
||||
conn = op.get_bind()
|
||||
dialect = conn.dialect.name
|
||||
|
||||
t = sa.table(table, sa.column('id', sa.Text), sa.column(column, sa.Text))
|
||||
|
||||
# SQLite cannot ALTER COLUMN → must recreate column
|
||||
if dialect == 'sqlite':
|
||||
# 1. Add temporary column
|
||||
op.add_column(table, sa.Column(f'{column}_json', sa.JSON(), nullable=True))
|
||||
|
||||
rows = conn.execute(sa.select(t.c.id, t.c[column])).fetchall()
|
||||
# 2. Load old data
|
||||
rows = conn.execute(sa.text(f'SELECT id, {column} FROM "{table}"')).fetchall()
|
||||
|
||||
for uid, raw in rows:
|
||||
for row in rows:
|
||||
uid, raw = row
|
||||
if raw is None:
|
||||
parsed = None
|
||||
else:
|
||||
try:
|
||||
parsed = json.loads(raw)
|
||||
except Exception:
|
||||
parsed = None
|
||||
parsed = None # fallback safe behavior
|
||||
|
||||
conn.execute(
|
||||
sa.update(sa.table(table, sa.column('id'), sa.column(f'{column}_json', sa.JSON)))
|
||||
.where(sa.column('id') == uid)
|
||||
.values({f'{column}_json': parsed})
|
||||
sa.text(f'UPDATE "{table}" SET {column}_json = :val WHERE id = :id'),
|
||||
{'val': json.dumps(parsed) if parsed else None, 'id': uid},
|
||||
)
|
||||
|
||||
# 3. Drop old TEXT column
|
||||
op.drop_column(table, column)
|
||||
|
||||
# 4. Rename new JSON column → original name
|
||||
op.alter_column(table, f'{column}_json', new_column_name=column)
|
||||
|
||||
else:
|
||||
@@ -111,18 +87,15 @@ def _convert_column_to_text(table: str, column: str):
|
||||
conn = op.get_bind()
|
||||
dialect = conn.dialect.name
|
||||
|
||||
t = sa.table(table, sa.column('id', sa.Text), sa.column(column, sa.JSON))
|
||||
|
||||
if dialect == 'sqlite':
|
||||
op.add_column(table, sa.Column(f'{column}_text', sa.Text(), nullable=True))
|
||||
|
||||
rows = conn.execute(sa.select(t.c.id, t.c[column])).fetchall()
|
||||
rows = conn.execute(sa.text(f'SELECT id, {column} FROM "{table}"')).fetchall()
|
||||
|
||||
for uid, raw in rows:
|
||||
conn.execute(
|
||||
sa.update(sa.table(table, sa.column('id'), sa.column(f'{column}_text', sa.Text)))
|
||||
.where(sa.column('id') == uid)
|
||||
.values({f'{column}_text': json.dumps(raw) if raw is not None else None})
|
||||
sa.text(f'UPDATE "{table}" SET {column}_text = :val WHERE id = :id'),
|
||||
{'val': json.dumps(raw) if raw else None, 'id': uid},
|
||||
)
|
||||
|
||||
op.drop_column(table, column)
|
||||
@@ -138,93 +111,88 @@ def _convert_column_to_text(table: str, column: str):
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column('user', sa.Column('profile_banner_image_url', sa.Text(), nullable=True))
|
||||
op.add_column('user', sa.Column('timezone', sa.String(), nullable=True))
|
||||
|
||||
op.add_column('user', sa.Column('presence_state', sa.String(), nullable=True))
|
||||
op.add_column('user', sa.Column('status_emoji', sa.String(), nullable=True))
|
||||
op.add_column('user', sa.Column('status_message', sa.Text(), nullable=True))
|
||||
op.add_column('user', sa.Column('status_expires_at', sa.BigInteger(), nullable=True))
|
||||
|
||||
op.add_column('user', sa.Column('oauth', sa.JSON(), nullable=True))
|
||||
|
||||
# Convert info (TEXT/JSONField) → JSON
|
||||
_convert_column_to_json('user', 'info')
|
||||
# Convert settings (TEXT/JSONField) → JSON
|
||||
_convert_column_to_json('user', 'settings')
|
||||
|
||||
op.create_table(
|
||||
'api_key',
|
||||
sa.Column('id', sa.Text(), primary_key=True, unique=True),
|
||||
sa.Column('user_id', sa.Text(), sa.ForeignKey('user.id', ondelete='CASCADE')),
|
||||
sa.Column('key', sa.Text(), unique=True, nullable=False),
|
||||
sa.Column('data', sa.JSON(), nullable=True),
|
||||
sa.Column('expires_at', sa.BigInteger(), nullable=True),
|
||||
sa.Column('last_used_at', sa.BigInteger(), nullable=True),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=False),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=False),
|
||||
)
|
||||
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
existing_tables = set(inspector.get_table_names())
|
||||
user_columns = {c['name'] for c in inspector.get_columns('user')}
|
||||
users = conn.execute(sa.text('SELECT id, oauth_sub FROM "user" WHERE oauth_sub IS NOT NULL')).fetchall()
|
||||
|
||||
# ── Add new columns (idempotent) ──────────────────────────────────
|
||||
for col_name, col_type in [
|
||||
('profile_banner_image_url', sa.Text()),
|
||||
('timezone', sa.String()),
|
||||
('presence_state', sa.String()),
|
||||
('status_emoji', sa.String()),
|
||||
('status_message', sa.Text()),
|
||||
('status_expires_at', sa.BigInteger()),
|
||||
('oauth', sa.JSON()),
|
||||
]:
|
||||
if col_name not in user_columns:
|
||||
op.add_column('user', sa.Column(col_name, col_type, nullable=True))
|
||||
for uid, oauth_sub in users:
|
||||
if oauth_sub:
|
||||
# Example formats supported:
|
||||
# provider@sub
|
||||
# plain sub (stored as {"oidc": {"sub": sub}})
|
||||
if '@' in oauth_sub:
|
||||
provider, sub = oauth_sub.split('@', 1)
|
||||
else:
|
||||
provider, sub = 'oidc', oauth_sub
|
||||
|
||||
# Convert info (TEXT/JSONField) → JSON (skip if already JSON)
|
||||
user_col_types = {c['name']: c['type'] for c in inspector.get_columns('user')}
|
||||
if isinstance(user_col_types.get('info'), sa.Text):
|
||||
_convert_column_to_json('user', 'info')
|
||||
# Convert settings (TEXT/JSONField) → JSON (skip if already JSON)
|
||||
if isinstance(user_col_types.get('settings'), sa.Text):
|
||||
_convert_column_to_json('user', 'settings')
|
||||
oauth_json = json.dumps({provider: {'sub': sub}})
|
||||
conn.execute(
|
||||
sa.text('UPDATE "user" SET oauth = :oauth WHERE id = :id'),
|
||||
{'oauth': oauth_json, 'id': uid},
|
||||
)
|
||||
|
||||
# ── Create api_key table (idempotent) ─────────────────────────────
|
||||
if 'api_key' not in existing_tables:
|
||||
op.create_table(
|
||||
'api_key',
|
||||
sa.Column('id', sa.Text(), primary_key=True, unique=True),
|
||||
sa.Column('user_id', sa.Text(), sa.ForeignKey('user.id', ondelete='CASCADE')),
|
||||
sa.Column('key', sa.Text(), unique=True, nullable=False),
|
||||
sa.Column('data', sa.JSON(), nullable=True),
|
||||
sa.Column('expires_at', sa.BigInteger(), nullable=True),
|
||||
sa.Column('last_used_at', sa.BigInteger(), nullable=True),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=False),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=False),
|
||||
)
|
||||
users_with_keys = conn.execute(sa.text('SELECT id, api_key FROM "user" WHERE api_key IS NOT NULL')).fetchall()
|
||||
now = int(time.time())
|
||||
|
||||
# ── Migrate oauth_sub → oauth JSON (only if old column still exists)
|
||||
if 'oauth_sub' in user_columns:
|
||||
rows = conn.execute(sa.select(_user.c.id, _user.c.oauth_sub).where(_user.c.oauth_sub.is_not(None))).fetchall()
|
||||
for uid, api_key in users_with_keys:
|
||||
if api_key:
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
INSERT INTO api_key (id, user_id, key, created_at, updated_at)
|
||||
VALUES (:id, :user_id, :key, :created_at, :updated_at)
|
||||
"""),
|
||||
{
|
||||
'id': f'key_{uid}',
|
||||
'user_id': uid,
|
||||
'key': api_key,
|
||||
'created_at': now,
|
||||
'updated_at': now,
|
||||
},
|
||||
)
|
||||
|
||||
for uid, oauth_sub in rows:
|
||||
if oauth_sub:
|
||||
provider, sub = oauth_sub.split('@', 1) if '@' in oauth_sub else ('oidc', oauth_sub)
|
||||
conn.execute(
|
||||
sa.update(_user).where(_user.c.id == uid).values(oauth=json.dumps({provider: {'sub': sub}}))
|
||||
)
|
||||
if conn.dialect.name == 'sqlite':
|
||||
_drop_sqlite_indexes_for_column('user', 'api_key', conn)
|
||||
_drop_sqlite_indexes_for_column('user', 'oauth_sub', conn)
|
||||
|
||||
# ── Migrate api_key column → api_key table (only if old column still exists)
|
||||
if 'api_key' in user_columns:
|
||||
rows = conn.execute(sa.select(_user.c.id, _user.c.api_key).where(_user.c.api_key.is_not(None))).fetchall()
|
||||
now = int(time.time())
|
||||
|
||||
for uid, key_val in rows:
|
||||
if key_val:
|
||||
conn.execute(
|
||||
sa.insert(_api_key).values(
|
||||
id=f'key_{uid}',
|
||||
user_id=uid,
|
||||
key=key_val,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
)
|
||||
|
||||
# ── Drop legacy columns (idempotent) ──────────────────────────────
|
||||
cols_to_drop = {'api_key', 'oauth_sub'} & user_columns
|
||||
if cols_to_drop:
|
||||
if conn.dialect.name == 'sqlite':
|
||||
for col in cols_to_drop:
|
||||
_drop_sqlite_indexes_for_column('user', col, conn)
|
||||
|
||||
with op.batch_alter_table('user') as batch_op:
|
||||
for col in cols_to_drop:
|
||||
batch_op.drop_column(col)
|
||||
with op.batch_alter_table('user') as batch_op:
|
||||
batch_op.drop_column('api_key')
|
||||
batch_op.drop_column('oauth_sub')
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# --- 1. Restore old oauth_sub column ---
|
||||
op.add_column('user', sa.Column('oauth_sub', sa.Text(), nullable=True))
|
||||
|
||||
conn = op.get_bind()
|
||||
rows = conn.execute(sa.select(_user.c.id, _user.c.oauth).where(_user.c.oauth.is_not(None))).fetchall()
|
||||
users = conn.execute(sa.text('SELECT id, oauth FROM "user" WHERE oauth IS NOT NULL')).fetchall()
|
||||
|
||||
for uid, oauth in rows:
|
||||
for uid, oauth in users:
|
||||
try:
|
||||
data = json.loads(oauth)
|
||||
provider = list(data.keys())[0]
|
||||
@@ -233,17 +201,25 @@ def downgrade() -> None:
|
||||
except Exception:
|
||||
oauth_sub = None
|
||||
|
||||
conn.execute(sa.update(_user).where(_user.c.id == uid).values(oauth_sub=oauth_sub))
|
||||
conn.execute(
|
||||
sa.text('UPDATE "user" SET oauth_sub = :oauth_sub WHERE id = :id'),
|
||||
{'oauth_sub': oauth_sub, 'id': uid},
|
||||
)
|
||||
|
||||
op.drop_column('user', 'oauth')
|
||||
|
||||
# --- Restore api_key field ---
|
||||
# --- 2. Restore api_key field ---
|
||||
op.add_column('user', sa.Column('api_key', sa.String(), nullable=True))
|
||||
|
||||
keys = conn.execute(sa.select(_api_key.c.user_id, _api_key.c.key)).fetchall()
|
||||
# Restore values from api_key
|
||||
keys = conn.execute(sa.text('SELECT user_id, key FROM api_key')).fetchall()
|
||||
for uid, key in keys:
|
||||
conn.execute(sa.update(_user).where(_user.c.id == uid).values(api_key=key))
|
||||
conn.execute(
|
||||
sa.text('UPDATE "user" SET api_key = :key WHERE id = :id'),
|
||||
{'key': key, 'id': uid},
|
||||
)
|
||||
|
||||
# Drop new table
|
||||
op.drop_table('api_key')
|
||||
|
||||
with op.batch_alter_table('user') as batch_op:
|
||||
|
||||
+2
-7
@@ -8,8 +8,8 @@ Create Date: 2026-02-13 14:19:00.000000
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'b2c3d4e5f6a7'
|
||||
@@ -19,12 +19,7 @@ depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
user_cols = {c['name'] for c in inspector.get_columns('user')}
|
||||
|
||||
if 'scim' not in user_cols:
|
||||
op.add_column('user', sa.Column('scim', sa.JSON(), nullable=True))
|
||||
op.add_column('user', sa.Column('scim', sa.JSON(), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
@@ -6,8 +6,9 @@ Create Date: 2026-04-01 04:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'b7c8d9e0f1a2'
|
||||
@@ -17,14 +18,9 @@ depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
columns = [col['name'] for col in inspector.get_columns('chat')]
|
||||
|
||||
if 'last_read_at' not in columns:
|
||||
op.add_column('chat', sa.Column('last_read_at', sa.BigInteger(), nullable=True))
|
||||
# Set existing chats to be marked as read
|
||||
op.execute('UPDATE chat SET last_read_at = updated_at')
|
||||
op.add_column('chat', sa.Column('last_read_at', sa.BigInteger(), nullable=True))
|
||||
# Set existing chats to be marked as read
|
||||
op.execute('UPDATE chat SET last_read_at = updated_at')
|
||||
|
||||
|
||||
def downgrade():
|
||||
|
||||
@@ -19,17 +19,10 @@ depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
file_cols = {c['name'] for c in inspector.get_columns('file')}
|
||||
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
if 'hash' not in file_cols:
|
||||
op.add_column('file', sa.Column('hash', sa.Text(), nullable=True))
|
||||
if 'data' not in file_cols:
|
||||
op.add_column('file', sa.Column('data', sa.JSON(), nullable=True))
|
||||
if 'updated_at' not in file_cols:
|
||||
op.add_column('file', sa.Column('updated_at', sa.BigInteger(), nullable=True))
|
||||
op.add_column('file', sa.Column('hash', sa.Text(), nullable=True))
|
||||
op.add_column('file', sa.Column('data', sa.JSON(), nullable=True))
|
||||
op.add_column('file', sa.Column('updated_at', sa.BigInteger(), nullable=True))
|
||||
|
||||
|
||||
def downgrade():
|
||||
|
||||
@@ -9,8 +9,8 @@ Create Date: 2026-04-16 23:00:00.000000
|
||||
import time
|
||||
import uuid
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = 'c1d2e3f4a5b6'
|
||||
down_revision = 'e1f2a3b4c5d6'
|
||||
@@ -61,21 +61,18 @@ access_grant_t = sa.table(
|
||||
|
||||
def upgrade():
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
tables = inspector.get_table_names()
|
||||
|
||||
# 1. Create shared_chat table (idempotent)
|
||||
if 'shared_chat' not in tables:
|
||||
op.create_table(
|
||||
'shared_chat',
|
||||
sa.Column('id', sa.Text(), primary_key=True),
|
||||
sa.Column('chat_id', sa.Text(), sa.ForeignKey('chat.id', ondelete='CASCADE'), nullable=False),
|
||||
sa.Column('user_id', sa.Text(), nullable=False),
|
||||
sa.Column('title', sa.Text(), nullable=True),
|
||||
sa.Column('chat', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=True),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=True),
|
||||
)
|
||||
# 1. Create shared_chat table
|
||||
op.create_table(
|
||||
'shared_chat',
|
||||
sa.Column('id', sa.Text(), primary_key=True),
|
||||
sa.Column('chat_id', sa.Text(), sa.ForeignKey('chat.id', ondelete='CASCADE'), nullable=False),
|
||||
sa.Column('user_id', sa.Text(), nullable=False),
|
||||
sa.Column('title', sa.Text(), nullable=True),
|
||||
sa.Column('chat', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=True),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=True),
|
||||
)
|
||||
|
||||
# 2. Migrate existing shared-* rows
|
||||
shared_rows = conn.execute(
|
||||
@@ -99,51 +96,31 @@ def upgrade():
|
||||
if not original:
|
||||
continue
|
||||
|
||||
# Check if shared_chat record already exists (idempotent)
|
||||
existing_shared = conn.execute(
|
||||
sa.select(shared_chat_t.c.id).where(shared_chat_t.c.id == share_token)
|
||||
).fetchone()
|
||||
|
||||
if not existing_shared:
|
||||
# Insert snapshot into shared_chat
|
||||
conn.execute(
|
||||
shared_chat_t.insert().values(
|
||||
id=share_token,
|
||||
chat_id=original_chat_id,
|
||||
user_id=original.user_id,
|
||||
title=row.title,
|
||||
chat=row.chat,
|
||||
created_at=row.created_at,
|
||||
updated_at=row.updated_at,
|
||||
)
|
||||
# Insert snapshot into shared_chat
|
||||
conn.execute(
|
||||
shared_chat_t.insert().values(
|
||||
id=share_token,
|
||||
chat_id=original_chat_id,
|
||||
user_id=original.user_id,
|
||||
title=row.title,
|
||||
chat=row.chat,
|
||||
created_at=row.created_at,
|
||||
updated_at=row.updated_at,
|
||||
)
|
||||
)
|
||||
|
||||
# Check if access_grant record already exists (idempotent)
|
||||
existing_grant = conn.execute(
|
||||
sa.select(access_grant_t.c.id).where(
|
||||
sa.and_(
|
||||
access_grant_t.c.resource_type == 'shared_chat',
|
||||
access_grant_t.c.resource_id == original_chat_id,
|
||||
access_grant_t.c.principal_type == 'user',
|
||||
access_grant_t.c.principal_id == '*',
|
||||
access_grant_t.c.permission == 'read',
|
||||
)
|
||||
)
|
||||
).fetchone()
|
||||
|
||||
if not existing_grant:
|
||||
# Create user:*:read grant for backward compat
|
||||
conn.execute(
|
||||
access_grant_t.insert().values(
|
||||
id=str(uuid.uuid4()),
|
||||
resource_type='shared_chat',
|
||||
resource_id=original_chat_id,
|
||||
principal_type='user',
|
||||
principal_id='*',
|
||||
permission='read',
|
||||
created_at=row.created_at or int(time.time()),
|
||||
)
|
||||
# Create user:*:read grant for backward compat
|
||||
conn.execute(
|
||||
access_grant_t.insert().values(
|
||||
id=str(uuid.uuid4()),
|
||||
resource_type='shared_chat',
|
||||
resource_id=original_chat_id,
|
||||
principal_type='user',
|
||||
principal_id='*',
|
||||
permission='read',
|
||||
created_at=row.created_at or int(time.time()),
|
||||
)
|
||||
)
|
||||
|
||||
# 3. Clean up old phantom rows
|
||||
conn.execute(
|
||||
|
||||
@@ -6,12 +6,11 @@ Create Date: 2024-10-20 17:02:35.241684
|
||||
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy import JSON, String, Text, and_
|
||||
from sqlalchemy.sql import column, table
|
||||
import sqlalchemy as sa
|
||||
import json
|
||||
from sqlalchemy.sql import table, column
|
||||
from sqlalchemy import String, Text, JSON, and_
|
||||
|
||||
revision = 'c29facfe716b'
|
||||
down_revision = 'c69f45358db4'
|
||||
@@ -20,13 +19,8 @@ depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
file_cols = {c['name'] for c in inspector.get_columns('file')}
|
||||
|
||||
# 1. Add the `path` column to the "file" table.
|
||||
if 'path' not in file_cols:
|
||||
op.add_column('file', sa.Column('path', sa.Text(), nullable=True))
|
||||
op.add_column('file', sa.Column('path', sa.Text(), nullable=True))
|
||||
|
||||
# 2. Convert the `meta` column from Text/JSONField to `JSON()`
|
||||
# Use Alembic's default batch_op for dialect compatibility.
|
||||
|
||||
@@ -8,8 +8,8 @@ Create Date: 2025-12-21 20:27:41.694897
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'c440947495f3'
|
||||
@@ -19,39 +19,36 @@ depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
existing_tables = set(inspector.get_table_names())
|
||||
|
||||
if 'chat_file' not in existing_tables:
|
||||
op.create_table(
|
||||
'chat_file',
|
||||
sa.Column('id', sa.Text(), primary_key=True),
|
||||
sa.Column('user_id', sa.Text(), nullable=False),
|
||||
sa.Column(
|
||||
'chat_id',
|
||||
sa.Text(),
|
||||
sa.ForeignKey('chat.id', ondelete='CASCADE'),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
'file_id',
|
||||
sa.Text(),
|
||||
sa.ForeignKey('file.id', ondelete='CASCADE'),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column('message_id', sa.Text(), nullable=True),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=False),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=False),
|
||||
# indexes
|
||||
sa.Index('ix_chat_file_chat_id', 'chat_id'),
|
||||
sa.Index('ix_chat_file_file_id', 'file_id'),
|
||||
sa.Index('ix_chat_file_message_id', 'message_id'),
|
||||
sa.Index('ix_chat_file_user_id', 'user_id'),
|
||||
# unique constraints
|
||||
sa.UniqueConstraint('chat_id', 'file_id', name='uq_chat_file_chat_file'), # prevent duplicate entries
|
||||
)
|
||||
op.create_table(
|
||||
'chat_file',
|
||||
sa.Column('id', sa.Text(), primary_key=True),
|
||||
sa.Column('user_id', sa.Text(), nullable=False),
|
||||
sa.Column(
|
||||
'chat_id',
|
||||
sa.Text(),
|
||||
sa.ForeignKey('chat.id', ondelete='CASCADE'),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
'file_id',
|
||||
sa.Text(),
|
||||
sa.ForeignKey('file.id', ondelete='CASCADE'),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column('message_id', sa.Text(), nullable=True),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=False),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=False),
|
||||
# indexes
|
||||
sa.Index('ix_chat_file_chat_id', 'chat_id'),
|
||||
sa.Index('ix_chat_file_file_id', 'file_id'),
|
||||
sa.Index('ix_chat_file_message_id', 'message_id'),
|
||||
sa.Index('ix_chat_file_user_id', 'user_id'),
|
||||
# unique constraints
|
||||
sa.UniqueConstraint('chat_id', 'file_id', name='uq_chat_file_chat_file'), # prevent duplicate entries
|
||||
)
|
||||
pass
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table('chat_file')
|
||||
pass
|
||||
|
||||
@@ -6,8 +6,8 @@ Create Date: 2024-10-16 02:02:35.241684
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = 'c69f45358db4'
|
||||
down_revision = '3ab32c4b8f59'
|
||||
@@ -16,37 +16,30 @@ depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
existing_tables = set(inspector.get_table_names())
|
||||
op.create_table(
|
||||
'folder',
|
||||
sa.Column('id', sa.Text(), nullable=False),
|
||||
sa.Column('parent_id', sa.Text(), nullable=True),
|
||||
sa.Column('user_id', sa.Text(), nullable=False),
|
||||
sa.Column('name', sa.Text(), nullable=False),
|
||||
sa.Column('items', sa.JSON(), nullable=True),
|
||||
sa.Column('meta', sa.JSON(), nullable=True),
|
||||
sa.Column('is_expanded', sa.Boolean(), default=False, nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column(
|
||||
'updated_at',
|
||||
sa.DateTime(),
|
||||
nullable=False,
|
||||
server_default=sa.func.now(),
|
||||
onupdate=sa.func.now(),
|
||||
),
|
||||
sa.PrimaryKeyConstraint('id', 'user_id'),
|
||||
)
|
||||
|
||||
if 'folder' not in existing_tables:
|
||||
op.create_table(
|
||||
'folder',
|
||||
sa.Column('id', sa.Text(), nullable=False),
|
||||
sa.Column('parent_id', sa.Text(), nullable=True),
|
||||
sa.Column('user_id', sa.Text(), nullable=False),
|
||||
sa.Column('name', sa.Text(), nullable=False),
|
||||
sa.Column('items', sa.JSON(), nullable=True),
|
||||
sa.Column('meta', sa.JSON(), nullable=True),
|
||||
sa.Column('is_expanded', sa.Boolean(), default=False, nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column(
|
||||
'updated_at',
|
||||
sa.DateTime(),
|
||||
nullable=False,
|
||||
server_default=sa.func.now(),
|
||||
onupdate=sa.func.now(),
|
||||
),
|
||||
sa.PrimaryKeyConstraint('id', 'user_id'),
|
||||
)
|
||||
|
||||
chat_cols = {c['name'] for c in inspector.get_columns('chat')}
|
||||
if 'folder_id' not in chat_cols:
|
||||
op.add_column(
|
||||
'chat',
|
||||
sa.Column('folder_id', sa.Text(), nullable=True),
|
||||
)
|
||||
op.add_column(
|
||||
'chat',
|
||||
sa.Column('folder_id', sa.Text(), nullable=True),
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"""Add config table.
|
||||
"""Add config table
|
||||
|
||||
Revision ID: ca81bd47c050
|
||||
Revises: 7e5b5dc7342b
|
||||
Create Date: 2024-08-25 15:26:35.241684
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
@@ -10,40 +11,29 @@ from typing import Sequence, Union
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'ca81bd47c050'
|
||||
down_revision: Union[str, None] = '7e5b5dc7342b'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Create a key-value config table with versioning."""
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
existing_tables = set(inspector.get_table_names())
|
||||
|
||||
if 'config' not in existing_tables:
|
||||
op.create_table(
|
||||
'config',
|
||||
sa.Column('id', sa.Integer, primary_key=True),
|
||||
sa.Column('data', sa.JSON(), nullable=False),
|
||||
sa.Column('version', sa.Integer, nullable=False),
|
||||
sa.Column(
|
||||
'created_at',
|
||||
sa.DateTime(),
|
||||
nullable=False,
|
||||
server_default=sa.func.now(),
|
||||
),
|
||||
sa.Column(
|
||||
'updated_at',
|
||||
sa.DateTime(),
|
||||
nullable=True,
|
||||
server_default=sa.func.now(),
|
||||
onupdate=sa.func.now(),
|
||||
),
|
||||
)
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
'config',
|
||||
sa.Column('id', sa.Integer, primary_key=True),
|
||||
sa.Column('data', sa.JSON(), nullable=False),
|
||||
sa.Column('version', sa.Integer, nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.func.now()),
|
||||
sa.Column(
|
||||
'updated_at',
|
||||
sa.DateTime(),
|
||||
nullable=True,
|
||||
server_default=sa.func.now(),
|
||||
onupdate=sa.func.now(),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Drop the config table."""
|
||||
def downgrade():
|
||||
op.drop_table('config')
|
||||
|
||||
@@ -6,8 +6,8 @@ Create Date: 2025-07-13 03:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = 'd31026856c01'
|
||||
down_revision = '9f0c9cd09105'
|
||||
@@ -16,12 +16,7 @@ depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
folder_cols = {c['name'] for c in inspector.get_columns('folder')}
|
||||
|
||||
if 'data' not in folder_cols:
|
||||
op.add_column('folder', sa.Column('data', sa.JSON(), nullable=True))
|
||||
op.add_column('folder', sa.Column('data', sa.JSON(), nullable=True))
|
||||
|
||||
|
||||
def downgrade():
|
||||
|
||||
@@ -7,8 +7,8 @@ Create Date: 2026-03-30
|
||||
|
||||
from typing import Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision: str = 'd4e5f6a7b8c9'
|
||||
down_revision: Union[str, None] = 'a3dd5bedd151'
|
||||
@@ -16,56 +16,36 @@ branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def _index_exists(inspector, index_name, table_name):
|
||||
"""Check if an index already exists on the given table (works for both SQLite and PostgreSQL)."""
|
||||
indexes = inspector.get_indexes(table_name)
|
||||
return any(idx['name'] == index_name for idx in indexes)
|
||||
|
||||
|
||||
def upgrade():
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
tables = inspector.get_table_names()
|
||||
op.create_table(
|
||||
'automation',
|
||||
sa.Column('id', sa.Text(), primary_key=True),
|
||||
sa.Column('user_id', sa.Text(), nullable=False),
|
||||
sa.Column('name', sa.Text(), nullable=False),
|
||||
sa.Column('data', sa.JSON(), nullable=False),
|
||||
sa.Column('meta', sa.JSON(), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False, default=True),
|
||||
sa.Column('last_run_at', sa.BigInteger(), nullable=True),
|
||||
sa.Column('next_run_at', sa.BigInteger(), nullable=True),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=False),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=False),
|
||||
)
|
||||
op.create_index('ix_automation_next_run', 'automation', ['next_run_at'])
|
||||
|
||||
if 'automation' not in tables:
|
||||
op.create_table(
|
||||
'automation',
|
||||
sa.Column('id', sa.Text(), primary_key=True),
|
||||
sa.Column('user_id', sa.Text(), nullable=False),
|
||||
sa.Column('name', sa.Text(), nullable=False),
|
||||
sa.Column('data', sa.JSON(), nullable=False),
|
||||
sa.Column('meta', sa.JSON(), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False, default=True),
|
||||
sa.Column('last_run_at', sa.BigInteger(), nullable=True),
|
||||
sa.Column('next_run_at', sa.BigInteger(), nullable=True),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=False),
|
||||
sa.Column('updated_at', sa.BigInteger(), nullable=False),
|
||||
)
|
||||
|
||||
inspector.clear_cache()
|
||||
if 'automation' in inspector.get_table_names():
|
||||
if not _index_exists(inspector, 'ix_automation_next_run', 'automation'):
|
||||
op.create_index('ix_automation_next_run', 'automation', ['next_run_at'])
|
||||
|
||||
if 'automation_run' not in tables:
|
||||
op.create_table(
|
||||
'automation_run',
|
||||
sa.Column('id', sa.Text(), primary_key=True),
|
||||
sa.Column('automation_id', sa.Text(), nullable=False),
|
||||
sa.Column('chat_id', sa.Text(), nullable=True),
|
||||
sa.Column('status', sa.Text(), nullable=False),
|
||||
sa.Column('error', sa.Text(), nullable=True),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=False),
|
||||
)
|
||||
|
||||
inspector.clear_cache()
|
||||
if 'automation_run' in inspector.get_table_names():
|
||||
if not _index_exists(inspector, 'ix_automation_run_automation_id', 'automation_run'):
|
||||
op.create_index(
|
||||
'ix_automation_run_automation_id',
|
||||
'automation_run',
|
||||
['automation_id'],
|
||||
)
|
||||
op.create_table(
|
||||
'automation_run',
|
||||
sa.Column('id', sa.Text(), primary_key=True),
|
||||
sa.Column('automation_id', sa.Text(), nullable=False),
|
||||
sa.Column('chat_id', sa.Text(), nullable=True),
|
||||
sa.Column('status', sa.Text(), nullable=False),
|
||||
sa.Column('error', sa.Text(), nullable=True),
|
||||
sa.Column('created_at', sa.BigInteger(), nullable=False),
|
||||
)
|
||||
op.create_index(
|
||||
'ix_automation_run_automation_id',
|
||||
'automation_run',
|
||||
['automation_id'],
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
|
||||
@@ -6,8 +6,8 @@ Create Date: 2026-04-14 22:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = 'e1f2a3b4c5d6'
|
||||
down_revision = 'b7c8d9e0f1a2'
|
||||
@@ -16,12 +16,7 @@ depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
columns = [col['name'] for col in inspector.get_columns('note')]
|
||||
|
||||
if 'is_pinned' not in columns:
|
||||
op.add_column('note', sa.Column('is_pinned', sa.Boolean(), nullable=True))
|
||||
op.add_column('note', sa.Column('is_pinned', sa.Boolean(), nullable=True))
|
||||
|
||||
|
||||
def downgrade():
|
||||
|
||||
@@ -11,12 +11,13 @@ Access control semantics:
|
||||
- {read: {...}, write: {...}}: Custom permissions -> insert specific grants
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
import time
|
||||
import uuid
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from open_webui.migrations.util import get_existing_tables
|
||||
|
||||
revision: str = 'f1e2d3c4b5a6'
|
||||
@@ -80,17 +81,12 @@ def upgrade() -> None:
|
||||
if table_name not in existing_tables:
|
||||
continue
|
||||
|
||||
# Check if access_control and id columns exist (may already be dropped on re-run,
|
||||
# or table may have been rebuilt without id during intermediate migration states)
|
||||
insp = sa.inspect(conn)
|
||||
insp.clear_cache() # Ensure fresh metadata after prior migrations that rebuild tables
|
||||
table_cols = {c['name'] for c in insp.get_columns(table_name)}
|
||||
if 'access_control' not in table_cols or 'id' not in table_cols:
|
||||
continue
|
||||
|
||||
# Query all rows
|
||||
result = conn.execute(sa.text(f'SELECT id, access_control FROM "{table_name}"'))
|
||||
rows = result.fetchall()
|
||||
try:
|
||||
result = conn.execute(sa.text(f'SELECT id, access_control FROM "{table_name}"'))
|
||||
rows = result.fetchall()
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
for row in rows:
|
||||
resource_id = row[0]
|
||||
@@ -212,15 +208,15 @@ def upgrade() -> None:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Drop access_control columns from resource tables (only if column still exists)
|
||||
inspector = sa.inspect(conn)
|
||||
# Drop access_control columns from resource tables
|
||||
for table_name, _ in resource_tables:
|
||||
if table_name not in existing_tables:
|
||||
continue
|
||||
cols = {c['name'] for c in inspector.get_columns(table_name)}
|
||||
if 'access_control' in cols:
|
||||
try:
|
||||
with op.batch_alter_table(table_name) as batch:
|
||||
batch.drop_column('access_control')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
@@ -3,11 +3,13 @@ import time
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from open_webui.internal.db import Base, get_async_db_context
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from sqlalchemy import BigInteger, Column, Text, UniqueConstraint, and_, delete, or_, select
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy import select, delete
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from open_webui.internal.db import Base, get_async_db_context
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from sqlalchemy import BigInteger, Column, Text, UniqueConstraint, or_, and_
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -621,8 +623,8 @@ class AccessGrantsTable:
|
||||
Get all users who have the specified permission on a resource.
|
||||
Returns a list of UserModel instances.
|
||||
"""
|
||||
from open_webui.models.users import Users, UserModel
|
||||
from open_webui.models.groups import Groups
|
||||
from open_webui.models.users import UserModel, Users
|
||||
|
||||
async with get_async_db_context(db) as db:
|
||||
result = await db.execute(
|
||||
|
||||
+118
-145
@@ -1,56 +1,50 @@
|
||||
"""Auth credential models and data-access layer."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
import bcrypt
|
||||
from sqlalchemy import select, delete, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from open_webui.internal.db import Base, JSONField, get_async_db_context
|
||||
from open_webui.models.users import User, UserModel, UserProfileImageResponse, Users
|
||||
from open_webui.utils.validate import validate_profile_image_url
|
||||
from pydantic import BaseModel, field_validator
|
||||
from sqlalchemy import Boolean, Column, String, Text, delete, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import Boolean, Column, String, Text
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Pre-computed hash verified on signin paths that lack a real credential
|
||||
# (unknown user, inactive account) so response timing cannot reveal
|
||||
# whether an account exists (CWE-208).
|
||||
PLACEHOLDER_HASH = bcrypt.hashpw(b'placeholder', bcrypt.gensalt()).decode('utf-8')
|
||||
####################
|
||||
# DB MODEL
|
||||
####################
|
||||
|
||||
|
||||
class Auth(Base): # credential ↔ user linkage
|
||||
"""Maps a user ID to an email/password pair with an active flag."""
|
||||
|
||||
class Auth(Base):
|
||||
__tablename__ = 'auth'
|
||||
|
||||
id = Column(String, primary_key=True, unique=True) # mirrors User.id
|
||||
email = Column(String) # login address, kept in sync with User.email
|
||||
password = Column(Text) # argon2 / bcrypt hash
|
||||
active = Column(Boolean) # account soft-disable toggle
|
||||
id = Column(String, primary_key=True, unique=True)
|
||||
email = Column(String)
|
||||
password = Column(Text)
|
||||
active = Column(Boolean)
|
||||
|
||||
|
||||
class AuthModel(BaseModel):
|
||||
"""Pydantic mirror of the ``auth`` table row."""
|
||||
|
||||
id: str
|
||||
email: str
|
||||
password: str
|
||||
active: bool = True
|
||||
|
||||
|
||||
class Token(BaseModel):
|
||||
"""JWT bearer-token response wrapper."""
|
||||
####################
|
||||
# Forms
|
||||
####################
|
||||
|
||||
|
||||
class Token(BaseModel):
|
||||
token: str
|
||||
token_type: str
|
||||
|
||||
|
||||
class ApiKey(BaseModel):
|
||||
api_key: str | None = None
|
||||
api_key: Optional[str] = None
|
||||
|
||||
|
||||
class SigninResponse(Token, UserProfileImageResponse):
|
||||
@@ -80,26 +74,21 @@ class SignupForm(BaseModel):
|
||||
name: str
|
||||
email: str
|
||||
password: str
|
||||
profile_image_url: str | None = '/user.png'
|
||||
profile_image_url: Optional[str] = '/user.png'
|
||||
|
||||
@field_validator('profile_image_url')
|
||||
@classmethod
|
||||
def check_profile_image_url(cls, v: str | None) -> str | None:
|
||||
def check_profile_image_url(cls, v: Optional[str]) -> Optional[str]:
|
||||
if v is not None:
|
||||
return validate_profile_image_url(v)
|
||||
return v
|
||||
|
||||
|
||||
class AddUserForm(SignupForm):
|
||||
role: str | None = 'pending'
|
||||
|
||||
|
||||
# --- data-access layer ---
|
||||
role: Optional[str] = 'pending'
|
||||
|
||||
|
||||
class AuthsTable:
|
||||
"""Provides CRUD operations for the Auth ↔ User lifecycle."""
|
||||
|
||||
async def insert_new_auth(
|
||||
self,
|
||||
email: str,
|
||||
@@ -107,133 +96,117 @@ class AuthsTable:
|
||||
name: str,
|
||||
profile_image_url: str = '/user.png',
|
||||
role: str = 'pending',
|
||||
oauth: dict | None = None,
|
||||
db: AsyncSession | None = None,
|
||||
) -> UserModel | None:
|
||||
"""Create an Auth + User pair inside a single transaction."""
|
||||
async with get_async_db_context(db) as session:
|
||||
oauth: Optional[dict] = None,
|
||||
db: Optional[AsyncSession] = None,
|
||||
) -> Optional[UserModel]:
|
||||
async with get_async_db_context(db) as db:
|
||||
log.info('insert_new_auth')
|
||||
|
||||
new_id = str(uuid.uuid4())
|
||||
id = str(uuid.uuid4())
|
||||
|
||||
credential = Auth(
|
||||
id=new_id,
|
||||
email=email,
|
||||
password=password,
|
||||
active=True,
|
||||
)
|
||||
session.add(credential)
|
||||
auth = AuthModel(**{'id': id, 'email': email, 'password': password, 'active': True})
|
||||
result = Auth(**auth.model_dump())
|
||||
db.add(result)
|
||||
|
||||
created_user = await Users.insert_new_user(
|
||||
new_id,
|
||||
name,
|
||||
email,
|
||||
profile_image_url,
|
||||
role,
|
||||
oauth=oauth,
|
||||
db=session,
|
||||
)
|
||||
# persist both records and reload generated defaults
|
||||
await session.commit()
|
||||
await session.refresh(credential)
|
||||
return created_user if credential and created_user else None
|
||||
user = await Users.insert_new_user(id, name, email, profile_image_url, role, oauth=oauth, db=db)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(result)
|
||||
|
||||
if result and user:
|
||||
return user
|
||||
else:
|
||||
return None
|
||||
|
||||
async def authenticate_user(
|
||||
self,
|
||||
email: str,
|
||||
verify_password: callable,
|
||||
db: AsyncSession | None = None,
|
||||
) -> UserModel | None:
|
||||
"""Verify email + password credentials and return the matching user."""
|
||||
log.info('authenticate_user: %s', email)
|
||||
resolved = await Users.get_user_by_email(email, db=db)
|
||||
if not resolved:
|
||||
await verify_password(PLACEHOLDER_HASH)
|
||||
return
|
||||
# load the credential row and verify the password hash
|
||||
async with get_async_db_context(db) as session:
|
||||
credential = await session.get(Auth, resolved.id)
|
||||
if not credential or not credential.active:
|
||||
await verify_password(PLACEHOLDER_HASH)
|
||||
return
|
||||
if not await verify_password(credential.password):
|
||||
return
|
||||
return resolved
|
||||
self, email: str, verify_password: callable, db: Optional[AsyncSession] = None
|
||||
) -> Optional[UserModel]:
|
||||
log.info(f'authenticate_user: {email}')
|
||||
|
||||
user = await Users.get_user_by_email(email, db=db)
|
||||
if not user:
|
||||
return None
|
||||
|
||||
try:
|
||||
async with get_async_db_context(db) as db:
|
||||
result = await db.execute(select(Auth).filter_by(id=user.id, active=True))
|
||||
auth = result.scalars().first()
|
||||
if auth:
|
||||
if verify_password(auth.password):
|
||||
return user
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
async def authenticate_user_by_api_key(
|
||||
self,
|
||||
api_key: str,
|
||||
db: AsyncSession | None = None,
|
||||
) -> UserModel | None:
|
||||
"""Look up the user that owns the given API key."""
|
||||
log.info('authenticate_user_by_api_key')
|
||||
self, api_key: str, db: Optional[AsyncSession] = None
|
||||
) -> Optional[UserModel]:
|
||||
log.info(f'authenticate_user_by_api_key')
|
||||
# if no api_key, return None
|
||||
if not api_key:
|
||||
return
|
||||
# delegate to the Users model for the actual lookup
|
||||
return await Users.get_user_by_api_key(api_key, db=db)
|
||||
return None
|
||||
|
||||
async def authenticate_user_by_email(
|
||||
self,
|
||||
email: str,
|
||||
db: AsyncSession | None = None,
|
||||
) -> UserModel | None:
|
||||
"""Single-query auth via JOIN on Auth ↔ User, filtered by active flag."""
|
||||
log.info('authenticate_user_by_email: %s', email)
|
||||
# single JOIN avoids N+1 — returns (Auth, User) tuple or None
|
||||
async with get_async_db_context(db) as session:
|
||||
joined_query = (
|
||||
select(Auth, User).join(User, Auth.id == User.id).where(Auth.email == email, Auth.active.is_(True))
|
||||
)
|
||||
match = (await session.execute(joined_query)).first()
|
||||
if not match:
|
||||
return
|
||||
_, found_user = match
|
||||
return UserModel.model_validate(found_user)
|
||||
try:
|
||||
user = await Users.get_user_by_api_key(api_key, db=db)
|
||||
return user if user else None
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
async def update_email_by_id(
|
||||
self,
|
||||
user_id: str,
|
||||
email: str,
|
||||
db: AsyncSession | None = None,
|
||||
) -> bool:
|
||||
"""Set a new email on the auth record and propagate to the user row."""
|
||||
async with get_async_db_context(db) as session:
|
||||
auth_row = await session.get(Auth, user_id)
|
||||
if auth_row is None:
|
||||
async def authenticate_user_by_email(self, email: str, db: Optional[AsyncSession] = None) -> Optional[UserModel]:
|
||||
log.info(f'authenticate_user_by_email: {email}')
|
||||
try:
|
||||
async with get_async_db_context(db) as db:
|
||||
# Single JOIN query instead of two separate queries
|
||||
result = await db.execute(
|
||||
select(Auth, User).join(User, Auth.id == User.id).filter(Auth.email == email, Auth.active == True)
|
||||
)
|
||||
row = result.first()
|
||||
if row:
|
||||
_, user = row
|
||||
return UserModel.model_validate(user)
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
async def update_user_password_by_id(self, id: str, new_password: str, db: Optional[AsyncSession] = None) -> bool:
|
||||
try:
|
||||
async with get_async_db_context(db) as db:
|
||||
result = await db.execute(update(Auth).filter_by(id=id).values(password=new_password))
|
||||
await db.commit()
|
||||
return True if result.rowcount == 1 else False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
async def update_email_by_id(self, id: str, email: str, db: Optional[AsyncSession] = None) -> bool:
|
||||
try:
|
||||
async with get_async_db_context(db) as db:
|
||||
result = await db.execute(update(Auth).filter_by(id=id).values(email=email))
|
||||
await db.commit()
|
||||
if result.rowcount == 1:
|
||||
await Users.update_user_by_id(id, {'email': email}, db=db)
|
||||
return True
|
||||
return False
|
||||
auth_row.email = email
|
||||
await session.commit()
|
||||
await Users.update_user_by_id(user_id, {'email': email}, db=session)
|
||||
return True
|
||||
# --- password modification ---
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
async def update_user_password_by_id(
|
||||
self,
|
||||
user_id: str,
|
||||
new_password: str,
|
||||
db: AsyncSession | None = None,
|
||||
) -> bool:
|
||||
"""Set a new password hash for an existing user."""
|
||||
async with get_async_db_context(db) as session:
|
||||
auth_row = await session.get(Auth, user_id)
|
||||
if auth_row is None:
|
||||
return False
|
||||
auth_row.password = new_password
|
||||
await session.commit()
|
||||
return True
|
||||
async def delete_auth_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool:
|
||||
try:
|
||||
async with get_async_db_context(db) as db:
|
||||
# Delete User
|
||||
result = await Users.delete_user_by_id(id, db=db)
|
||||
|
||||
async def delete_auth_by_id(
|
||||
self,
|
||||
id: str,
|
||||
db: AsyncSession | None = None,
|
||||
) -> bool:
|
||||
"""Remove a user and their auth credential in one transaction."""
|
||||
async with get_async_db_context(db) as session:
|
||||
if not await Users.delete_user_by_id(id, db=session):
|
||||
return False
|
||||
await session.execute(delete(Auth).where(Auth.id == id))
|
||||
await session.commit()
|
||||
return True
|
||||
if result:
|
||||
await db.execute(delete(Auth).filter_by(id=id))
|
||||
await db.commit()
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
Auths = AuthsTable() # singleton — module-level instance
|
||||
Auths = AuthsTable()
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import logging
|
||||
import time
|
||||
import logging
|
||||
from typing import Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from open_webui.internal.db import Base, get_async_db_context
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from sqlalchemy import JSON, BigInteger, Boolean, Column, Index, String, Text, cast, delete, func, or_, select, update
|
||||
from sqlalchemy import Column, Text, JSON, Boolean, BigInteger, Index, select, or_, func, cast, String, delete, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from open_webui.internal.db import Base, get_async_db_context
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
import logging
|
||||
import time
|
||||
import logging
|
||||
from typing import Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from sqlalchemy import (
|
||||
Column,
|
||||
Text,
|
||||
JSON,
|
||||
Boolean,
|
||||
BigInteger,
|
||||
Index,
|
||||
UniqueConstraint,
|
||||
select,
|
||||
or_,
|
||||
exists,
|
||||
func,
|
||||
delete,
|
||||
update,
|
||||
)
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from open_webui.internal.db import Base, get_async_db_context
|
||||
from open_webui.models.access_grants import AccessGrantModel, AccessGrants
|
||||
from open_webui.models.groups import Groups
|
||||
from open_webui.models.users import User, UserModel, UserResponse
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from sqlalchemy import (
|
||||
JSON,
|
||||
BigInteger,
|
||||
Boolean,
|
||||
Column,
|
||||
Index,
|
||||
Text,
|
||||
UniqueConstraint,
|
||||
delete,
|
||||
exists,
|
||||
func,
|
||||
or_,
|
||||
select,
|
||||
update,
|
||||
)
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -394,16 +395,14 @@ class CalendarTable:
|
||||
# Delete events
|
||||
await db.execute(delete(CalendarEvent).filter(CalendarEvent.calendar_id == id))
|
||||
|
||||
# Delete access grants
|
||||
await AccessGrants.revoke_all_access('calendar', id, db=db)
|
||||
|
||||
# Delete calendar
|
||||
await db.execute(delete(Calendar).filter(Calendar.id == id))
|
||||
await db.commit()
|
||||
|
||||
# Revoke access grants in a separate transaction to avoid
|
||||
# write-lock contention on SQLite when session sharing is off.
|
||||
await AccessGrants.revoke_all_access('calendar', id)
|
||||
return True
|
||||
except Exception as e:
|
||||
log.exception(f'Failed to delete calendar {id}: {e}')
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
@@ -695,19 +694,12 @@ class CalendarEventTable:
|
||||
self,
|
||||
now_ns: int,
|
||||
default_lookahead_ns: int,
|
||||
grace_ns: int = 0,
|
||||
db: Optional[AsyncSession] = None,
|
||||
) -> list[tuple[CalendarEventModel, Optional[str]]]:
|
||||
"""Events starting between now and now + lookahead, for alert processing.
|
||||
|
||||
Per-event lookahead is read from meta.alert_minutes (falls back to
|
||||
default_lookahead_ns). Returns (event, user_timezone) pairs.
|
||||
|
||||
*grace_ns* widens the SQL lower bound so that events whose start_at
|
||||
is up to *grace_ns* nanoseconds in the past are still fetched. This
|
||||
ensures "At time of event" alerts (alert_minutes=0) are not missed
|
||||
when the scheduler polls a few seconds after the event's exact start
|
||||
time.
|
||||
"""
|
||||
from open_webui.models.users import User as UserRow
|
||||
|
||||
@@ -722,7 +714,7 @@ class CalendarEventTable:
|
||||
.outerjoin(UserRow, UserRow.id == CalendarEvent.user_id)
|
||||
.filter(
|
||||
CalendarEvent.is_cancelled == False,
|
||||
CalendarEvent.start_at >= now_ns - grace_ns,
|
||||
CalendarEvent.start_at >= now_ns,
|
||||
CalendarEvent.start_at <= upper,
|
||||
)
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user