From 8516c80762f017be936e60a2223c680c9c2e3598 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Tue, 2 Jun 2026 10:25:20 +0200 Subject: [PATCH] fix: expose model/selected skills as a lazy-loaded view_skill manifest --- backend/open_webui/utils/middleware.py | 39 +++++++++++++-------- src/lib/components/chat/Chat.svelte | 48 ++++++-------------------- 2 files changed, 34 insertions(+), 53 deletions(-) diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 63de31fbba..daeb8c8b1b 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -2619,19 +2619,28 @@ 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 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', [])) + # Skills are surfaced two ways: + # - $-mention tags (<$skillId|label>) in the message pull a skill's full content + # into the chat immediately (extracted here so persisted chats work too). + # - skill_ids (the integrations menu, where the frontend also pre-selects the + # model's attached skills) plus the model's own attached skills are exposed as a + # lightweight manifest, loaded on demand via view_skill. + mention_skill_ids = extract_skill_ids_from_messages(form_data.get('messages', [])) + menu_skill_ids = set(form_data.pop('skill_ids', None) or []) model_skill_ids = set(model.get('info', {}).get('meta', {}).get('skillIds', [])) + all_skill_ids = mention_skill_ids | menu_skill_ids | model_skill_ids + + # The view_skill tool is only registered for native function calling with builtin + # tools enabled (see get_builtin_tools); when it can't be, manifest skills are + # injected as full content too so they stay usable. + builtin_tools_enabled = (model.get('info', {}).get('meta', {}).get('capabilities') or {}).get('builtin_tools', True) + skills_lazy_load = metadata.get('params', {}).get('function_calling') == 'native' and builtin_tools_enabled - all_skill_ids = user_skill_ids | model_skill_ids available_skills = [] if all_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: if sid in accessible_skill_ids: s = await SkillsModel.get_skill_by_id(sid) @@ -2640,15 +2649,15 @@ async def process_chat_payload(request, form_data, user, metadata, model): skill_descriptions = '' for skill in available_skills: - if skill.id in user_skill_ids: - # User-selected: inject full content + # $-mentioned skills (and, when view_skill can't be registered, every skill) + # get full content injected; the rest are lightweight manifest entries. + if skill.id in mention_skill_ids or not skills_lazy_load: 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' if skill_descriptions: @@ -2838,11 +2847,8 @@ async def process_chat_payload(request, form_data, user, metadata, model): if mcp_clients: metadata['mcp_clients'] = mcp_clients - # Inject builtin tools for native function calling based on enabled features and model capability - # Check if builtin_tools capability is enabled for this model (defaults to True if not specified) - builtin_tools_enabled = (model.get('info', {}).get('meta', {}).get('capabilities') or {}).get( - 'builtin_tools', True - ) + # Inject builtin tools for native function calling based on enabled features and model capability. + # builtin_tools_enabled (model "builtin_tools" capability) was resolved with the skills above. if metadata.get('params', {}).get('function_calling') == 'native' and builtin_tools_enabled: # Add file context to user messages chat_id = metadata.get('chat_id') @@ -2852,7 +2858,10 @@ 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], + # This branch only runs when skills_lazy_load is True; $-mentioned + # skills are injected as full content, the rest are manifest entries + # the model loads via view_skill. + '__skill_ids__': [s.id for s in available_skills if s.id not in mention_skill_ids], }, features, model, diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 69652dbe41..a954085277 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -374,7 +374,11 @@ selectedToolIds = selectedToolIds.filter((id) => !id.startsWith('direct_server:')); } - // Set Default Skills + // Pre-select model-attached skills so they appear enabled in the integrations + // menu. These — and any skill the user enables there or via $-mention — are sent + // as skill_ids and exposed to the model as an manifest; the + // model loads full content on demand via the builtin view_skill tool rather than + // having it injected up front. if (model?.info?.meta?.skillIds) { selectedSkillIds = [ ...new Set( @@ -2386,44 +2390,12 @@ } } - // Parse skill mentions (<$skillId|label>) from user messages - const skillMentionRegex = /<\$([^|>]+)\|?[^>]*>/g; + // Skills enabled via the integrations menu (the model's attached skills are + // pre-selected here) are sent as skill_ids and exposed to the model as a manifest, + // loaded on demand via view_skill. $-mention skills are left inline in the message: + // the backend extracts them, injects their full content into the chat, and strips + // the markup — so a mention is pulled into the chat rather than lazy-loaded. const skillIds = [...selectedSkillIds]; - const mentionSkillIds = []; - for (const message of messages) { - const content = - typeof message.content === 'string' ? message.content : (message.content?.[0]?.text ?? ''); - for (const match of content.matchAll(skillMentionRegex)) { - if (!mentionSkillIds.includes(match[1])) { - mentionSkillIds.push(match[1]); - } - if (!skillIds.includes(match[1])) { - skillIds.push(match[1]); - } - } - } - - // Strip skill mentions from message content - if (mentionSkillIds.length > 0) { - messages = messages.map((message) => { - if (typeof message.content === 'string') { - return { - ...message, - content: message.content.replace(/<\$[^>]+>/g, '').trim() - }; - } else if (Array.isArray(message.content)) { - return { - ...message, - content: message.content.map((part) => - part.type === 'text' - ? { ...part, text: part.text.replace(/<\$[^>]+>/g, '').trim() } - : part - ) - }; - } - return message; - }); - } // Use the user-selected terminal from the dropdown const activeTerminalId = $selectedTerminalId ?? null;