Skip to content

Commit 2ed3055

Browse files
authored
Merge pull request open-webui#21618 from open-webui/dev
0.8.4
2 parents b8112d7 + 1792f66 commit 2ed3055

349 files changed

Lines changed: 8286 additions & 2436 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/docker-build.yaml

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -804,3 +804,109 @@ jobs:
804804
- name: Inspect image
805805
run: |
806806
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
807+
808+
# Copy images from GHCR to Docker Hub (best-effort, won't block GHCR)
809+
copy-to-dockerhub:
810+
runs-on: ubuntu-latest
811+
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
812+
needs: [merge-main-images, merge-cuda-images, merge-cuda126-images, merge-ollama-images, merge-slim-images]
813+
continue-on-error: true
814+
strategy:
815+
fail-fast: false
816+
matrix:
817+
include:
818+
- variant: main
819+
suffix: ""
820+
- variant: cuda
821+
suffix: "-cuda"
822+
- variant: cuda126
823+
suffix: "-cuda126"
824+
- variant: ollama
825+
suffix: "-ollama"
826+
- variant: slim
827+
suffix: "-slim"
828+
steps:
829+
- name: Set repository and image name to lowercase
830+
run: |
831+
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
832+
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
833+
env:
834+
IMAGE_NAME: '${{ github.repository }}'
835+
836+
- name: Set up Docker Buildx
837+
uses: docker/setup-buildx-action@v3
838+
839+
- name: Log in to the Container registry
840+
uses: docker/login-action@v3
841+
with:
842+
registry: ${{ env.REGISTRY }}
843+
username: ${{ github.actor }}
844+
password: ${{ secrets.GITHUB_TOKEN }}
845+
846+
- name: Log in to Docker Hub
847+
uses: docker/login-action@v3
848+
with:
849+
username: ${{ secrets.DOCKERHUB_USERNAME }}
850+
password: ${{ secrets.DOCKERHUB_TOKEN }}
851+
852+
- name: Determine source and destination tags
853+
id: tags
854+
run: |
855+
DOCKERHUB_IMAGE="openwebui/open-webui"
856+
SUFFIX="${{ matrix.suffix }}"
857+
858+
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
859+
# For version tags: copy version tag and major.minor tag
860+
VERSION="${{ github.ref_name }}"
861+
VERSION="${VERSION#v}"
862+
MAJOR_MINOR="${VERSION%.*}"
863+
864+
echo "tags<<EOF" >> $GITHUB_OUTPUT
865+
echo "${VERSION}${SUFFIX}" >> $GITHUB_OUTPUT
866+
echo "${MAJOR_MINOR}${SUFFIX}" >> $GITHUB_OUTPUT
867+
echo "EOF" >> $GITHUB_OUTPUT
868+
else
869+
# For main branch
870+
if [ -z "$SUFFIX" ]; then
871+
echo "tags=latest" >> $GITHUB_OUTPUT
872+
else
873+
# e.g. latest-cuda -> also tag as just "cuda"
874+
VARIANT_NAME="${SUFFIX#-}"
875+
echo "tags<<EOF" >> $GITHUB_OUTPUT
876+
echo "latest${SUFFIX}" >> $GITHUB_OUTPUT
877+
echo "${VARIANT_NAME}" >> $GITHUB_OUTPUT
878+
echo "EOF" >> $GITHUB_OUTPUT
879+
fi
880+
fi
881+
882+
echo "dockerhub_image=${DOCKERHUB_IMAGE}" >> $GITHUB_OUTPUT
883+
884+
- name: Copy images from GHCR to Docker Hub
885+
run: |
886+
DOCKERHUB_IMAGE="${{ steps.tags.outputs.dockerhub_image }}"
887+
SUFFIX="${{ matrix.suffix }}"
888+
889+
# Determine the source tag on GHCR
890+
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
891+
VERSION="${{ github.ref_name }}"
892+
VERSION="${VERSION#v}"
893+
SOURCE_TAG="${VERSION}${SUFFIX}"
894+
else
895+
if [ -z "$SUFFIX" ]; then
896+
SOURCE_TAG="latest"
897+
else
898+
SOURCE_TAG="latest${SUFFIX}"
899+
fi
900+
fi
901+
902+
SOURCE="${{ env.FULL_IMAGE_NAME }}:${SOURCE_TAG}"
903+
904+
echo "Copying from ${SOURCE} to Docker Hub..."
905+
906+
# Copy each destination tag
907+
while IFS= read -r TAG; do
908+
[ -z "$TAG" ] && continue
909+
DEST="${DOCKERHUB_IMAGE}:${TAG}"
910+
echo " -> ${DEST}"
911+
docker buildx imagetools create -t "${DEST}" "${SOURCE}"
912+
done <<< "${{ steps.tags.outputs.tags }}"

CHANGELOG.md

Lines changed: 91 additions & 0 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
![Open WebUI Banner](./banner.png)
1414

