feat: add configurable subagent model

This commit is contained in:
Mord0reK 2026-07-27 17:29:22 +02:00
parent d02b6a21fc
commit ff3da6a5ee
4 changed files with 98 additions and 10 deletions

View file

@ -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,

View file

@ -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

View file

@ -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()),
}

View file

@ -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 @@
</p>
{#if enabled}
<div>
<label class="text-xs text-gray-600 dark:text-gray-400" for="sa-model">
{$i18n.t('Sub-agent Model')}
</label>
<SettingsSelect
id="sa-model"
bind:value={subagentModelId}
className="mt-1 w-full"
placeholder={$i18n.t('Select a model')}
on:change={() => {
subagentModelId = normalizeModelSelection(subagentModelId);
}}
>
<option value="" selected>{$i18n.t('Current Model')}</option>
{#each models as model}
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">
{model.name}
{model?.connection_type === 'local' ? `(${$i18n.t('Local')})` : ''}
</option>
{/each}
</SettingsSelect>
<p class="mt-1 text-[0.6875rem] text-gray-400 dark:text-gray-600">
{$i18n.t(
'Choose a dedicated model for sub-agents. Current Model follows the active chat model.'
)}
</p>
</div>
<div>
<label class="text-xs text-gray-600 dark:text-gray-400" for="sa-concurrent">
{$i18n.t('Max concurrent')}