From ff3da6a5ee1d6f34215a345dfdc335cd2028afc4 Mon Sep 17 00:00:00 2001 From: Mord0reK <135718526+Mord0reK@users.noreply.github.com> Date: Mon, 27 Jul 2026 17:29:22 +0200 Subject: [PATCH] feat: add configurable subagent model --- backend/open_webui/config.py | 2 + backend/open_webui/routers/configs.py | 2 + backend/open_webui/utils/subagents.py | 43 ++++++++++--- .../admin/Settings/Subagents.svelte | 61 ++++++++++++++++++- 4 files changed, 98 insertions(+), 10 deletions(-) diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 262e7c0818..eacfe43e16 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -2028,6 +2028,7 @@ ENABLE_CALENDAR = os.getenv('ENABLE_CALENDAR', 'True').lower() == 'true' ENABLE_AUTOMATIONS = os.getenv('ENABLE_AUTOMATIONS', 'True').lower() == 'true' ENABLE_SUBAGENTS = os.getenv('ENABLE_SUBAGENTS', 'False').lower() == 'true' +SUBAGENTS_MODEL_ID = os.getenv('SUBAGENTS_MODEL_ID', '') SUBAGENTS_BACKGROUND_ENABLED = os.getenv('SUBAGENTS_BACKGROUND_ENABLED', 'False').lower() == 'true' SUBAGENTS_MAX_CONCURRENT = int(os.getenv('SUBAGENTS_MAX_CONCURRENT', '20')) SUBAGENTS_MAX_ASYNC = int(os.getenv('SUBAGENTS_MAX_ASYNC', '20')) @@ -3092,6 +3093,7 @@ DEFAULT_CONFIG = { 'calendar.enable': ENABLE_CALENDAR, 'automations.enable': ENABLE_AUTOMATIONS, 'subagents.enable': ENABLE_SUBAGENTS, + 'subagents.model_id': SUBAGENTS_MODEL_ID, 'subagents.background_enabled': SUBAGENTS_BACKGROUND_ENABLED, 'subagents.max_concurrent': SUBAGENTS_MAX_CONCURRENT, 'subagents.max_async': SUBAGENTS_MAX_ASYNC, diff --git a/backend/open_webui/routers/configs.py b/backend/open_webui/routers/configs.py index f591908a6d..4028dcc5b0 100644 --- a/backend/open_webui/routers/configs.py +++ b/backend/open_webui/routers/configs.py @@ -69,6 +69,7 @@ MODELS_CONFIG_KEYS = { } SUBAGENTS_CONFIG_KEYS = { 'ENABLE_SUBAGENTS': 'subagents.enable', + 'SUBAGENTS_MODEL_ID': 'subagents.model_id', 'SUBAGENTS_BACKGROUND_ENABLED': 'subagents.background_enabled', 'SUBAGENTS_MAX_CONCURRENT': 'subagents.max_concurrent', 'SUBAGENTS_MAX_ASYNC': 'subagents.max_async', @@ -766,6 +767,7 @@ async def set_models_config(request: Request, form_data: ModelsConfigForm, user= class SubagentsConfigForm(BaseModel): ENABLE_SUBAGENTS: bool + SUBAGENTS_MODEL_ID: str = '' SUBAGENTS_BACKGROUND_ENABLED: bool SUBAGENTS_MAX_CONCURRENT: int SUBAGENTS_MAX_ASYNC: int diff --git a/backend/open_webui/utils/subagents.py b/backend/open_webui/utils/subagents.py index 7092df44c2..3a53100188 100644 --- a/backend/open_webui/utils/subagents.py +++ b/backend/open_webui/utils/subagents.py @@ -44,6 +44,13 @@ _foreground_semaphore: asyncio.Semaphore | None = None _parent_locks: dict[str, asyncio.Lock] = {} +def get_subagent_model_ids( + parent_model_id: str | None, configured_model_id: str | None +) -> tuple[str | None, str | None]: + subagent_model_id = configured_model_id or parent_model_id + return parent_model_id, subagent_model_id + + def _build_request(source: Request, user_id: str, *, internal: bool) -> Request: scope = { 'type': 'http', @@ -123,7 +130,17 @@ async def process_pending_internal_messages( if kind == 'timer' and first_meta.get('timer_id'): timer = await Chats.get_chat_by_id(first_meta['timer_id']) run = {**run, **(((timer.meta or {}).get('run') if timer else None) or {})} - model_id = first.get('model') or run['model_id'] + parent_model_id = first_meta.get('parent_model_id') or run.get('parent_model_id') + subagent_model_id = ( + first.get('model') + or first_meta.get('subagent_model_id') + or run.get('subagent_model_id') + or run.get('model_id') + ) + if kind == 'subagent': + model_id = parent_model_id or subagent_model_id + else: + model_id = subagent_model_id if kind == 'timer': batch = [first] else: @@ -132,7 +149,7 @@ async def process_pending_internal_messages( for message in pending for meta in [message.get('meta') or {}] if message.get('parentId') == parent_id - and (message.get('model') or model_id) == model_id + and (message.get('model') or subagent_model_id) == subagent_model_id and ( meta.get('internal') is True and meta.get('type') == 'subagent' @@ -294,6 +311,7 @@ async def delegate( 'subagents.max_iterations', 'subagents.max_output', 'subagents.system_prompt', + 'subagents.model_id', ) max_concurrent = int(config.get('subagents.max_concurrent') or 20) max_async = int(config.get('subagents.max_async') or 20) @@ -314,8 +332,13 @@ async def delegate( and await Config.get('code_interpreter.engine', 'pyodide') != 'jupyter' ): features.pop('code_interpreter') + parent_model_id, subagent_model_id = get_subagent_model_ids( + metadata.get('model_id') or (metadata.get('model') or {}).get('id'), + config.get('subagents.model_id'), + ) run = { - 'model_id': metadata.get('model_id') or (metadata.get('model') or {}).get('id'), + 'parent_model_id': parent_model_id, + 'subagent_model_id': subagent_model_id, 'session_id': metadata.get('session_id'), 'tool_ids': copy.deepcopy(metadata.get('tool_ids') or []), 'skill_ids': copy.deepcopy(metadata.get('skill_ids') or []), @@ -328,7 +351,7 @@ async def delegate( 'variables': copy.deepcopy(metadata.get('variables') or {}), 'direct': bool(metadata.get('direct')), } - if not run.get('model_id'): + if not run.get('subagent_model_id'): return 'Error: model context is required.' if run.get('direct'): return 'Error: sub-agents are unavailable for direct connections.' @@ -391,7 +414,7 @@ async def delegate( 'role': 'user', 'content': prompt, 'timestamp': int(time.time()), - 'models': [run['model_id']], + 'models': [run['subagent_model_id']], **({'files': prompt_files} if prompt_files else {}), } chat = await Chats.insert_new_chat( @@ -401,7 +424,7 @@ async def delegate( chat={ 'id': chat_id, 'title': f'Sub-agent: {task[:60]}', - 'models': [run['model_id']], + 'models': [run['subagent_model_id']], 'history': { 'currentId': assistant_message_id, 'messages': { @@ -413,7 +436,7 @@ async def delegate( 'role': 'assistant', 'content': '', 'done': False, - 'model': run['model_id'], + 'model': run['subagent_model_id'], 'timestamp': int(time.time()), }, }, @@ -457,7 +480,7 @@ async def delegate( str(config.get('subagents.system_prompt') or '').strip() or DEFAULT_SUBAGENT_SYSTEM_PROMPT ) form_data = { - 'model': run['model_id'], + 'model': run['subagent_model_id'], 'messages': [ { 'role': 'system', @@ -589,6 +612,8 @@ async def delegate( 'type': 'subagent', 'delegation_id': delegation_id, 'subagent_chat_id': chat_id, + 'parent_model_id': run['parent_model_id'], + 'subagent_model_id': run['subagent_model_id'], } pending_message = { 'id': pending_message_id, @@ -596,7 +621,7 @@ async def delegate( 'childrenIds': [], 'role': 'user', 'content': '\n'.join(lines), - 'model': run['model_id'], + 'model': run['subagent_model_id'], 'meta': pending_meta, 'timestamp': int(time.time()), } diff --git a/src/lib/components/admin/Settings/Subagents.svelte b/src/lib/components/admin/Settings/Subagents.svelte index 47b72233f0..1f132edddc 100644 --- a/src/lib/components/admin/Settings/Subagents.svelte +++ b/src/lib/components/admin/Settings/Subagents.svelte @@ -2,7 +2,10 @@ import { getContext, onMount } from 'svelte'; import { toast } from 'svelte-sonner'; + import { getModels } from '$lib/apis'; import { getSubagentsConfig, setSubagentsConfig } from '$lib/apis/configs'; + import { getBaseModels } from '$lib/apis/models'; + import SettingsSelect from '$lib/components/common/SettingsSelect.svelte'; import Spinner from '$lib/components/common/Spinner.svelte'; import Switch from '$lib/components/common/Switch.svelte'; @@ -11,23 +14,50 @@ let loading = true; let saving = false; let enabled = false; + let subagentModelId = ''; let backgroundEnabled = false; let maxConcurrent = 20; let maxAsync = 20; let maxIterations = 30; let maxOutput = 30000; let systemPrompt = ''; + let models: any[] = []; + + const normalizeModelSelection = (modelId: string | null | undefined) => { + if (!modelId) { + return ''; + } + + const model = models.find((item: any) => item.id === modelId); + if (!model) { + return modelId; + } + + return model.id; + }; onMount(async () => { try { - const config = await getSubagentsConfig(localStorage.token); + const [config, workspaceModels, baseModels] = await Promise.all([ + getSubagentsConfig(localStorage.token), + getBaseModels(localStorage.token), + getModels(localStorage.token, null, false) + ]); enabled = config?.ENABLE_SUBAGENTS ?? false; + subagentModelId = config?.SUBAGENTS_MODEL_ID ?? ''; backgroundEnabled = config?.SUBAGENTS_BACKGROUND_ENABLED ?? false; maxConcurrent = Number(config?.SUBAGENTS_MAX_CONCURRENT) || 20; maxAsync = Number(config?.SUBAGENTS_MAX_ASYNC) || 20; maxIterations = Number(config?.SUBAGENTS_MAX_ITERATIONS) || 30; maxOutput = Number(config?.SUBAGENTS_MAX_OUTPUT) || 30000; systemPrompt = config?.SUBAGENTS_SYSTEM_PROMPT ?? ''; + models = baseModels.map((model: any) => { + const workspaceModel = workspaceModels.find((item: any) => item.id === model.id); + + return workspaceModel + ? { ...model, ...workspaceModel } + : { ...model, id: model.id, name: model.name, is_active: true }; + }); } catch (error) { toast.error(`${error}`); } finally { @@ -40,6 +70,7 @@ try { await setSubagentsConfig(localStorage.token, { ENABLE_SUBAGENTS: enabled, + SUBAGENTS_MODEL_ID: subagentModelId, SUBAGENTS_BACKGROUND_ENABLED: backgroundEnabled, SUBAGENTS_MAX_CONCURRENT: maxConcurrent, SUBAGENTS_MAX_ASYNC: maxAsync, @@ -77,6 +108,34 @@

{#if enabled} +
+ + { + subagentModelId = normalizeModelSelection(subagentModelId); + }} + > + + {#each models as model} + + {/each} + +

+ {$i18n.t( + 'Choose a dedicated model for sub-agents. Current Model follows the active chat model.' + )} +

+
+