chore: format
This commit is contained in:
@@ -426,7 +426,7 @@ async def get_current_user_by_api_key(request, api_key: str):
|
||||
allowed_paths = [
|
||||
path.strip() for path in str(request.app.state.config.API_KEYS_ALLOWED_ENDPOINTS).split(',') if path.strip()
|
||||
]
|
||||
request_path = request.scope["path"] # Use raw ASGI path — not spoofable via Host header (CVE-2026-48710)
|
||||
request_path = request.scope['path'] # Use raw ASGI path — not spoofable via Host header (CVE-2026-48710)
|
||||
is_allowed = any(request_path == allowed or request_path.startswith(allowed + '/') for allowed in allowed_paths)
|
||||
if not is_allowed:
|
||||
raise HTTPException(
|
||||
|
||||
@@ -191,11 +191,7 @@ async def get_image_base64_from_file_id(id: str, user=None) -> Optional[str]:
|
||||
# Owner, admin, and explicit read-grant holders are allowed.
|
||||
if user is None:
|
||||
return None
|
||||
if (
|
||||
file.user_id != user.id
|
||||
and user.role != 'admin'
|
||||
and not await has_access_to_file(file.id, 'read', user)
|
||||
):
|
||||
if file.user_id != user.id and user.role != 'admin' and not await has_access_to_file(file.id, 'read', user):
|
||||
return None
|
||||
|
||||
try:
|
||||
|
||||
@@ -41,4 +41,3 @@ def get_custom_headers(custom_headers: dict, user=None, metadata: dict = None) -
|
||||
parsed_headers[key] = value
|
||||
|
||||
return parsed_headers
|
||||
|
||||
|
||||
@@ -67,9 +67,7 @@ def _json_sink(message: 'Message') -> None:
|
||||
log_entry['error'] = {
|
||||
'type': exc.type.__name__ if exc.type else None,
|
||||
'message': str(exc.value) if exc.value else None,
|
||||
'stacktrace': ''.join(
|
||||
traceback.format_exception(exc.type, exc.value, exc.traceback)
|
||||
).rstrip(),
|
||||
'stacktrace': ''.join(traceback.format_exception(exc.type, exc.value, exc.traceback)).rstrip(),
|
||||
}
|
||||
|
||||
sys.stdout.write(json.dumps(log_entry, ensure_ascii=False, default=str) + '\n')
|
||||
|
||||
@@ -11,7 +11,11 @@ from mcp import ClientSession
|
||||
from mcp.client.auth import OAuthClientProvider, TokenStorage
|
||||
from mcp.client.streamable_http import streamablehttp_client
|
||||
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken
|
||||
from open_webui.env import AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL, AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER, MCP_INITIALIZE_TIMEOUT
|
||||
from open_webui.env import (
|
||||
AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL,
|
||||
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER,
|
||||
MCP_INITIALIZE_TIMEOUT,
|
||||
)
|
||||
|
||||
|
||||
def _build_httpx_client(headers=None, timeout=None, auth=None, verify=True):
|
||||
|
||||
@@ -2257,7 +2257,6 @@ def strip_skill_mentions(messages: list[dict]) -> None:
|
||||
part['text'] = strip_re.sub(r'\1', text).strip()
|
||||
|
||||
|
||||
|
||||
async def connect_mcp_server(
|
||||
request,
|
||||
server_id: str,
|
||||
@@ -2271,10 +2270,7 @@ async def connect_mcp_server(
|
||||
"""
|
||||
mcp_server_connection = None
|
||||
for server_connection in request.app.state.config.TOOL_SERVER_CONNECTIONS:
|
||||
if (
|
||||
server_connection.get('type', '') == 'mcp'
|
||||
and server_connection.get('info', {}).get('id') == server_id
|
||||
):
|
||||
if server_connection.get('type', '') == 'mcp' and server_connection.get('info', {}).get('id') == server_id:
|
||||
mcp_server_connection = server_connection
|
||||
break
|
||||
|
||||
@@ -2287,8 +2283,12 @@ async def connect_mcp_server(
|
||||
return None
|
||||
|
||||
headers, _ = await build_tool_server_headers(
|
||||
mcp_server_connection, request, user,
|
||||
server_id=server_id, metadata=metadata, extra_params=extra_params,
|
||||
mcp_server_connection,
|
||||
request,
|
||||
user,
|
||||
server_id=server_id,
|
||||
metadata=metadata,
|
||||
extra_params=extra_params,
|
||||
)
|
||||
|
||||
client = MCPClient()
|
||||
@@ -2297,18 +2297,13 @@ async def connect_mcp_server(
|
||||
headers=headers if headers else None,
|
||||
)
|
||||
|
||||
function_name_filter_list = mcp_server_connection.get('config', {}).get(
|
||||
'function_name_filter_list', ''
|
||||
)
|
||||
function_name_filter_list = mcp_server_connection.get('config', {}).get('function_name_filter_list', '')
|
||||
if isinstance(function_name_filter_list, str):
|
||||
function_name_filter_list = function_name_filter_list.split(',')
|
||||
|
||||
tool_specs = await client.list_tool_specs()
|
||||
if function_name_filter_list:
|
||||
tool_specs = [
|
||||
spec for spec in tool_specs
|
||||
if is_string_allowed(spec['name'], function_name_filter_list)
|
||||
]
|
||||
tool_specs = [spec for spec in tool_specs if is_string_allowed(spec['name'], function_name_filter_list)]
|
||||
|
||||
return client, tool_specs
|
||||
|
||||
@@ -2730,10 +2725,14 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
||||
for tool_id in tool_ids:
|
||||
if tool_id.startswith('server:mcp:'):
|
||||
try:
|
||||
server_id = tool_id[len('server:mcp:'):]
|
||||
server_id = tool_id[len('server:mcp:') :]
|
||||
|
||||
result = await connect_mcp_server(
|
||||
request, server_id, user, metadata, extra_params,
|
||||
request,
|
||||
server_id,
|
||||
user,
|
||||
metadata,
|
||||
extra_params,
|
||||
)
|
||||
if result is None:
|
||||
continue
|
||||
@@ -2742,6 +2741,7 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
||||
mcp_clients[server_id] = client
|
||||
|
||||
for tool_spec in tool_specs:
|
||||
|
||||
async def make_tool_function(client, function_name):
|
||||
async def tool_function(**kwargs):
|
||||
return await client.call_tool(
|
||||
@@ -3392,7 +3392,9 @@ async def outlet_filter_handler(ctx):
|
||||
if outlet_message_id and outlet_message_id in messages_map:
|
||||
original_message = messages_map[outlet_message_id]
|
||||
content_changed = original_message.get('content') != message.get('content')
|
||||
output_changed = message.get('output') and message.get('output') != original_message.get('output')
|
||||
output_changed = message.get('output') and message.get('output') != original_message.get(
|
||||
'output'
|
||||
)
|
||||
if content_changed or output_changed:
|
||||
# If output was modified, re-derive content from it
|
||||
new_content = message.get('content', original_message.get('content', ''))
|
||||
@@ -3874,7 +3876,7 @@ async def streaming_chat_response_handler(response, ctx):
|
||||
# Mirror the five gates from utils/tools.py get_builtin_tools so the
|
||||
# legacy XML-tag path enforces the same authz as native FC.
|
||||
features = metadata.get('features', {}) or {}
|
||||
model_capabilities = (model.get('info', {}).get('meta', {}).get('capabilities') or {})
|
||||
model_capabilities = model.get('info', {}).get('meta', {}).get('capabilities') or {}
|
||||
builtin_tools_meta = model.get('info', {}).get('meta', {}).get('builtinTools', {})
|
||||
DETECT_CODE_INTERPRETER = (
|
||||
bool(features.get('code_interpreter'))
|
||||
@@ -4401,7 +4403,9 @@ async def streaming_chat_response_handler(response, ctx):
|
||||
if end:
|
||||
break
|
||||
|
||||
if ENABLE_REALTIME_CHAT_SAVE and not metadata.get('chat_id', '').startswith('channel:'):
|
||||
if ENABLE_REALTIME_CHAT_SAVE and not metadata.get('chat_id', '').startswith(
|
||||
'channel:'
|
||||
):
|
||||
# Save message in the database
|
||||
await Chats.upsert_message_to_chat_by_id_and_message_id(
|
||||
metadata['chat_id'],
|
||||
|
||||
@@ -141,9 +141,7 @@ def reconcile_tool_pairs(messages: list[dict]) -> list[dict]:
|
||||
Well-formed output is unaffected: every id pairs, so nothing is stripped.
|
||||
"""
|
||||
completed_tool_call_ids = {
|
||||
message['tool_call_id']
|
||||
for message in messages
|
||||
if message.get('role') == 'tool' and message.get('tool_call_id')
|
||||
message['tool_call_id'] for message in messages if message.get('role') == 'tool' and message.get('tool_call_id')
|
||||
}
|
||||
requested_tool_call_ids = {
|
||||
tool_call['id']
|
||||
@@ -167,9 +165,7 @@ def reconcile_tool_pairs(messages: list[dict]) -> list[dict]:
|
||||
|
||||
# Keep only tool_calls whose id received a tool-role response.
|
||||
valid_tool_calls = [
|
||||
tool_call
|
||||
for tool_call in message['tool_calls']
|
||||
if tool_call.get('id') in completed_tool_call_ids
|
||||
tool_call for tool_call in message['tool_calls'] if tool_call.get('id') in completed_tool_call_ids
|
||||
]
|
||||
|
||||
if valid_tool_calls:
|
||||
@@ -181,9 +177,7 @@ def reconcile_tool_pairs(messages: list[dict]) -> list[dict]:
|
||||
content = message.get('content', '')
|
||||
has_meaningful_content = content.strip() if isinstance(content, str) else content
|
||||
if has_meaningful_content or message.get('reasoning_content'):
|
||||
reconciled_messages.append(
|
||||
{key: value for key, value in message.items() if key != 'tool_calls'}
|
||||
)
|
||||
reconciled_messages.append({key: value for key, value in message.items() if key != 'tool_calls'})
|
||||
|
||||
return reconciled_messages
|
||||
|
||||
|
||||
@@ -375,7 +375,9 @@ async def get_protected_resource_metadata(server_url: str) -> ProtectedResourceM
|
||||
except Exception as e:
|
||||
log.debug(f'MCP Protected Resource discovery failed: {e}')
|
||||
|
||||
return ProtectedResourceMetadata(resource=resource, authorization_servers=authorization_servers, scopes_supported=scopes)
|
||||
return ProtectedResourceMetadata(
|
||||
resource=resource, authorization_servers=authorization_servers, scopes_supported=scopes
|
||||
)
|
||||
|
||||
|
||||
def _build_well_known_urls(server_url: str) -> list[str]:
|
||||
|
||||
@@ -29,12 +29,12 @@ from open_webui.env import (
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
_ACCEPTED_SCHEMES = frozenset({"redis", "rediss"})
|
||||
_ACCEPTED_SCHEMES = frozenset({'redis', 'rediss'})
|
||||
_SENTINEL_RETRYABLE = (
|
||||
_redis_sync.exceptions.ConnectionError,
|
||||
_redis_sync.exceptions.ReadOnlyError,
|
||||
)
|
||||
_FACTORY_METHODS = frozenset({"pipeline", "pubsub", "monitor", "client", "transaction"})
|
||||
_FACTORY_METHODS = frozenset({'pipeline', 'pubsub', 'monitor', 'client', 'transaction'})
|
||||
_CONNECTION_POOL: dict[tuple, Any] = {}
|
||||
|
||||
|
||||
@@ -42,17 +42,16 @@ def parse_redis_url(url: str) -> dict[str, Any]:
|
||||
"""Break a ``redis://`` URL into its parts: service, port, db, username, password."""
|
||||
parts: ParseResult = urlparse(url)
|
||||
if parts.scheme not in _ACCEPTED_SCHEMES:
|
||||
raise ValueError(
|
||||
f"Invalid Redis URL scheme '{parts.scheme}'; expected 'redis' or 'rediss'."
|
||||
)
|
||||
raise ValueError(f"Invalid Redis URL scheme '{parts.scheme}'; expected 'redis' or 'rediss'.")
|
||||
return {
|
||||
"service": parts.hostname or "mymaster",
|
||||
"port": parts.port or 6379,
|
||||
"db": int(parts.path.lstrip("/") or "0"),
|
||||
"username": parts.username or None,
|
||||
"password": parts.password or None,
|
||||
'service': parts.hostname or 'mymaster',
|
||||
'port': parts.port or 6379,
|
||||
'db': int(parts.path.lstrip('/') or '0'),
|
||||
'username': parts.username or None,
|
||||
'password': parts.password or None,
|
||||
}
|
||||
|
||||
|
||||
parse_redis_service_url = parse_redis_url
|
||||
|
||||
|
||||
@@ -64,11 +63,7 @@ def get_sentinels_from_env(
|
||||
if not hosts_csv:
|
||||
return []
|
||||
resolved_port = int(port) if port else 26379
|
||||
return [
|
||||
(host.strip(), resolved_port)
|
||||
for host in hosts_csv.split(",")
|
||||
if host.strip()
|
||||
]
|
||||
return [(host.strip(), resolved_port) for host in hosts_csv.split(',') if host.strip()]
|
||||
|
||||
|
||||
def build_sentinel_url(
|
||||
@@ -82,13 +77,11 @@ def build_sentinel_url(
|
||||
``hosts_csv`` is a comma-separated list of sentinel hostnames.
|
||||
"""
|
||||
cfg = parse_redis_url(base_url)
|
||||
auth = ""
|
||||
if cfg["username"] or cfg["password"]:
|
||||
auth = f"{cfg['username'] or ''}:{cfg['password'] or ''}@"
|
||||
nodes = ",".join(
|
||||
f"{host.strip()}:{port}" for host in hosts_csv.split(",") if host.strip()
|
||||
)
|
||||
return f"redis+sentinel://{auth}{nodes}/{cfg['db']}/{cfg['service']}"
|
||||
auth = ''
|
||||
if cfg['username'] or cfg['password']:
|
||||
auth = f'{cfg["username"] or ""}:{cfg["password"] or ""}@'
|
||||
nodes = ','.join(f'{host.strip()}:{port}' for host in hosts_csv.split(',') if host.strip())
|
||||
return f'redis+sentinel://{auth}{nodes}/{cfg["db"]}/{cfg["service"]}'
|
||||
|
||||
|
||||
def get_redis_client(async_mode: bool = False) -> Any | None:
|
||||
@@ -107,7 +100,7 @@ def get_redis_client(async_mode: bool = False) -> Any | None:
|
||||
async_mode=async_mode,
|
||||
)
|
||||
except Exception:
|
||||
log.debug("Could not establish Redis connection", exc_info=True)
|
||||
log.debug('Could not establish Redis connection', exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
@@ -157,7 +150,7 @@ class SentinelRedisProxy:
|
||||
|
||||
def _log_retry(self, exc: Exception, attempt: int) -> None:
|
||||
log.debug(
|
||||
"Sentinel failover (%s) — retry %d/%d",
|
||||
'Sentinel failover (%s) — retry %d/%d',
|
||||
type(exc).__name__,
|
||||
attempt + 1,
|
||||
REDIS_SENTINEL_MAX_RETRY_COUNT,
|
||||
@@ -165,7 +158,7 @@ class SentinelRedisProxy:
|
||||
|
||||
def _log_exhausted(self, exc: Exception) -> None:
|
||||
log.error(
|
||||
"Redis operation failed after %d retries: %s",
|
||||
'Redis operation failed after %d retries: %s',
|
||||
REDIS_SENTINEL_MAX_RETRY_COUNT,
|
||||
exc,
|
||||
)
|
||||
@@ -254,11 +247,11 @@ def _socket_options() -> dict[str, Any]:
|
||||
"""Collect optional socket-level kwargs once instead of repeating them."""
|
||||
opts: dict[str, Any] = {}
|
||||
if REDIS_SOCKET_CONNECT_TIMEOUT is not None:
|
||||
opts["socket_connect_timeout"] = REDIS_SOCKET_CONNECT_TIMEOUT
|
||||
opts['socket_connect_timeout'] = REDIS_SOCKET_CONNECT_TIMEOUT
|
||||
if REDIS_SOCKET_KEEPALIVE:
|
||||
opts["socket_keepalive"] = True
|
||||
opts['socket_keepalive'] = True
|
||||
if REDIS_HEALTH_CHECK_INTERVAL:
|
||||
opts["health_check_interval"] = REDIS_HEALTH_CHECK_INTERVAL
|
||||
opts['health_check_interval'] = REDIS_HEALTH_CHECK_INTERVAL
|
||||
return opts
|
||||
|
||||
|
||||
@@ -273,15 +266,15 @@ def _build_sentinel(
|
||||
cfg = parse_redis_url(url)
|
||||
sentinel = redis_module.sentinel.Sentinel(
|
||||
sentinels,
|
||||
port=cfg["port"],
|
||||
db=cfg["db"],
|
||||
username=cfg["username"],
|
||||
password=cfg["password"],
|
||||
port=cfg['port'],
|
||||
db=cfg['db'],
|
||||
username=cfg['username'],
|
||||
password=cfg['password'],
|
||||
decode_responses=decode_responses,
|
||||
socket_connect_timeout=REDIS_SOCKET_CONNECT_TIMEOUT,
|
||||
**{k: v for k, v in _socket_options().items() if k != "socket_connect_timeout"},
|
||||
**{k: v for k, v in _socket_options().items() if k != 'socket_connect_timeout'},
|
||||
)
|
||||
return SentinelRedisProxy(sentinel, cfg["service"], async_mode=async_mode)
|
||||
return SentinelRedisProxy(sentinel, cfg['service'], async_mode=async_mode)
|
||||
|
||||
|
||||
def get_redis_connection(
|
||||
@@ -320,12 +313,14 @@ def get_redis_connection(
|
||||
connection = _build_sentinel(redis_mod, redis_url, redis_sentinels, decode_responses, async_mode)
|
||||
elif redis_cluster:
|
||||
if not redis_url:
|
||||
raise ValueError("Redis URL is required for cluster mode.")
|
||||
raise ValueError('Redis URL is required for cluster mode.')
|
||||
connection = redis_mod.cluster.RedisCluster.from_url(
|
||||
redis_url, decode_responses=decode_responses, **extra,
|
||||
redis_url,
|
||||
decode_responses=decode_responses,
|
||||
**extra,
|
||||
)
|
||||
elif redis_url:
|
||||
factory = getattr(redis_mod, "from_url", None) or redis_mod.Redis.from_url
|
||||
factory = getattr(redis_mod, 'from_url', None) or redis_mod.Redis.from_url
|
||||
connection = factory(redis_url, decode_responses=decode_responses, **extra)
|
||||
|
||||
_CONNECTION_POOL[cache_key] = connection
|
||||
|
||||
@@ -376,8 +376,12 @@ async def get_tools(request: Request, tool_ids: list[str], user: UserModel, extr
|
||||
|
||||
metadata = extra_params.get('__metadata__', {})
|
||||
headers, cookies = await build_tool_server_headers(
|
||||
tool_server_connection, request, user,
|
||||
server_id=server_id, metadata=metadata, extra_params=extra_params,
|
||||
tool_server_connection,
|
||||
request,
|
||||
user,
|
||||
server_id=server_id,
|
||||
metadata=metadata,
|
||||
extra_params=extra_params,
|
||||
)
|
||||
headers.setdefault('Content-Type', 'application/json')
|
||||
|
||||
@@ -484,7 +488,9 @@ async def get_builtin_tools(
|
||||
builtin_functions.append(query_knowledge_bases)
|
||||
builtin_functions.append(search_knowledge_bases)
|
||||
elif model_knowledge:
|
||||
builtin_functions.extend([list_knowledge, search_knowledge_files, grep_knowledge_files, query_knowledge_files])
|
||||
builtin_functions.extend(
|
||||
[list_knowledge, search_knowledge_files, grep_knowledge_files, query_knowledge_files]
|
||||
)
|
||||
|
||||
knowledge_types = {item.get('type') for item in model_knowledge}
|
||||
if 'file' in knowledge_types or 'collection' in knowledge_types:
|
||||
@@ -492,11 +498,17 @@ async def get_builtin_tools(
|
||||
if 'note' in knowledge_types:
|
||||
builtin_functions.append(view_note)
|
||||
else:
|
||||
builtin_functions.extend([
|
||||
list_knowledge_bases, search_knowledge_bases, query_knowledge_bases,
|
||||
grep_knowledge_files, search_knowledge_files, query_knowledge_files,
|
||||
view_knowledge_file,
|
||||
])
|
||||
builtin_functions.extend(
|
||||
[
|
||||
list_knowledge_bases,
|
||||
search_knowledge_bases,
|
||||
query_knowledge_bases,
|
||||
grep_knowledge_files,
|
||||
search_knowledge_files,
|
||||
query_knowledge_files,
|
||||
view_knowledge_file,
|
||||
]
|
||||
)
|
||||
|
||||
# Chats tools - search and fetch user's chat history
|
||||
if is_builtin_tool_enabled('chats'):
|
||||
|
||||
@@ -76,8 +76,7 @@ def validate_profile_image_url(url: str) -> str:
|
||||
if _SAFE_DATA_URI_RE.match(url):
|
||||
if PROFILE_IMAGE_MAX_DATA_URI_SIZE and len(url) > PROFILE_IMAGE_MAX_DATA_URI_SIZE:
|
||||
raise ValueError(
|
||||
f'Invalid profile image URL: data URI exceeds the '
|
||||
f'{PROFILE_IMAGE_MAX_DATA_URI_SIZE}-byte limit.'
|
||||
f'Invalid profile image URL: data URI exceeds the {PROFILE_IMAGE_MAX_DATA_URI_SIZE}-byte limit.'
|
||||
)
|
||||
return url
|
||||
|
||||
|
||||
Reference in New Issue
Block a user