This commit is contained in:
Timothy Jaeryang Baek 2026-10-16 14:31:39 +04:00
parent 8c56361ee0
commit 92e64c1d59
8 changed files with 77 additions and 12 deletions

View file

@ -2058,6 +2058,14 @@ ENABLE_FOLDERS = os.getenv('ENABLE_FOLDERS', 'True').lower() == 'true'
FOLDER_MAX_FILE_COUNT = os.getenv('FOLDER_MAX_FILE_COUNT', '')
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')
SKILL_PREVIEW_MAX_CHARS = int(os.environ['SKILL_PREVIEW_MAX_CHARS']) if os.getenv('SKILL_PREVIEW_MAX_CHARS') else None
if SKILL_PREVIEW_MAX_CHARS is not None and SKILL_PREVIEW_MAX_CHARS <= 0:
raise ValueError('SKILL_PREVIEW_MAX_CHARS must be a positive integer or unset')
ENABLE_CHANNELS = os.getenv('ENABLE_CHANNELS', 'False').lower() == 'true'
ENABLE_ENCRYPTED_CHANNELS = os.getenv('ENABLE_ENCRYPTED_CHANNELS', 'False').lower() == 'true'
@ -2892,6 +2900,8 @@ 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.content_max_chars': SKILL_CONTENT_MAX_CHARS,
'skills.preview_max_chars': SKILL_PREVIEW_MAX_CHARS,
'direct.enable': ENABLE_DIRECT_CONNECTIONS,
'direct.integrations.enable': ENABLE_DIRECT_INTEGRATIONS,
'ollama.enable': ENABLE_OLLAMA_API,

View file

