Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
168 commits
Select commit Hold shift + click to select a range
e9017c7
i18n: update and improve it-IT (#30297)
andreadegiovine Sep 21, 2026
7eebbbb
fix: stop the MinerU API key blocking a save in local mode (#30299)
silentoplayz Sep 21, 2026
9012bd1
fix: stop sending count to the Staan search API (#30303)
Classic298 Sep 21, 2026
aa65f55
fix: insert the whole emoji when its shortcode is a codepoint sequenc…
silentoplayz Sep 21, 2026
5399e39
fix: enforce the account form's required fields the way the admin use…
silentoplayz Sep 21, 2026
f16e9ea
feat: fall back to an available model when a chat's models are gone (…
Classic298 Sep 21, 2026
f5967ae
fix: restore chat input draft after reload in chats started from the …
Classic298 Sep 21, 2026
c2f11ed
fix: keep a workspace model's base model when cloning it from the adm…
silentoplayz Sep 21, 2026
dc4f5da
refac
tjbck Sep 21, 2026
7f703dc
refac
tjbck Sep 21, 2026
440d13e
fix: refresh the prompt editor after a version is set as production (…
silentoplayz Sep 21, 2026
f40318c
fix: keep the live prompt unchanged when a version is saved without S…
silentoplayz Sep 21, 2026
d9ea46f
fix: hide Insert into note on a note the viewer may only read (#30223)
silentoplayz Sep 21, 2026
7fa8673
fix: resolve this instance's own file URLs in edit_image regardless o…
Classic298 Sep 21, 2026
0180efe
refac
tjbck Sep 21, 2026
438d9db
fix: correct recurrence rule parsing for schedules and calendar event…
Classic298 Sep 21, 2026
6fb68e4
refac(images): request an unencoded body when fetching remote chat im…
Classic298 Sep 21, 2026
488b3c5
perf: stop note co-editing echoing every remote update back to the se…
Classic298 Sep 21, 2026
5fb869d
refac
tjbck Sep 21, 2026
49b2550
perf: stop round-tripping the whole chat to read or write one message…
Classic298 Sep 21, 2026
aceda89
perf: serve the shared model pool from a per-worker cache (#28176)
Classic298 Sep 21, 2026
a9541c1
refac
tjbck Sep 21, 2026
bc50026
fix: make the Delete Chat shortcut work whenever a chat is open, not …
silentoplayz Sep 21, 2026
c864e3a
fix: stop asking for chat variables a model's system prompt no longer…
silentoplayz Sep 21, 2026
754c4b5
refac
tjbck Sep 21, 2026
e8bd066
refac
tjbck Sep 21, 2026
9688d32
fix: keep the background image chosen while creating a folder from th…
silentoplayz Sep 21, 2026
94eea41
fix: surface searchapi errors, news results and redirect links (#30308)
Classic298 Sep 21, 2026
3fc1146
refac
tjbck Sep 21, 2026
419d248
refac: check the timer owner's role before running a due timer (#30220)
Classic298 Sep 21, 2026
30881db
refac
tjbck Sep 21, 2026
478d178
refac
tjbck Sep 21, 2026
e8c26f8
fix: stop the background memory review when memory is switched off (#…
Classic298 Sep 21, 2026
9fba284
refac
tjbck Sep 21, 2026
4b61b86
fix: apply the knowledge File content filter on the first click (#30211)
silentoplayz Sep 21, 2026
b988f06
fix: drop every local reference to a channel message that was deleted…
silentoplayz Sep 21, 2026
bf47645
chore: format
tjbck Sep 21, 2026
c07fa08
refac
tjbck Sep 21, 2026
ee3ece1
refac
tjbck Sep 21, 2026
46ea826
refac: keep rendered diagrams and SVG on same-origin resources (#30271)
Classic298 Sep 21, 2026
9530cc1
i18n: restore placeholder names that were translated or lost their br…
Na5co Sep 21, 2026
33df4a7
chore: changelog (#29874)
Classic298 Sep 21, 2026
ca75b4d
refac
tjbck Sep 21, 2026
344ea53
doc: changelog
tjbck Sep 21, 2026
fecbeac
Merge pull request #30354 from Classic298/i18n-en-us-fallback
Classic298 Sep 22, 2026
6f6792d
fix: pass MCP tool images to the model, not only to the UI (#30358)
Classic298 Sep 22, 2026
f956f7a
fix: send only the file name to Docling instead of the full storage p…
Classic298 Sep 22, 2026
6b36f62
fix: stop KeyError 'model' traceback on every new chat from initial t…
Classic298 Sep 22, 2026
0a2e9a4
fix: detect the real image type of bare base64 generated images (#30359)
Classic298 Sep 22, 2026
69fcad0
i18n: complete Indonesian (id-ID) translations (#30328)
DarRahman Sep 22, 2026
845b791
i18n: translate Persian counters and preserve prompt variable (#30329)
MiRHaDi Sep 22, 2026
f2702e1
fix: fall back to the user's system prompt when the chat's own is cle…
silentoplayz Sep 22, 2026
e93a59f
refac
tjbck Sep 22, 2026
fe56ab2
fix: page Chroma get() so hybrid search works on collections over 32k…
Classic298 Sep 22, 2026
1a74a9f
perf: per-room channel delivery for the socket.io Redis manager (#28818)
Classic298 Sep 22, 2026
d603b57
fix: FireFox regional setting (#30377)
Classic298 Sep 22, 2026
15f350b
fix: keep an open chat's tools when clicking a folder name (#30376)
Classic298 Sep 22, 2026
2f92635
fix: keep the model system prompt on Ollama tool-call follow-ups (#30…
Classic298 Sep 22, 2026
24c01bd
fix: stop voice call hanging in "speaking" when TTS fails (#30372)
Classic298 Sep 22, 2026
d66e5dc
fix: make the admin analytics tab scrollable inside the settings moda…
Classic298 Sep 23, 2026
9db1a51
fix: refresh an expiring OAuth token once when requests race (#30426)
Classic298 Sep 23, 2026
abd60d3
fix: evaluate automation schedules on Windows with PostgreSQL (#30424)
Classic298 Sep 23, 2026
c86c46e
i18n: Update catalan translation.json (#30415)
aleixdorca Sep 24, 2026
53c7ed3
i18n: add missing french translation in fr_FR (#30403)
CosmicFlavour Sep 24, 2026
b3bb82e
refac: scope ydoc update save scheduling to note documents (#30395)
Classic298 Sep 24, 2026
869a5b1
fix: stop voice mode listening after leaving the chat page (#30405) (…
Classic298 Sep 24, 2026
9e51fe4
fix: read S3 files with long non-ASCII names (#30418)
Classic298 Sep 24, 2026
95d6af4
fix: mark imported and cloned chats as read (#30754)
silentoplayz Sep 24, 2026
a7d81eb
fix: keep a user's chats listed in the admin chats view after deletin…
silentoplayz Sep 24, 2026
7e2bea8
fix: load the leaderboard activity chart for model ids that contain a…
silentoplayz Sep 24, 2026
0ab3a2b
fix: restore tag rows after unarchiving all chats and remove unused o…
silentoplayz Sep 24, 2026
4caf255
fix: refresh an expiring OAuth token once across workers and replicas…
Classic298 Sep 24, 2026
a973e77
refac: folder file checks (#30442)
Classic298 Sep 24, 2026
443736e
refactor: use secrets module for generated secret key (#30441)
Classic298 Sep 24, 2026
2867f72
refac: sync channel room on member removal (#30446)
Classic298 Sep 24, 2026
9db6898
feat: allow reordering admin models while a search or filter is activ…
Classic298 Sep 24, 2026
744ce6c
fix: send the configured USER_AGENT on the Attach Webpage pre-check (…
Classic298 Sep 24, 2026
e13e523
feat: bundle python-pptx and python-docx for the Pyodide code interpr…
Classic298 Sep 24, 2026
60ded56
fix: load models before resolving automation model defaults (#30379)
Classic298 Sep 24, 2026
4e4b324
fix: skip the pyodide plt.show patch when matplotlib is not loaded (#…
Classic298 Sep 24, 2026
1b5a8ac
fix: keep model default features when comparing multiple models (#30383)
Classic298 Sep 24, 2026
7924ce7
fix: unhandled play() rejection wedges the Read Aloud button; auto-pl…
Classic298 Sep 24, 2026
f7ce402
fix: keep requested MCP OAuth scope when DCR response omits it (#30384)
Classic298 Sep 24, 2026
b6191a0
fix: stop storing MCP image/audio base64 in file metadata (#30419)
Classic298 Sep 24, 2026
f729416
fix: show ComfyUI images saved by the Save Image (Advanced) node (#30…
Classic298 Sep 24, 2026
14c5b65
refac: tighten details and image patterns in background task message …
Classic298 Sep 24, 2026
e14f245
fix: keep the M mute shortcut working during voice calls (#30421)
Classic298 Sep 24, 2026
d9dff09
refac: build the SQLite LIKE regex from wildcard-split segments (#30393)
Classic298 Sep 24, 2026
aea7d34
refac
tjbck Sep 24, 2026
e67d7f9
fix: stop Interface settings save from overwriting the default model …
silentoplayz Sep 24, 2026
5f8d8f0
fix: resume the send queue after a queued message is deleted or edite…
silentoplayz Sep 24, 2026
a046e07
fix: save a file edit made through write access to a shared knowledge…
silentoplayz Sep 24, 2026
e6e9fc9
fix: keep the composer's + and Integrations buttons visible at larger…
Classic298 Sep 24, 2026
ff89756
refac
tjbck Sep 24, 2026
bbfa876
fix: stop the token and API key copy buttons from saving the account …
silentoplayz Sep 24, 2026
f412538
refac
tjbck Sep 24, 2026
6c6d35c
chore: bump hiredis (#31328)
Classic298 Sep 24, 2026
9836367
fix: apply the group filter to the Analytics hourly chart (#30978)
silentoplayz Sep 24, 2026
893251e
fix: show the new share link's own access after the old link is delet…
silentoplayz Sep 24, 2026
7ad0ae4
refac
tjbck Sep 24, 2026
fccd755
fix: send the saved title in the chat:title event when title generati…
Classic298 Sep 24, 2026
0864f8b
fix: save the removal of a direct connection (#31384)
silentoplayz Sep 25, 2026
da36d14
fix: reject a folder name made only of spaces instead of saving it bl…
silentoplayz Sep 25, 2026
3f5881c
fix: load terminal AGENTS.md on Windows Open Terminal hosts (#31342)
Classic298 Sep 25, 2026
3c47f0d
fix: stop decoding Korean and Japanese text uploads as Chinese (#31356)
Classic298 Sep 25, 2026
06bc8cb
fix: honor Ctrl+Enter to Send in the channel message input (#31319)
silentoplayz Sep 25, 2026
fcb0af3
fix: skip blocked OAuth groups when auto-creating groups (#31316)
Classic298 Sep 25, 2026
db697c7
fix: skip the blank last line of a user CSV import (#31374)
silentoplayz Sep 25, 2026
2c922bb
fix: let a chat's Stream Chat Response setting override the account-w…
silentoplayz Sep 25, 2026
9630717
fix: show the stored authentication settings after a value is refused…
silentoplayz Sep 25, 2026
0663009
fix: include pasted images when exporting a note as PDF (#30975)
silentoplayz Sep 25, 2026
ac00d40
fix: model sync no longer fails with "database is locked" on SQLite (…
Classic298 Sep 25, 2026
9d6b17f
fix: errors on Responses API connections are not shown or not kept af…
Classic298 Sep 26, 2026
8081ac2
fix: missing space in reasoning model answers right after the thinkin…
Classic298 Sep 26, 2026
583f5a6
fix: max_tokens sent through the API is ignored for Ollama models (#3…
Classic298 Sep 26, 2026
25604d7
fix: chats keep failing on Anthropic and Bedrock after a tool call is…
Classic298 Sep 26, 2026
fe8b304
fix: stray <|end_of_solution|> marker left in the reply (#31436)
Classic298 Sep 26, 2026
c402acb
fix: skills picked from the / menu are not applied to the reply (#31429)
Classic298 Sep 26, 2026
42cd4f0
refactor: check shared chat access before loading the snapshot on clo…
Classic298 Sep 26, 2026
ffa6bb1
fix: refresh the skill list used in chat after turning a skill on or …
silentoplayz Sep 26, 2026
693f666
fix: start a reply saved as a copy without the original's rating (#30…
silentoplayz Sep 26, 2026
301aa8c
fix: clear the Add Connection form's advanced fields after a connecti…
silentoplayz Sep 26, 2026
f8e15f9
fix: check the external loader headers whenever they are set, so Save…
silentoplayz Sep 26, 2026
6fdbe3a
fix: move a calendar event's end with its date instead of keeping the…
silentoplayz Sep 26, 2026
ffe21be
fix: remove the share links of chats deleted along with their folder …
silentoplayz Sep 26, 2026
b233d0d
fix: paste the clipboard as is for {{CLIPBOARD}}, keeping dollar sign…
silentoplayz Sep 26, 2026
420b4a2
fix(retrieval): name the link when process/web cannot fetch it (#31351)
Classic298 Sep 26, 2026
0d00834
fix: keep admin access to private arena models with no grants
macodev00 Sep 24, 2026
b0650d0
fix: failed timer leaves a blank, unfinished reply in the chat (#31483)
Classic298 Sep 27, 2026
2ab5023
i18n: complete and correct German (de-DE) translations (#31462)
Classic298 Sep 27, 2026
8a90f0f
fix: file uploads and hybrid search fail on Qdrant with strict mode e…
Classic298 Sep 27, 2026
eda8d85
fix: hybrid search finds nothing when a collection cannot be read (#3…
Classic298 Sep 27, 2026
31a09a1
refac: check the owner's role before continuing a chat after a subage…
Classic298 Sep 27, 2026
b39abad
refac
tjbck Sep 27, 2026
59ea3b7
fix: backslashes in uploaded HTML files turn into line breaks or brea…
Classic298 Sep 27, 2026
6c2cdd0
i18n(tr-TR): fill in 62 missing Turkish strings (#31449)
qwist1233-cpu Sep 27, 2026
d6b19dc
fix: on PostgreSQL, searching automations or filtering models by a no…
Classic298 Sep 27, 2026
e26b2ed
fix: Share dialog says anyone with the URL can view a new link, but i…
Classic298 Sep 27, 2026
5d4f9b9
fix: foreground sub-agents cannot use personal tool servers like Open…
Classic298 Sep 27, 2026
9fc038e
i18n: improve it-IT (#31401)
andreadegiovine Sep 27, 2026
91fb33e
fix(retrieval): name the link when process/url cannot fetch it (#31354)
Classic298 Sep 27, 2026
08972e5
chore: bump pillow and aiohttp (#31339)
Classic298 Sep 27, 2026
b91a558
fix: stop forwarding empty tools arrays from the Anthropic Messages e…
Classic298 Sep 27, 2026
35dda25
fix: approve every tool call from a multi-call turn in ask mode (#31315)
Classic298 Sep 27, 2026
b8de508
fix: keep arena model access after editing it in Settings > Models (#…
Classic298 Sep 27, 2026
6e5e5fe
feat: add a Tavily search depth setting (#31308)
Classic298 Sep 27, 2026
8fc416e
refac
tjbck Sep 27, 2026
efe63bd
fix: clamp the artifacts pane's version when an artifact is removed (…
silentoplayz Sep 27, 2026
3322376
refac
tjbck Sep 27, 2026
0fe9ed0
fix: Anthropic API streams report a failed response as a finished one…
Classic298 Sep 27, 2026
1c81390
fix: keep relevance scores on knowledge tool citations (#31307)
Classic298 Sep 27, 2026
19957bc
refac
tjbck Sep 27, 2026
bc2416c
refac
tjbck Sep 27, 2026
0082c15
fix: first click in a chat's Share or Delete dialog does nothing (#31…
Classic298 Sep 27, 2026
00a245b
fix: Manage Ollama ignores a connection's custom headers and auth typ…
Classic298 Sep 27, 2026
af6b82a
refac
tjbck Sep 27, 2026
fdae17f
refac
tjbck Sep 27, 2026
e8d6a87
fix: model upload, download and unload ignore a connection's custom h…
Classic298 Sep 27, 2026
fc9ad75
fix: admins can still read and change other users' chats with ENABLE_…
Classic298 Sep 27, 2026
b6f9679
refac: only open web, mail, phone and relative file links (#31491)
Classic298 Sep 27, 2026
ef67cc3
fix: first click in a dialog opened from a menu does nothing (#31496)
Classic298 Sep 27, 2026
82b5181
refac
tjbck Sep 28, 2026
c8a3e10
fix: let Enter through when the message box's suggestion list has not…
silentoplayz Sep 28, 2026
8667dd7
fix: actions in a model's menu in the model selector do nothing (#31503)
Classic298 Sep 28, 2026
b00745c
fix: approved tool results are lost and approved tools can run twice …
Classic298 Sep 28, 2026
d4b9d19
fix: system prompt and compacted context are lost after approving a t…
Classic298 Sep 28, 2026
176d31d
fix: turn Code Interpreter off when opening another chat from the sid…
silentoplayz Sep 28, 2026
e655e8e
Merge branch 'dev' into cursor/fix-private-arena-admin-access-11ee
macodev00 Sep 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
248 changes: 248 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions backend/open_webui/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import base64
import os
import random
import secrets
import sys
from pathlib import Path
from typing import Annotated
Expand Down Expand Up @@ -45,7 +45,7 @@ def serve(
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(secrets.token_bytes(key_length)))
typer.echo(f'Loading WEBUI_SECRET_KEY from {KEY_FILE}')
os.environ['WEBUI_SECRET_KEY'] = KEY_FILE.read_text()

Expand Down
39 changes: 6 additions & 33 deletions backend/open_webui/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1302,6 +1302,8 @@ def reachable(host: str, port: int) -> bool:

TAVILY_EXTRACT_DEPTH = os.getenv('TAVILY_EXTRACT_DEPTH', 'basic')

TAVILY_SEARCH_DEPTH = os.getenv('TAVILY_SEARCH_DEPTH', 'basic')

STAAN_API_KEY = os.getenv('STAAN_API_KEY', '')

STAAN_MARKET = os.getenv('STAAN_MARKET', 'en-us')
Expand Down Expand Up @@ -1675,43 +1677,13 @@ def reachable(host: str, port: int) -> bool:

DEFAULT_PINNED_MODELS = os.getenv('DEFAULT_PINNED_MODELS', None)

# None uses the frontend's localized defaults; an empty list disables suggestions.
try:
default_prompt_suggestions = JSONCodec.loads(os.getenv('DEFAULT_PROMPT_SUGGESTIONS', '[]'))
DEFAULT_PROMPT_SUGGESTIONS = JSONCodec.loads(os.getenv('DEFAULT_PROMPT_SUGGESTIONS', 'null'))
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 = None

DEFAULT_PROMPT_SUGGESTIONS = default_prompt_suggestions
DEFAULT_PROMPT_SUGGESTIONS_I18N = {}

try:
Expand Down Expand Up @@ -3028,6 +3000,7 @@ def feishu_oauth_register(oauth: OAuth):
'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.search.tavily_search_depth': TAVILY_SEARCH_DEPTH,
'web.search.staan_api_key': STAAN_API_KEY,
'web.search.staan_market': STAAN_MARKET,
'web.search.staan_max_snippets': STAAN_MAX_SNIPPETS,
Expand Down
18 changes: 17 additions & 1 deletion backend/open_webui/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,14 @@ def parse_section(section):
except ValueError:
REDIS_RESPONSE_STREAM_TTL = 3600

# Seconds a task survives without a heartbeat. 0 disables expiry.
try:
REDIS_TASK_TTL = int(os.getenv('REDIS_TASK_TTL', '300'))
if REDIS_TASK_TTL != 0 and REDIS_TASK_TTL < 60:
REDIS_TASK_TTL = 300
except ValueError:
REDIS_TASK_TTL = 300

REDIS_SENTINEL_HOSTS = os.getenv('REDIS_SENTINEL_HOSTS', '')
REDIS_SENTINEL_PORT = os.getenv('REDIS_SENTINEL_PORT', '26379')

Expand Down Expand Up @@ -485,6 +493,12 @@ def parse_section(section):
WEBSOCKET_REDIS_URL = os.getenv('WEBSOCKET_REDIS_URL', REDIS_URL)
WEBSOCKET_REDIS_CLUSTER = os.getenv('WEBSOCKET_REDIS_CLUSTER', str(REDIS_CLUSTER)).lower() == 'true'

# publishes room-targeted emits on per-room redis channels so instances skip
# messages for rooms without local members; must be identical across the fleet
# (toggle with a full restart, not a rolling one), set false for the previous
# shared-channel-only delivery
WEBSOCKET_REDIS_ROOM_CHANNELS = os.getenv('WEBSOCKET_REDIS_ROOM_CHANNELS', 'True').lower() == 'true'

websocket_redis_lock_timeout = os.getenv('WEBSOCKET_REDIS_LOCK_TIMEOUT', '60')

try:
Expand Down Expand Up @@ -1048,7 +1062,9 @@ def _parse_ssl_env(value: str) -> 'bool | _ssl.SSLContext':
# Opt in to CPython's in-place string append optimization for streamed responses.
# Off by default for a staged rollout. Only a host already out of memory can lose
# text here; the default path (a full copy per chunk) raises there too.
ENABLE_CHAT_RESPONSE_STREAM_INPLACE_APPEND = os.getenv('ENABLE_CHAT_RESPONSE_STREAM_INPLACE_APPEND', 'False').lower() == 'true'
ENABLE_CHAT_RESPONSE_STREAM_INPLACE_APPEND = (
os.getenv('ENABLE_CHAT_RESPONSE_STREAM_INPLACE_APPEND', 'False').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
Expand Down
15 changes: 10 additions & 5 deletions backend/open_webui/internal/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,23 +348,28 @@ def like(pattern, value, escape=None):
if compiled is False:
return False
if compiled is None:
regex = []
segments = ['']
escaped = False
for char in pattern:
if escape and not escaped and char == escape:
escaped = True
continue
regex.append(
'.*' if not escaped and char == '%' else '.' if not escaped and char == '_' else re.escape(char)
)
if not escaped and char == '%':
segments.append('')
else:
segments[-1] += '.' if not escaped and char == '_' else re.escape(char)
escaped = False
if escaped:
compiled = False
if len(compiled_patterns) >= 512:
compiled_patterns.clear()
compiled_patterns[key] = compiled
return False
compiled = re.compile(''.join(regex), re.DOTALL)
# Atomic groups pin each middle segment to its first match, so '%' never backtracks.
regex = segments[0] + ''.join(f'(?>.*?{segment})' for segment in segments[1:-1])
if len(segments) > 1:
regex += '.*' + segments[-1]
compiled = re.compile(regex, re.DOTALL)
if len(compiled_patterns) >= 512:
compiled_patterns.clear()
compiled_patterns[key] = compiled
Expand Down
58 changes: 39 additions & 19 deletions backend/open_webui/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
seed_registered_defaults,
)
from open_webui.constants import ERROR_MESSAGES, TASKS
from open_webui.utils.recurrence import RecurrenceEvaluationTimeout
from open_webui.env import (
USE_SLIM,
AIOHTTP_CLIENT_SESSION_SSL,
Expand Down Expand Up @@ -107,12 +108,14 @@
MAX_BODY_LOG_SIZE,
# Redis
REDIS_KEY_PREFIX,
REDIS_TASK_TTL,
REDIS_URL,
RESET_CONFIG_ON_START,
SAFE_MODE,
SCIM_TOKEN,
VERSION,
WEBSOCKET_HEARTBEAT_INTERVAL,
WEBSOCKET_MANAGER,
# Admin Account Runtime Creation
WEBUI_ADMIN_EMAIL,
WEBUI_ADMIN_NAME,
Expand Down Expand Up @@ -189,6 +192,7 @@
get_user_id_from_session_pool,
periodic_session_pool_cleanup,
periodic_usage_pool_cleanup,
redis_event_listener,
)
from open_webui.socket.main import (
app as socket_app,
Expand All @@ -200,6 +204,7 @@
list_task_ids_by_item_id,
list_tasks,
redis_task_command_listener,
redis_task_heartbeat,
stop_item_tasks,
stop_task,
) # Import from tasks.py
Expand Down Expand Up @@ -232,6 +237,7 @@
normalize_chat_variables,
)
from open_webui.utils.embeddings import generate_embeddings
from open_webui.utils.headers import get_headers_and_cookies
from open_webui.utils.json_codec import JSONCodec
from open_webui.utils.json_response import apply_orjson_http_json
from open_webui.utils.logger import start_logger
Expand Down Expand Up @@ -259,7 +265,7 @@
encrypt_data,
get_oauth_client_info_with_dynamic_client_registration,
get_oauth_client_info_with_static_credentials,
recover_static_oauth_client_metadata,
recover_oauth_client_metadata,
resolve_oauth_client_info,
)
from open_webui.utils.plugin import install_tool_and_function_dependencies
Expand Down Expand Up @@ -386,6 +392,11 @@ async def lifespan(app: FastAPI):

if app.state.redis is not None:
app.state.redis_task_command_listener = asyncio.create_task(redis_task_command_listener(app))
if REDIS_TASK_TTL > 0:
app.state.redis_task_heartbeat = asyncio.create_task(redis_task_heartbeat(app))

if WEBSOCKET_MANAGER == 'redis':
app.state.redis_event_listener = asyncio.create_task(redis_event_listener())

app.state.periodic_usage_pool_cleanup = asyncio.create_task(periodic_usage_pool_cleanup())
app.state.periodic_session_pool_cleanup = asyncio.create_task(periodic_session_pool_cleanup())
Expand Down Expand Up @@ -473,6 +484,12 @@ async def lifespan(app: FastAPI):
if hasattr(app.state, 'redis_task_command_listener'):
app.state.redis_task_command_listener.cancel()

if hasattr(app.state, 'redis_task_heartbeat'):
app.state.redis_task_heartbeat.cancel()

if hasattr(app.state, 'redis_event_listener'):
app.state.redis_event_listener.cancel()

app.state.periodic_usage_pool_cleanup.cancel()
app.state.periodic_session_pool_cleanup.cancel()
app.state.scheduler_worker_loop.cancel()
Expand All @@ -495,6 +512,12 @@ async def lifespan(app: FastAPI):
lifespan=lifespan,
)


@app.exception_handler(RecurrenceEvaluationTimeout)
async def recurrence_timeout_handler(request: Request, exc: RecurrenceEvaluationTimeout):
return JSONResponse(status_code=400, content={'detail': str(exc)})


# Used by readiness checks to gate traffic until startup work is done.
app.state.startup_complete = False

Expand Down Expand Up @@ -614,9 +637,7 @@ async def initialize_runtime_config(app: FastAPI):
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 = await recover_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}',
Expand Down Expand Up @@ -974,14 +995,12 @@ async def unload_model(request: Request, form_data: ModelUnloadForm, user=Depend
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 {}),
}
headers, cookies = await get_headers_and_cookies(request, url, key, api_config, user=user)
async with session.post(
f'{url}/api/generate',
data=payload,
headers=headers,
cookies=cookies,
) as r:
if not r.ok:
errors.append({'url_idx': idx, 'error': await r.text()})
Expand Down Expand Up @@ -1015,14 +1034,12 @@ async def unload_model(request: Request, form_data: ModelUnloadForm, user=Depend
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 {}),
}
headers, cookies = await get_headers_and_cookies(request, base_url, key, api_config, user=user)
async with session.post(
f'{root_url}/models/unload',
json={'model': actual_model},
headers=headers,
cookies=cookies,
) as r:
if not r.ok:
detail = await r.text()
Expand Down Expand Up @@ -1470,7 +1487,9 @@ async def run_initial_title_generation():
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':
if not await Chats.is_chat_owner(chat_id, user.id) and not (
user.role == 'admin' and ENABLE_ADMIN_CHAT_ACCESS
):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.DEFAULT(),
Expand Down Expand Up @@ -1625,7 +1644,8 @@ async def run_initial_title_generation():
async def process_chat(request, form_data, user, metadata, model, tasks=None):
try:
ctx = None
if metadata.get('assistant_message_id'):
# Saved chats load the message after approved tool calls run, so their results are kept
if metadata.get('assistant_message_id') and not is_saved_chat_id(metadata.get('chat_id')):
ctx = await build_chat_response_context(request, form_data, user, model, metadata, tasks, [])
form_data, metadata, events = await process_chat_payload(request, form_data, user, metadata, model)

Expand Down Expand Up @@ -2058,7 +2078,7 @@ async def verify_chat_ownership(chat_id: str | None, user) -> None:
detail='Channel chats are not supported on this endpoint',
)

if user.role != 'admin' and not await Chats.is_chat_owner(chat_id, user.id):
if not (user.role == 'admin' and ENABLE_ADMIN_CHAT_ACCESS) and not await Chats.is_chat_owner(chat_id, user.id):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.DEFAULT(),
Expand Down Expand Up @@ -2122,11 +2142,11 @@ async def list_tasks_by_chat_id_endpoint(request: Request, chat_id: str, user=De
socket_id = get_temporary_chat_session_id(chat_id)
if socket_id:
owner_id = get_user_id_from_session_pool(socket_id)
if owner_id != user.id and user.role != 'admin':
if owner_id != user.id and not (user.role == 'admin' and ENABLE_ADMIN_CHAT_ACCESS):
return {'task_ids': []}
else:
chat = await Chats.get_chat_by_id(chat_id)
if chat is None or (chat.user_id != user.id and user.role != 'admin'):
if chat is None or (chat.user_id != user.id and not (user.role == 'admin' and ENABLE_ADMIN_CHAT_ACCESS)):
return {'task_ids': []}

task_ids = await list_task_ids_by_item_id(request.app.state.redis, chat_id)
Expand All @@ -2141,11 +2161,11 @@ async def stop_tasks_by_chat_id_endpoint(request: Request, chat_id: str, user=De
chat = None
if socket_id:
owner_id = get_user_id_from_session_pool(socket_id)
if owner_id != user.id and user.role != 'admin':
if owner_id != user.id and not (user.role == 'admin' and ENABLE_ADMIN_CHAT_ACCESS):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND)
else:
chat = await Chats.get_chat_by_id(chat_id)
if chat is None or (chat.user_id != user.id and user.role != 'admin'):
if chat is None or (chat.user_id != user.id and not (user.role == 'admin' and ENABLE_ADMIN_CHAT_ACCESS)):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND)
result = await stop_item_tasks(request.app.state.redis, chat_id)

Expand Down
Loading
Loading