15-
**Open WebUI is an [extensible](https://docs.openwebui.com/features/plugin/), feature-rich, and user-friendly self-hosted AI platform designed to operate entirely offline.** It supports various LLM runners like **Ollama** and **OpenAI-compatible APIs**, with **built-in inference engine** for RAG, making it a **powerful AI deployment solution**.
15+
**Open WebUI is an [extensible](https://docs.openwebui.com/features/extensibility/plugin), feature-rich, and user-friendly self-hosted AI platform designed to operate entirely offline.** It supports various LLM runners like **Ollama** and **OpenAI-compatible APIs**, with **built-in inference engine** for RAG, making it a **powerful AI deployment solution**.
1616

1717
Passionate about open-source AI? [Join our team →](https://careers.openwebui.com/)
1818

@@ -172,7 +172,7 @@ After installation, you can access Open WebUI at [http://localhost:3000](http://
172172

173173
We offer various installation alternatives, including non-Docker native installation methods, Docker Compose, Kustomize, and Helm. Visit our [Open WebUI Documentation](https://docs.openwebui.com/getting-started/) or join our [Discord community](https://discord.gg/5rJgQTnV4s) for comprehensive guidance.
174174

175-
Look at the [Local Development Guide](https://docs.openwebui.com/getting-started/advanced-topics/development) for instructions on setting up a local development environment.
175+
Look at the [Local Development Guide](https://docs.openwebui.com/getting-started/development) for instructions on setting up a local development environment.
176176

177177
### Troubleshooting
178178

backend/open_webui/config.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,20 @@ def __getattr__(self, key):
573573
)
574574

575575

576+
oauth_group_default_share = (
577+
os.environ.get("OAUTH_GROUP_DEFAULT_SHARE", "true").strip().lower()
578+
)
579+
OAUTH_GROUP_DEFAULT_SHARE = PersistentConfig(
580+
"OAUTH_GROUP_DEFAULT_SHARE",
581+
"oauth.group_default_share",
582+
(
583+
"members"
584+
if oauth_group_default_share == "members"
585+
else oauth_group_default_share == "true"
586+
),
587+
)
588+
589+
576590
OAUTH_BLOCKED_GROUPS = PersistentConfig(
577591
"OAUTH_BLOCKED_GROUPS",
578592
"oauth.blocked_groups",
@@ -1249,6 +1263,18 @@ def reachable(host: str, port: int) -> bool:
12491263
[],
12501264
)
12511265

1266+
DEFAULT_MODEL_METADATA = PersistentConfig(
1267+
"DEFAULT_MODEL_METADATA",
1268+
"models.default_metadata",
1269+
{},
1270+
)
1271+
1272+
DEFAULT_MODEL_PARAMS = PersistentConfig(
1273+
"DEFAULT_MODEL_PARAMS",
1274+
"models.default_params",
1275+
{},
1276+
)
1277+
12521278
DEFAULT_USER_ROLE = PersistentConfig(
12531279
"DEFAULT_USER_ROLE",
12541280
"ui.default_user_role",
@@ -1428,6 +1454,10 @@ def reachable(host: str, port: int) -> bool:
14281454
os.environ.get("USER_PERMISSIONS_CHAT_FILE_UPLOAD", "True").lower() == "true"
14291455
)
14301456

1457+
USER_PERMISSIONS_CHAT_WEB_UPLOAD = (
1458+
os.environ.get("USER_PERMISSIONS_CHAT_WEB_UPLOAD", "True").lower() == "true"
1459+
)
1460+
14311461
USER_PERMISSIONS_CHAT_DELETE = (
14321462
os.environ.get("USER_PERMISSIONS_CHAT_DELETE", "True").lower() == "true"
14331463
)
@@ -1566,6 +1596,7 @@ def reachable(host: str, port: int) -> bool:
15661596
"system_prompt": USER_PERMISSIONS_CHAT_SYSTEM_PROMPT,
15671597
"params": USER_PERMISSIONS_CHAT_PARAMS,
15681598
"file_upload": USER_PERMISSIONS_CHAT_FILE_UPLOAD,
1599+
"web_upload": USER_PERMISSIONS_CHAT_WEB_UPLOAD,
15691600
"delete": USER_PERMISSIONS_CHAT_DELETE,
15701601
"delete_message": USER_PERMISSIONS_CHAT_DELETE_MESSAGE,
15711602
"continue_response": USER_PERMISSIONS_CHAT_CONTINUE_RESPONSE,
@@ -1678,6 +1709,10 @@ def reachable(host: str, port: int) -> bool:
16781709
os.environ.get("ENABLE_ADMIN_CHAT_ACCESS", "True").lower() == "true"
16791710
)
16801711

1712+
ENABLE_ADMIN_ANALYTICS = (
1713+
os.environ.get("ENABLE_ADMIN_ANALYTICS", "True").lower() == "true"
1714+
)
1715+
16811716
ENABLE_COMMUNITY_SHARING = PersistentConfig(
16821717
"ENABLE_COMMUNITY_SHARING",
16831718
"ui.enable_community_sharing",
@@ -2903,6 +2938,12 @@ class BannerModel(BaseModel):
29032938
os.environ.get("ENABLE_ASYNC_EMBEDDING", "True").lower() == "true",
29042939
)
29052940

2941+
RAG_EMBEDDING_CONCURRENT_REQUESTS = PersistentConfig(
2942+
"RAG_EMBEDDING_CONCURRENT_REQUESTS",
2943+
"rag.embedding_concurrent_requests",
2944+
int(os.getenv("RAG_EMBEDDING_CONCURRENT_REQUESTS", "0")),
2945+
)
2946+
29062947
RAG_EMBEDDING_QUERY_PREFIX = os.environ.get("RAG_EMBEDDING_QUERY_PREFIX", None)
29072948

29082949
RAG_EMBEDDING_CONTENT_PREFIX = os.environ.get("RAG_EMBEDDING_CONTENT_PREFIX", None)
@@ -3485,6 +3526,12 @@ class BannerModel(BaseModel):
34853526
os.environ.get("YANDEX_WEB_SEARCH_CONFIG", ""),
34863527
)
34873528

3529+
YOUCOM_API_KEY = PersistentConfig(
3530+
"YOUCOM_API_KEY",
3531+
"rag.web.search.youcom_api_key",
3532+
os.environ.get("YOUCOM_API_KEY", ""),
3533+
)
3534+
34883535
####################################
34893536
# Images
34903537
####################################

backend/open_webui/env.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
import pkgutil
66
import sys
77
import shutil
8+
import traceback
9+
from datetime import datetime, timezone
10+
from typing import Any
811
from uuid import uuid4
912
from pathlib import Path
1013
from cryptography.hazmat.primitives import serialization
@@ -72,9 +75,51 @@
7275
# LOGGING
7376
####################################
7477

78+
_LEVEL_MAP = {
79+
"DEBUG": "debug",
80+
"INFO": "info",
81+
"WARNING": "warn",
82+
"ERROR": "error",
83+
"CRITICAL": "fatal",
84+
}
85+
86+
87+
class JSONFormatter(logging.Formatter):
88+
"""Format log records as single-line JSON objects for structured logging."""
89+
90+
def format(self, record: logging.LogRecord) -> str:
91+
log_entry: dict[str, Any] = {
92+
"ts": datetime.fromtimestamp(record.created, tz=timezone.utc).isoformat(
93+
timespec="milliseconds"
94+
),
95+
"level": _LEVEL_MAP.get(record.levelname, record.levelname.lower()),
96+
"msg": record.getMessage(),
97+
"caller": record.name,
98+
}
99+
100+
if record.exc_info and record.exc_info[0] is not None:
101+
log_entry["error"] = "".join(
102+
traceback.format_exception(*record.exc_info)
103+
).rstrip()
104+
elif record.exc_text:
105+
log_entry["error"] = record.exc_text
106+
107+
if record.stack_info:
108+
log_entry["stacktrace"] = record.stack_info
109+
110+
return json.dumps(log_entry, ensure_ascii=False, default=str)
111+
112+
113+
LOG_FORMAT = os.environ.get("LOG_FORMAT", "").lower()
114+
75115
GLOBAL_LOG_LEVEL = os.environ.get("GLOBAL_LOG_LEVEL", "").upper()
76116
if GLOBAL_LOG_LEVEL in logging.getLevelNamesMapping():
77-
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL, force=True)
117+
if LOG_FORMAT == "json":
118+
_handler = logging.StreamHandler(sys.stdout)
119+
_handler.setFormatter(JSONFormatter())
120+
logging.basicConfig(handlers=[_handler], level=GLOBAL_LOG_LEVEL, force=True)
121+
else:
122+
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL, force=True)
78123
else:
79124
GLOBAL_LOG_LEVEL = "INFO"
80125

@@ -557,6 +602,10 @@ def parse_section(section):
557602
"OAUTH_SESSION_TOKEN_ENCRYPTION_KEY", WEBUI_SECRET_KEY
558603
)
559604

605+
# Maximum number of concurrent OAuth sessions per user per provider
606+
# This prevents unbounded session growth while allowing multi-device usage
607+
OAUTH_MAX_SESSIONS_PER_USER = int(os.environ.get("OAUTH_MAX_SESSIONS_PER_USER", "10"))
608+
560609
# Token Exchange Configuration
561610
# Allows external apps to exchange OAuth tokens for OpenWebUI tokens
562611
ENABLE_OAUTH_TOKEN_EXCHANGE = (
@@ -978,6 +1027,11 @@ def parse_section(section):
9781027
# TOOLS/FUNCTIONS PIP OPTIONS
9791028
####################################
9801029

1030+
ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS = (
1031+
os.environ.get("ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS", "True").lower()
1032+
== "true"
1033+
)
1034+
9811035
PIP_OPTIONS = os.getenv("PIP_OPTIONS", "").split()
9821036
PIP_PACKAGE_INDEX_OPTIONS = os.getenv("PIP_PACKAGE_INDEX_OPTIONS", "").split()
9831037

0 commit comments

Comments
 (0)