@ -82,7 +82,7 @@ from open_webui.utils.json_codec import JSONCodec
from open_webui.utils.mfa import MFA_CONFIG_KEYS, MfaConfigForm, get_mfa_config, limit_account, update_mfa_config
from open_webui.utils.misc import parse_duration, validate_email_format
from open_webui.utils.rate_limit import RateLimiter
from pydantic import BaseModel, StrictStr, field_validator
from pydantic import BaseModel, Field, StrictStr, field_validator
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
@ -134,6 +134,8 @@ ADMIN_CONFIG_KEYS = {
'ENABLE_CALENDAR': 'calendar.enable',
'ENABLE_MEMORIES': 'memories.enable',
'ENABLE_MEMORY_SYSTEM_CONTEXT': 'memories.system_context.enable',
'SKILL_CONTENT_MAX_CHARS': 'skills.content_max_chars',
'SKILL_PREVIEW_MAX_CHARS': 'skills.preview_max_chars',
'ENABLE_NOTES': 'notes.enable',
'ENABLE_USER_WEBHOOKS': 'ui.enable_user_webhooks',
'ENABLE_USER_STATUS': 'users.enable_status',
@ -1185,6 +1187,8 @@ class AdminConfig(MfaConfigForm):
ENABLE_CALENDAR: bool
ENABLE_MEMORIES: bool
ENABLE_MEMORY_SYSTEM_CONTEXT: bool
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
ENABLE_USER_WEBHOOKS: bool
ENABLE_USER_STATUS: bool
@ -1233,6 +1237,10 @@ 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 '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:
updates.pop('skills.preview_max_chars', None)
updates['ui.default_interface_settings'] = form_data.DEFAULT_INTERFACE_SETTINGS or {}
updates['folders.max_file_count'] = int(form_data.FOLDER_MAX_FILE_COUNT) if form_data.FOLDER_MAX_FILE_COUNT else ''
updates['automations.max_count'] = int(form_data.AUTOMATION_MAX_COUNT) if form_data.AUTOMATION_MAX_COUNT else ''

View file

@ -3550,7 +3550,7 @@ async def view_skill(
{
'id': skill.id,
'name': snapshot['name'],
**skill_overview(snapshot['content'], file_summaries(snapshot['data']['files'])),
**await skill_overview(snapshot['content'], file_summaries(snapshot['data']['files'])),
},
ensure_ascii=False,
)
@ -3573,7 +3573,7 @@ async def read_skill_file(
:param id: Skill ID.
:param path: Relative path within the skill.
:param offset: Character offset for paging text.
:param max_chars: Maximum characters to return, up to 20000 (default 10000).
:param max_chars: Maximum characters to return, capped by the admin skill content limit (default 10000).
"""
try:
from types import SimpleNamespace
@ -3627,7 +3627,7 @@ async def read_skill_file(
return JSONCodec.dumps(
{
'path': path,
**skill_content_page(file['content'], offset, max_chars),
**await skill_content_page(file['content'], offset, max_chars),
},
ensure_ascii=False,
)

View file

@ -2531,7 +2531,7 @@ async def process_chat_payload(request, form_data, user, metadata, model):
continue
skill_versions[skill.id] = version_id
skill_tools_enabled = use_builtin_tools and model_builtin_tools.get('skills', True)
overview = skill_overview(
overview = await skill_overview(
snapshot['content'],
[f['path'] for f in snapshot['data']['files'] if f['path'] != 'SKILL.md'],
tools_enabled=skill_tools_enabled,

View file

@ -31,9 +31,15 @@ SKILL_GREP_EXCERPT_MAX_CHARS = 1_200
SKILL_GREP_LOCATIONS_MAX_CHARS = 2_000
def skill_content_page(content: str, offset: int = 0, max_chars: int = SKILL_CONTENT_MAX_CHARS) -> dict:
async def skill_content_page(
content: str, offset: int = 0, max_chars: int = SKILL_CONTENT_MAX_CHARS, *, limit: int | None = None
) -> dict:
if limit is None:
from open_webui.models.config import Config
limit = await Config.get('skills.content_max_chars') or SKILL_CONTENT_MAX_CHARS
offset = max(0, offset)
end = offset + min(SKILL_CONTENT_MAX_CHARS, max(1, max_chars))
end = offset + min(limit, max(1, max_chars))
return {'content': content[offset:end], 'next_offset': end if end < len(content) else None}
@ -196,16 +202,21 @@ def grep_skill_content(files: list[dict], pattern: str, case_insensitive: bool =
return serialized
def skill_overview(content: str, files: list, field: str = 'files', tools_enabled: bool = True) -> dict:
async def skill_overview(content: str, files: list, field: str = 'files', tools_enabled: bool = True) -> dict:
if not tools_enabled:
return {
'content': content[:SKILL_INJECTION_MAX_CHARS],
'next_offset': SKILL_INJECTION_MAX_CHARS if len(content) > SKILL_INJECTION_MAX_CHARS else None,
**bounded_skill_manifest(files, field),
}
large = len(content) > SKILL_CONTENT_MAX_CHARS
from open_webui.models.config import Config
limits = await Config.get_many('skills.content_max_chars', 'skills.preview_max_chars')
content_limit = limits.get('skills.content_max_chars') or SKILL_CONTENT_MAX_CHARS
preview_limit = limits.get('skills.preview_max_chars') or SKILL_PREVIEW_MAX_CHARS
large = len(content) > content_limit
return {
**skill_content_page(content, max_chars=SKILL_PREVIEW_MAX_CHARS if large else SKILL_CONTENT_MAX_CHARS),
**await skill_content_page(content, max_chars=preview_limit if large else content_limit, limit=content_limit),
**bounded_skill_manifest(files, field, SKILL_OVERVIEW_LIST_MAX_CHARS),
}

View file

@ -287,10 +287,10 @@ async def get_terminal_skill(
'description': skill.get('description'),
'directory': directory,
**(
skill_overview(skill['content'], resources, 'resources', tools_enabled)
await skill_overview(skill['content'], resources, 'resources', tools_enabled)
if overview
else {
**skill_content_page(skill['content'], offset, max_chars),
**await skill_content_page(skill['content'], offset, max_chars),
**bounded_skill_manifest(resources, 'resources'),
}
),

View file

@ -84,6 +84,8 @@
const cleaned = entriesToI18n(uiI18nEntries);
const res = await updateAdminConfig(localStorage.token, {
...adminConfig,
SKILL_CONTENT_MAX_CHARS: adminConfig.SKILL_CONTENT_MAX_CHARS ?? null,
SKILL_PREVIEW_MAX_CHARS: adminConfig.SKILL_PREVIEW_MAX_CHARS ?? null,
DEFAULT_INTERFACE_SETTINGS: defaultInterfaceSettings,
I18N: cleaned
});
@ -337,6 +339,36 @@
/>
</AdminSettingRow>
{/if}
<AdminSettingField
forId="skill-content-max-chars"
label={$i18n.t('settings.admin.general.skillContentMaxChars.label')}
description={$i18n.t('settings.admin.general.skillContentMaxChars.description')}
>
<input
id="skill-content-max-chars"
class={inputClass}
type="number"
min="1"
step="1"
placeholder={$i18n.t('Default')}
bind:value={adminConfig.SKILL_CONTENT_MAX_CHARS}
/>
</AdminSettingField>
<AdminSettingField
forId="skill-preview-max-chars"
label={$i18n.t('settings.admin.general.skillPreviewMaxChars.label')}
description={$i18n.t('settings.admin.general.skillPreviewMaxChars.description')}
>
<input
id="skill-preview-max-chars"
class={inputClass}
type="number"
min="1"
step="1"
placeholder={$i18n.t('Default')}
bind:value={adminConfig.SKILL_PREVIEW_MAX_CHARS}
/>
</AdminSettingField>
<AdminSettingRow
label={$i18n.t('settings.admin.general.notes.label')}
description={$i18n.t('settings.admin.general.notes.description')}

View file

@ -2761,6 +2761,10 @@
"settings.admin.general.modelResponseMode.description": "Choose where model responses to root-level channel mentions are posted.",
"settings.admin.general.modelResponseMode.label": "Model Response Mode",
"settings.admin.general.notes.description": "Allow users to create and manage notes.",
"settings.admin.general.skillContentMaxChars.label": "Skill Read Limit",
"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.skillPreviewMaxChars.label": "Skill Preview Length",
"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.notes.label": "Notes",
"settings.admin.general.responseWatermark.description": "Append a watermark to assistant responses when configured.",
"settings.admin.general.responseWatermark.label": "Response Watermark",