Skip to content

Commit 2c3655a

Browse files
authored
Merge pull request open-webui#15909 from open-webui/dev
0.6.19
2 parents b8da4a8 + bfd8c6b commit 2c3655a

File tree

254 files changed

+12617
-4016
lines changed

Some content is hidden

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

254 files changed

+12617
-4016
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
x.py
2+
yarn.lock
13
.DS_Store
24
node_modules
35
/build
@@ -12,7 +14,8 @@ vite.config.ts.timestamp-*
1214
__pycache__/
1315
*.py[cod]
1416
*$py.class
15-
17+
.nvmrc
18+
CLAUDE.md
1619
# C extensions
1720
*.so
1821

CHANGELOG.md

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

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ For more information, be sure to check out our [Open WebUI Documentation](https:
3131

3232
- 🛡️ **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.
3333

34+
- 🔄 **SCIM 2.0 Support**: Enterprise-grade user and group provisioning through SCIM 2.0 protocol, enabling seamless integration with identity providers like Okta, Azure AD, and Google Workspace for automated user lifecycle management.
35+
3436
- 📱 **Responsive Design**: Enjoy a seamless experience across Desktop PC, Laptop, and Mobile devices.
3537

3638
- 📱 **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.

backend/dev.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
export CORS_ALLOW_ORIGIN=http://localhost:5173/
1+
export CORS_ALLOW_ORIGIN="http://localhost:5173"
22
PORT="${PORT:-8080}"
33
uvicorn open_webui.main:app --port $PORT --host 0.0.0.0 --forwarded-allow-ips '*' --reload

backend/open_webui/config.py

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

88
from datetime import datetime
99
from pathlib import Path
10-
from typing import Generic, Optional, TypeVar
10+
from typing import Generic, Union, Optional, TypeVar
1111
from urllib.parse import urlparse
1212

1313
import requests
@@ -168,9 +168,19 @@ def __init__(self, env_name: str, config_path: str, env_value: T):
168168
self.config_path = config_path
169169
self.env_value = env_value
170170
self.config_value = get_config_value(config_path)
171+
171172
if self.config_value is not None and ENABLE_PERSISTENT_CONFIG:
172-
log.info(f"'{env_name}' loaded from the latest database entry")
173-
self.value = self.config_value
173+
if (
174+
self.config_path.startswith("oauth.")
175+
and not ENABLE_OAUTH_PERSISTENT_CONFIG
176+
):
177+
log.info(
178+
f"Skipping loading of '{env_name}' as OAuth persistent config is disabled"
179+
)
180+
self.value = env_value
181+
else:
182+
log.info(f"'{env_name}' loaded from the latest database entry")
183+
self.value = self.config_value
174184
else:
175185
self.value = env_value
176186

@@ -213,21 +223,27 @@ def save(self):
213223

214224
class AppConfig:
215225
_state: dict[str, PersistentConfig]
216-
_redis: Optional[redis.Redis] = None
226+
_redis: Union[redis.Redis, redis.cluster.RedisCluster] = None
217227
_redis_key_prefix: str
218228

219229
def __init__(
220230
self,
221231
redis_url: Optional[str] = None,
222232
redis_sentinels: Optional[list] = [],
233+
redis_cluster: Optional[bool] = False,
223234
redis_key_prefix: str = "open-webui",
224235
):
225236
super().__setattr__("_state", {})
226237
super().__setattr__("_redis_key_prefix", redis_key_prefix)
227238
if redis_url:
228239
super().__setattr__(
229240
"_redis",
230-
get_redis_connection(redis_url, redis_sentinels, decode_responses=True),
241+
get_redis_connection(
242+
redis_url,
243+
redis_sentinels,
244+
redis_cluster,
245+
decode_responses=True,
246+
),
231247
)
232248

233249
def __setattr__(self, key, value):
@@ -296,6 +312,9 @@ def __getattr__(self, key):
296312
# OAuth config
297313
####################################
298314

315+
ENABLE_OAUTH_PERSISTENT_CONFIG = (
316+
os.environ.get("ENABLE_OAUTH_PERSISTENT_CONFIG", "True").lower() == "true"
317+
)
299318

300319
ENABLE_OAUTH_SIGNUP = PersistentConfig(
301320
"ENABLE_OAUTH_SIGNUP",
@@ -463,6 +482,12 @@ def __getattr__(self, key):
463482
os.environ.get("OAUTH_PROVIDER_NAME", "SSO"),
464483
)
465484

485+
OAUTH_SUB_CLAIM = PersistentConfig(
486+
"OAUTH_SUB_CLAIM",
487+
"oauth.oidc.sub_claim",
488+
os.environ.get("OAUTH_SUB_CLAIM", None),
489+
)
490+
466491
OAUTH_USERNAME_CLAIM = PersistentConfig(
467492
"OAUTH_USERNAME_CLAIM",
468493
"oauth.oidc.username_claim",
@@ -680,6 +705,23 @@ def oidc_oauth_register(client: OAuth):
680705
"register": oidc_oauth_register,
681706
}
682707

708+
configured_providers = []
709+
if GOOGLE_CLIENT_ID.value:
710+
configured_providers.append("Google")
711+
if MICROSOFT_CLIENT_ID.value:
712+
configured_providers.append("Microsoft")
713+
if GITHUB_CLIENT_ID.value:
714+
configured_providers.append("GitHub")
715+
716+
if configured_providers and not OPENID_PROVIDER_URL.value:
717+
provider_list = ", ".join(configured_providers)
718+
log.warning(
719+
f"⚠️ OAuth providers configured ({provider_list}) but OPENID_PROVIDER_URL not set - logout will not work!"
720+
)
721+
log.warning(
722+
f"Set OPENID_PROVIDER_URL to your OAuth provider's OpenID Connect discovery endpoint to fix logout functionality."
723+
)
724+
683725

684726
load_oauth_providers()
685727

@@ -1143,10 +1185,18 @@ def oidc_oauth_register(client: OAuth):
11431185
os.environ.get("USER_PERMISSIONS_CHAT_CONTROLS", "True").lower() == "true"
11441186
)
11451187

