diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py
index c91a5f7e6d..832f1b1ce7 100644
--- a/backend/open_webui/config.py
+++ b/backend/open_webui/config.py
@@ -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,
diff --git a/backend/open_webui/routers/auths.py b/backend/open_webui/routers/auths.py
index b48d6b1c45..701acfb360 100644
--- a/backend/open_webui/routers/auths.py
+++ b/backend/open_webui/routers/auths.py
@@ -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 ''
diff --git a/backend/open_webui/tools/builtin.py b/backend/open_webui/tools/builtin.py
index 297b3b055c..dcbdfe5201 100644
--- a/backend/open_webui/tools/builtin.py
+++ b/backend/open_webui/tools/builtin.py
@@ -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,
)
diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py
index 6d8cfe2b0a..7a806a7ebe 100644
--- a/backend/open_webui/utils/middleware.py
+++ b/backend/open_webui/utils/middleware.py
@@ -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,
diff --git a/backend/open_webui/utils/skill_files.py b/backend/open_webui/utils/skill_files.py
index 220f94fa12..47a53013df 100644
--- a/backend/open_webui/utils/skill_files.py
+++ b/backend/open_webui/utils/skill_files.py
@@ -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),
}
diff --git a/backend/open_webui/utils/terminals.py b/backend/open_webui/utils/terminals.py
index b1432c50dd..1bb2369629 100644
--- a/backend/open_webui/utils/terminals.py
+++ b/backend/open_webui/utils/terminals.py
@@ -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'),
}
),
diff --git a/src/lib/components/admin/Settings/General.svelte b/src/lib/components/admin/Settings/General.svelte
index 0256410e30..db9c9ea478 100644
--- a/src/lib/components/admin/Settings/General.svelte
+++ b/src/lib/components/admin/Settings/General.svelte
@@ -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 @@
/>
{/if}
+
+
+
+
+
+