diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py
index 55a70b1291..f3e2a7a704 100644
--- a/backend/open_webui/utils/middleware.py
+++ b/backend/open_webui/utils/middleware.py
@@ -2693,41 +2693,49 @@ 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)
- # Skills — extract IDs from message content (<$skillId|label> tags) so
- # persisted chats work without relying on the frontend to send skill_ids.
- user_skill_ids = set(form_data.pop('skill_ids', None) or [])
- user_skill_ids |= extract_skill_ids_from_messages(form_data.get('messages', []))
- model_skill_ids = set(model.get('info', {}).get('meta', {}).get('skillIds', []))
-
- all_skill_ids = user_skill_ids | model_skill_ids
+ # Mentioned skills get full content; selected/default skills can be loaded through view_skill.
+ mentioned_skill_ids = extract_skill_ids_from_messages(form_data.get('messages', []))
+ skill_ids = (
+ set(form_data.pop('skill_ids', None) or [])
+ | set(model.get('info', {}).get('meta', {}).get('skillIds', []))
+ | mentioned_skill_ids
+ )
available_skills = []
- if all_skill_ids:
+ view_skill_ids = []
+ use_builtin_tools = (
+ bool(metadata.get('session_id'))
+ and metadata.get('params', {}).get('function_calling') != 'legacy'
+ and (model.get('info', {}).get('meta', {}).get('capabilities') or {}).get('builtin_tools', True)
+ )
+
+ if skill_ids:
from open_webui.models.skills import Skills as SkillsModel
accessible_skill_ids = {s.id for s in await SkillsModel.get_skills_by_user_id(user.id, 'read')}
- available_skills = []
- for sid in all_skill_ids:
+ for sid in skill_ids:
if sid in accessible_skill_ids:
s = await SkillsModel.get_skill_by_id(sid)
if s and s.is_active:
available_skills.append(s)
- skill_descriptions = ''
+ skill_manifest = ''
for skill in available_skills:
- if skill.id in user_skill_ids:
- # User-selected: inject full content
+ if skill.id in mentioned_skill_ids or not use_builtin_tools:
form_data['messages'] = add_or_update_system_message(
f'\n{skill.content}\n',
form_data['messages'],
append=True,
)
else:
- # Model-attached: name+description only
- skill_descriptions += f'\n{skill.id}\n{skill.name}\n{skill.description or ""}\n\n'
+ view_skill_ids.append(skill.id)
+ skill_manifest += (
+ f'\n{skill.id}\n{skill.name}\n'
+ f'{skill.description or ""}\n\n'
+ )
- if skill_descriptions:
+ if skill_manifest:
form_data['messages'] = add_or_update_system_message(
- f'\n{skill_descriptions}',
+ f'\n{skill_manifest}',
form_data['messages'],
append=True,
)
@@ -2916,11 +2924,7 @@ async def process_chat_payload(request, form_data, user, metadata, model):
# Inject builtin tools for native function calling based on enabled features and model capability.
# Only inject when the request originates from the UI (identified by session_id).
# API callers don't expect hidden tools; they can explicitly request tools via tool_ids.
- builtin_tools_enabled = (model.get('info', {}).get('meta', {}).get('capabilities') or {}).get(
- 'builtin_tools', True
- )
- has_session = bool(metadata.get('session_id'))
- if has_session and metadata.get('params', {}).get('function_calling') != 'legacy' and builtin_tools_enabled:
+ if use_builtin_tools:
# Add file context to user messages
chat_id = metadata.get('chat_id')
form_data['messages'] = await add_file_context(form_data.get('messages', []), chat_id, user)
@@ -2929,7 +2933,7 @@ async def process_chat_payload(request, form_data, user, metadata, model):
{
**extra_params,
'__event_emitter__': event_emitter,
- '__skill_ids__': [s.id for s in available_skills if s.id not in user_skill_ids],
+ '__skill_ids__': view_skill_ids,
},
features,
model,