1188+
USER_PERMISSIONS_CHAT_VALVES = (
1189+
os.environ.get("USER_PERMISSIONS_CHAT_VALVES", "True").lower() == "true"
1190+
)
1191+
11461192
USER_PERMISSIONS_CHAT_SYSTEM_PROMPT = (
11471193
os.environ.get("USER_PERMISSIONS_CHAT_SYSTEM_PROMPT", "True").lower() == "true"
11481194
)
11491195

1196+
USER_PERMISSIONS_CHAT_PARAMS = (
1197+
os.environ.get("USER_PERMISSIONS_CHAT_PARAMS", "True").lower() == "true"
1198+
)
1199+
11501200
USER_PERMISSIONS_CHAT_FILE_UPLOAD = (
11511201
os.environ.get("USER_PERMISSIONS_CHAT_FILE_UPLOAD", "True").lower() == "true"
11521202
)
@@ -1232,7 +1282,9 @@ def oidc_oauth_register(client: OAuth):
12321282
},
12331283
"chat": {
12341284
"controls": USER_PERMISSIONS_CHAT_CONTROLS,
1285+
"valves": USER_PERMISSIONS_CHAT_VALVES,
12351286
"system_prompt": USER_PERMISSIONS_CHAT_SYSTEM_PROMPT,
1287+
"params": USER_PERMISSIONS_CHAT_PARAMS,
12361288
"file_upload": USER_PERMISSIONS_CHAT_FILE_UPLOAD,
12371289
"delete": USER_PERMISSIONS_CHAT_DELETE,
12381290
"edit": USER_PERMISSIONS_CHAT_EDIT,
@@ -1299,6 +1351,10 @@ def oidc_oauth_register(client: OAuth):
12991351

13001352
ENABLE_ADMIN_EXPORT = os.environ.get("ENABLE_ADMIN_EXPORT", "True").lower() == "true"
13011353

1354+
ENABLE_ADMIN_WORKSPACE_CONTENT_ACCESS = (
1355+
os.environ.get("ENABLE_ADMIN_WORKSPACE_CONTENT_ACCESS", "True").lower() == "true"
1356+
)
1357+
13021358
ENABLE_ADMIN_CHAT_ACCESS = (
13031359
os.environ.get("ENABLE_ADMIN_CHAT_ACCESS", "True").lower() == "true"
13041360
)
@@ -1337,10 +1393,11 @@ def oidc_oauth_register(client: OAuth):
13371393
def validate_cors_origin(origin):
13381394
parsed_url = urlparse(origin)
13391395

1340-
# Check if the scheme is either http or https
1341-
if parsed_url.scheme not in ["http", "https"]:
1396+
# Check if the scheme is either http or https, or a custom scheme
1397+
schemes = ["http", "https"] + CORS_ALLOW_CUSTOM_SCHEME
1398+
if parsed_url.scheme not in schemes:
13421399
raise ValueError(
1343-
f"Invalid scheme in CORS_ALLOW_ORIGIN: '{origin}'. Only 'http' and 'https' are allowed."
1400+
f"Invalid scheme in CORS_ALLOW_ORIGIN: '{origin}'. Only 'http' and 'https' and CORS_ALLOW_CUSTOM_SCHEME are allowed."
13441401
)
13451402

13461403
# Ensure that the netloc (domain + port) is present, indicating it's a valid URL
@@ -1355,6 +1412,11 @@ def validate_cors_origin(origin):
13551412
# in your .env file depending on your frontend port, 5173 in this case.
13561413
CORS_ALLOW_ORIGIN = os.environ.get("CORS_ALLOW_ORIGIN", "*").split(";")
13571414

1415+
# Allows custom URL schemes (e.g., app://) to be used as origins for CORS.
1416+
# Useful for local development or desktop clients with schemes like app:// or other custom protocols.
1417+
# Provide a semicolon-separated list of allowed schemes in the environment variable CORS_ALLOW_CUSTOM_SCHEMES.
1418+
CORS_ALLOW_CUSTOM_SCHEME = os.environ.get("CORS_ALLOW_CUSTOM_SCHEME", "").split(";")
1419+
13581420
if CORS_ALLOW_ORIGIN == ["*"]:
13591421
log.warning(
13601422
"\n\nWARNING: CORS_ALLOW_ORIGIN IS SET TO '*' - NOT RECOMMENDED FOR PRODUCTION DEPLOYMENTS.\n"
@@ -1862,6 +1924,8 @@ class BannerModel(BaseModel):
18621924
QDRANT_ON_DISK = os.environ.get("QDRANT_ON_DISK", "false").lower() == "true"
18631925
QDRANT_PREFER_GRPC = os.environ.get("QDRANT_PREFER_GRPC", "false").lower() == "true"
18641926
QDRANT_GRPC_PORT = int(os.environ.get("QDRANT_GRPC_PORT", "6334"))
1927+
QDRANT_TIMEOUT = int(os.environ.get("QDRANT_TIMEOUT", "5"))
1928+
QDRANT_HNSW_M = int(os.environ.get("QDRANT_HNSW_M", "16"))
18651929
ENABLE_QDRANT_MULTITENANCY_MODE = (
18661930
os.environ.get("ENABLE_QDRANT_MULTITENANCY_MODE", "true").lower() == "true"
18671931
)
@@ -1951,6 +2015,37 @@ class BannerModel(BaseModel):
19512015
PINECONE_METRIC = os.getenv("PINECONE_METRIC", "cosine")
19522016
PINECONE_CLOUD = os.getenv("PINECONE_CLOUD", "aws") # or "gcp" or "azure"
19532017

2018+
# ORACLE23AI (Oracle23ai Vector Search)
2019+
2020+
ORACLE_DB_USE_WALLET = os.environ.get("ORACLE_DB_USE_WALLET", "false").lower() == "true"
2021+
ORACLE_DB_USER = os.environ.get("ORACLE_DB_USER", None) #
2022+
ORACLE_DB_PASSWORD = os.environ.get("ORACLE_DB_PASSWORD", None) #
2023+
ORACLE_DB_DSN = os.environ.get("ORACLE_DB_DSN", None) #
2024+
ORACLE_WALLET_DIR = os.environ.get("ORACLE_WALLET_DIR", None)
2025+
ORACLE_WALLET_PASSWORD = os.environ.get("ORACLE_WALLET_PASSWORD", None)
2026+
ORACLE_VECTOR_LENGTH = os.environ.get("ORACLE_VECTOR_LENGTH", 768)
2027+
2028+
ORACLE_DB_POOL_MIN = int(os.environ.get("ORACLE_DB_POOL_MIN", 2))
2029+
ORACLE_DB_POOL_MAX = int(os.environ.get("ORACLE_DB_POOL_MAX", 10))
2030+
ORACLE_DB_POOL_INCREMENT = int(os.environ.get("ORACLE_DB_POOL_INCREMENT", 1))
2031+
2032+
2033+
if VECTOR_DB == "oracle23ai":
2034+
if not ORACLE_DB_USER or not ORACLE_DB_PASSWORD or not ORACLE_DB_DSN:
2035+
raise ValueError(
2036+
"Oracle23ai requires setting ORACLE_DB_USER, ORACLE_DB_PASSWORD, and ORACLE_DB_DSN."
2037+
)
2038+
if ORACLE_DB_USE_WALLET and (not ORACLE_WALLET_DIR or not ORACLE_WALLET_PASSWORD):
2039+
raise ValueError(
2040+
"Oracle23ai requires setting ORACLE_WALLET_DIR and ORACLE_WALLET_PASSWORD when using wallet authentication."
2041+
)
2042+
2043+
log.info(f"VECTOR_DB: {VECTOR_DB}")
2044+
2045+
# S3 Vector
2046+
S3_VECTOR_BUCKET_NAME = os.environ.get("S3_VECTOR_BUCKET_NAME", None)
2047+
S3_VECTOR_REGION = os.environ.get("S3_VECTOR_REGION", None)
2048+
19542049
####################################
19552050
# Information Retrieval (RAG)
19562051
####################################
@@ -2012,10 +2107,16 @@ class BannerModel(BaseModel):
20122107
os.environ.get("DATALAB_MARKER_API_KEY", ""),
20132108
)
20142109

2015-
DATALAB_MARKER_LANGS = PersistentConfig(
2016-
"DATALAB_MARKER_LANGS",
2017-
"rag.datalab_marker_langs",
2018-
os.environ.get("DATALAB_MARKER_LANGS", ""),
2110+
DATALAB_MARKER_API_BASE_URL = PersistentConfig(
2111+
"DATALAB_MARKER_API_BASE_URL",
2112+
"rag.datalab_marker_api_base_url",
2113+
os.environ.get("DATALAB_MARKER_API_BASE_URL", ""),
2114+
)
2115+
2116+
DATALAB_MARKER_ADDITIONAL_CONFIG = PersistentConfig(
2117+
"DATALAB_MARKER_ADDITIONAL_CONFIG",
2118+
"rag.datalab_marker_additional_config",
2119+
os.environ.get("DATALAB_MARKER_ADDITIONAL_CONFIG", ""),
20192120
)
20202121

20212122
DATALAB_MARKER_USE_LLM = PersistentConfig(
@@ -2055,6 +2156,12 @@ class BannerModel(BaseModel):
20552156
== "true",
20562157
)
20572158

2159+
DATALAB_MARKER_FORMAT_LINES = PersistentConfig(
2160+
"DATALAB_MARKER_FORMAT_LINES",
2161+
"rag.datalab_marker_format_lines",
2162+
os.environ.get("DATALAB_MARKER_FORMAT_LINES", "false").lower() == "true",
2163+
)
2164+
20582165
DATALAB_MARKER_OUTPUT_FORMAT = PersistentConfig(
20592166
"DATALAB_MARKER_OUTPUT_FORMAT",
20602167
"rag.datalab_marker_output_format",

0 commit comments

Comments
 (0)