diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py
index 832f1b1ce7..8a67f84539 100644
--- a/backend/open_webui/config.py
+++ b/backend/open_webui/config.py
@@ -2058,6 +2058,8 @@ ENABLE_FOLDERS = os.getenv('ENABLE_FOLDERS', 'True').lower() == 'true'
FOLDER_MAX_FILE_COUNT = os.getenv('FOLDER_MAX_FILE_COUNT', '')
+ENABLE_SKILL_LAZY_LOADING = os.getenv('ENABLE_SKILL_LAZY_LOADING', 'False').lower() == 'true'
+
SKILL_CONTENT_MAX_CHARS = int(os.environ['SKILL_CONTENT_MAX_CHARS']) if os.getenv('SKILL_CONTENT_MAX_CHARS') else None
if SKILL_CONTENT_MAX_CHARS is not None and SKILL_CONTENT_MAX_CHARS <= 0:
raise ValueError('SKILL_CONTENT_MAX_CHARS must be a positive integer or unset')
@@ -2900,6 +2902,7 @@ ENABLE_LDAP_GROUP_CREATION = os.getenv('ENABLE_LDAP_GROUP_CREATION', 'False').lo
LDAP_ATTRIBUTE_FOR_GROUPS = os.getenv('LDAP_ATTRIBUTE_FOR_GROUPS', 'memberOf')
DEFAULT_CONFIG = {
+ 'skills.lazy_loading.enable': ENABLE_SKILL_LAZY_LOADING,
'skills.content_max_chars': SKILL_CONTENT_MAX_CHARS,
'skills.preview_max_chars': SKILL_PREVIEW_MAX_CHARS,
'direct.enable': ENABLE_DIRECT_CONNECTIONS,
diff --git a/backend/open_webui/routers/auths.py b/backend/open_webui/routers/auths.py
index 701acfb360..5c9ea63455 100644
--- a/backend/open_webui/routers/auths.py
+++ b/backend/open_webui/routers/auths.py
@@ -134,6 +134,7 @@ ADMIN_CONFIG_KEYS = {
'ENABLE_CALENDAR': 'calendar.enable',
'ENABLE_MEMORIES': 'memories.enable',
'ENABLE_MEMORY_SYSTEM_CONTEXT': 'memories.system_context.enable',
+ 'ENABLE_SKILL_LAZY_LOADING': 'skills.lazy_loading.enable',
'SKILL_CONTENT_MAX_CHARS': 'skills.content_max_chars',
'SKILL_PREVIEW_MAX_CHARS': 'skills.preview_max_chars',
'ENABLE_NOTES': 'notes.enable',
@@ -1187,6 +1188,7 @@ class AdminConfig(MfaConfigForm):
ENABLE_CALENDAR: bool
ENABLE_MEMORIES: bool
ENABLE_MEMORY_SYSTEM_CONTEXT: bool
+ ENABLE_SKILL_LAZY_LOADING: bool = False
SKILL_CONTENT_MAX_CHARS: int | None = Field(default=None, gt=0, strict=True)
SKILL_PREVIEW_MAX_CHARS: int | None = Field(default=None, gt=0, strict=True)
ENABLE_NOTES: bool
@@ -1237,6 +1239,8 @@ async def update_admin_config(request: Request, form_data: AdminConfig, user=Dep
updates.pop('apps.enable', None)
if 'I18N' not in form_data.model_fields_set:
updates.pop('ui.i18n', None)
+ if 'ENABLE_SKILL_LAZY_LOADING' not in form_data.model_fields_set:
+ updates.pop('skills.lazy_loading.enable', None)
if 'SKILL_CONTENT_MAX_CHARS' not in form_data.model_fields_set:
updates.pop('skills.content_max_chars', None)
if 'SKILL_PREVIEW_MAX_CHARS' not in form_data.model_fields_set:
diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py
index 9d88eb9a92..7f8436e1cf 100644
--- a/backend/open_webui/utils/middleware.py
+++ b/backend/open_webui/utils/middleware.py
@@ -2453,7 +2453,7 @@ async def process_chat_payload(request, form_data, user, metadata, model):
# Otherwise, save any tools that filter inlets added for merging later.
inlet_filter_tools = None if payload_tools is not None else form_data.get('tools', None)
- # Selected and mentioned skills get bounded content; other skills remain available through view_skill.
+ # Mentions always load instructions; selected skills can optionally load through view_skill.
chat_context = metadata.get('chat_context') or {}
metadata['chat_context'] = chat_context
skill_versions = chat_context.setdefault('skill_versions', {})
@@ -2462,6 +2462,8 @@ async def process_chat_payload(request, form_data, user, metadata, model):
)
activated_skill_ids = set(form_data.pop('skill_ids', None) or []) | mentioned_skill_ids
skill_ids = sorted(activated_skill_ids | set(model.get('info', {}).get('meta', {}).get('skillIds', [])))
+ if payload_tools is None and await Config.get('skills.lazy_loading.enable', False):
+ activated_skill_ids = mentioned_skill_ids
available_skills = []
terminal_skills = []
if is_note_chat:
diff --git a/src/lib/components/admin/Settings/General.svelte b/src/lib/components/admin/Settings/General.svelte
index db9c9ea478..6207fd5b4b 100644
--- a/src/lib/components/admin/Settings/General.svelte
+++ b/src/lib/components/admin/Settings/General.svelte
@@ -339,6 +339,13 @@
/>
{/if}
+
+
+
mentions both load instructions.
+ // The backend applies the skill loading setting to menu-selected skills.
// Keep mentions in the message so the backend can resolve them.
const skillIds = [...selectedSkillIds];
diff --git a/src/lib/i18n/locales/en-US/translation.json b/src/lib/i18n/locales/en-US/translation.json
index 2c4d70a9ac..7fb24dd957 100644
--- a/src/lib/i18n/locales/en-US/translation.json
+++ b/src/lib/i18n/locales/en-US/translation.json
@@ -2828,6 +2828,8 @@
"settings.admin.general.seeWhatSNew.label": "See what's new",
"settings.admin.general.skillContentMaxChars.description": "Maximum characters per skill read. Longer instructions open as a preview. Leave empty to use the default.",
"settings.admin.general.skillContentMaxChars.label": "Skill Read Limit",
+ "settings.admin.general.skillLazyLoading.description": "Load selected and model-attached skills when needed instead of including instructions upfront. Explicit skill mentions still load immediately. Requires built-in skill tools; otherwise instructions load upfront.",
+ "settings.admin.general.skillLazyLoading.label": "Load selected skills on demand",
"settings.admin.general.skillPreviewMaxChars.description": "Characters shown when instructions exceed the read limit. Cannot exceed that limit. Leave empty to use the default.",
"settings.admin.general.skillPreviewMaxChars.label": "Skill Preview Length",
"settings.admin.general.title": "General",