This commit is contained in:
Timothy Jaeryang Baek
2026-11-08 19:06:43 +09:00
parent d4e7005954
commit 9b9bea514d
13 changed files with 91 additions and 109 deletions
+8 -19
View File
@@ -2263,20 +2263,14 @@ ENABLE_TOOL_PERMISSIONS = os.getenv('ENABLE_TOOL_PERMISSIONS', 'False').lower()
ENABLE_TOOL_CALL_LABELS = os.getenv('ENABLE_TOOL_CALL_LABELS', 'False').lower() == 'true'
ENABLE_TOOL_PERMISSIONS_AUTO = os.getenv('ENABLE_TOOL_PERMISSIONS_AUTO', 'False').lower() == 'true'
TOOL_PERMISSIONS_AUTO_ENGINE = os.getenv('TOOL_PERMISSIONS_AUTO_ENGINE', '')
# None distinguishes an unset prompt from an explicit reset to the default policy.
TOOL_PERMISSIONS_AUTO_PROMPT = os.getenv('TOOL_PERMISSIONS_AUTO_PROMPT')
TOOL_PERMISSIONS_AUTO_DECISIONS_API_BASE_URL = os.getenv('TOOL_PERMISSIONS_AUTO_DECISIONS_API_BASE_URL')
TOOL_PERMISSIONS_AUTO_DECISIONS_API_KEY = os.getenv('TOOL_PERMISSIONS_AUTO_DECISIONS_API_KEY')
TOOL_PERMISSIONS_AUTO_DECISIONS_MODEL = os.getenv('TOOL_PERMISSIONS_AUTO_DECISIONS_MODEL')
TOOL_PERMISSIONS_AUTO_DECISIONS_THRESHOLD = os.getenv('TOOL_PERMISSIONS_AUTO_DECISIONS_THRESHOLD')
if TOOL_PERMISSIONS_AUTO_DECISIONS_THRESHOLD is not None:
TOOL_PERMISSIONS_AUTO_DECISIONS_THRESHOLD = float(TOOL_PERMISSIONS_AUTO_DECISIONS_THRESHOLD)
# Deprecated environment defaults, used only when their Auto replacements are unset.
TOOL_PERMISSIONS_DECISIONS_API_BASE_URL = os.getenv('TOOL_PERMISSIONS_DECISIONS_API_BASE_URL', 'https://api.openai.com/v1')
TOOL_PERMISSIONS_DECISIONS_API_KEY = os.getenv('TOOL_PERMISSIONS_DECISIONS_API_KEY', '')
TOOL_PERMISSIONS_DECISIONS_MODEL = os.getenv('TOOL_PERMISSIONS_DECISIONS_MODEL', 'gpt-6-luna')
TOOL_PERMISSIONS_DECISIONS_THRESHOLD = float(os.getenv('TOOL_PERMISSIONS_DECISIONS_THRESHOLD', '0.5'))
TOOL_PERMISSIONS_DECISIONS_PROMPT = os.getenv('TOOL_PERMISSIONS_DECISIONS_PROMPT', '')
TOOL_PERMISSIONS_AUTO_PROMPT = os.getenv('TOOL_PERMISSIONS_AUTO_PROMPT', '')
TOOL_PERMISSIONS_AUTO_DECISIONS_API_BASE_URL = os.getenv(
'TOOL_PERMISSIONS_AUTO_DECISIONS_API_BASE_URL', 'https://api.openai.com/v1'
)
TOOL_PERMISSIONS_AUTO_DECISIONS_API_KEY = os.getenv('TOOL_PERMISSIONS_AUTO_DECISIONS_API_KEY', '')
TOOL_PERMISSIONS_AUTO_DECISIONS_MODEL = os.getenv('TOOL_PERMISSIONS_AUTO_DECISIONS_MODEL', 'gpt-6-luna')
TOOL_PERMISSIONS_AUTO_DECISIONS_THRESHOLD = float(os.getenv('TOOL_PERMISSIONS_AUTO_DECISIONS_THRESHOLD', '0.5'))
CONTEXT_COMPACTION_TOKEN_THRESHOLD = int(os.getenv('CONTEXT_COMPACTION_TOKEN_THRESHOLD', '80000'))
@@ -3318,11 +3312,6 @@ DEFAULT_CONFIG = {
'chat.tool_permissions.auto.decisions.api_key': TOOL_PERMISSIONS_AUTO_DECISIONS_API_KEY,
'chat.tool_permissions.auto.decisions.model': TOOL_PERMISSIONS_AUTO_DECISIONS_MODEL,
'chat.tool_permissions.auto.decisions.threshold': TOOL_PERMISSIONS_AUTO_DECISIONS_THRESHOLD,
'chat.tool_permissions.decisions.api_base_url': TOOL_PERMISSIONS_DECISIONS_API_BASE_URL,
'chat.tool_permissions.decisions.api_key': TOOL_PERMISSIONS_DECISIONS_API_KEY,
'chat.tool_permissions.decisions.model': TOOL_PERMISSIONS_DECISIONS_MODEL,
'chat.tool_permissions.decisions.threshold': TOOL_PERMISSIONS_DECISIONS_THRESHOLD,
'chat.tool_permissions.decisions.prompt': TOOL_PERMISSIONS_DECISIONS_PROMPT,
'chat.tool_search.enable': ENABLE_TOOL_SEARCH,
'chat.tool_search.engine': TOOL_SEARCH_ENGINE,
'chat.tool_search.decisions.api_base_url': TOOL_SEARCH_DECISIONS_API_BASE_URL,
+5 -1
View File
@@ -1134,7 +1134,11 @@ async def chat_completion(
if len(parts) != 4:
raise HTTPException(400, 'Invalid agent model')
candidates = await get_agent_models(
request, user, form_data.get('terminal_id'), form_data.get('chat_id'), provider=parts[2],
request,
user,
form_data.get('terminal_id'),
form_data.get('chat_id'),
provider=parts[2],
cwd=requested_directory,
)
agent_model = next((item for item in candidates if item['id'] == model_id), None)
+2 -6
View File
@@ -2850,9 +2850,7 @@ class ChatTable:
await session.execute(delete(ChatMessage).filter_by(chat_id=id))
recipients = await delete_resource_comments('chat', select(Chat.id).filter_by(id=id), db=session)
await session.execute(
delete(ChatViewState).where(
ChatViewState.chat_id.in_(select(Chat.id).filter_by(id=id))
)
delete(ChatViewState).where(ChatViewState.chat_id.in_(select(Chat.id).filter_by(id=id)))
)
await session.execute(delete(Chat).filter_by(id=id))
await session.commit()
@@ -2901,9 +2899,7 @@ class ChatTable:
'chat', select(Chat.id).filter_by(user_id=user_id), db=session
)
await session.execute(
delete(ChatViewState).where(
ChatViewState.chat_id.in_(select(Chat.id).filter_by(user_id=user_id))
)
delete(ChatViewState).where(ChatViewState.chat_id.in_(select(Chat.id).filter_by(user_id=user_id)))
)
await session.execute(delete(Chat).filter_by(user_id=user_id))
await session.commit()
+3 -1
View File
@@ -157,7 +157,9 @@ async def delete_notifications(*, resource_type=None, resource_ids=None, object_
return set(recipients)
async def update_notifications_read_at_by_resource_id(*, user_id: str, resource_type: str, resource_id: str, db) -> bool:
async def update_notifications_read_at_by_resource_id(
*, user_id: str, resource_type: str, resource_id: str, db
) -> bool:
now = time.time_ns()
result = await db.execute(
update(Notification)
+1
View File
@@ -924,6 +924,7 @@ async def signout(request: Request, response: Response, db: AsyncSession = Depen
actor = await Users.get_user_by_id(data['id'], db=db)
if data and data.get('typ', 'session') == 'session' and data.get('sid') and data.get('id'):
from open_webui.models.auth_sessions import AuthSessions
await AuthSessions.revoke(data['sid'], data['id'])
await invalidate_token(request, token)
await publish_event(
+5 -2
View File
@@ -1121,8 +1121,11 @@ async def send_notification(request, channel, message, active_user_ids, db=None)
try:
await notify(
u.id, f'channel:{channel.id}:{message.id}', f'{channel.name} / Open WebUI',
message.content, '/channels/' + quote(channel.id, safe=''),
u.id,
f'channel:{channel.id}:{message.id}',
f'{channel.name} / Open WebUI',
message.content,
'/channels/' + quote(channel.id, safe=''),
)
except Exception:
log.exception('Tether channel notification failed')
+7 -38
View File
@@ -50,7 +50,7 @@ from open_webui.utils.misc import get_message_list
from open_webui.utils.models import get_all_models
from open_webui.utils.tool_permissions import DEFAULT_TOOL_PERMISSIONS_AUTO_PROMPT, get_tool_permissions_auto_config
from open_webui.utils.tool_search import DEFAULT_TOOL_SEARCH_DECISIONS_PROMPT, validate_decisions_config
from pydantic import AliasChoices, BaseModel, Field, ValidationError, field_validator, model_validator
from pydantic import BaseModel, Field, ValidationError, field_validator, model_validator
from sqlalchemy.ext.asyncio import AsyncSession
log = logging.getLogger(__name__)
@@ -237,33 +237,12 @@ class ChatConfigForm(BaseModel):
ENABLE_TOOL_PERMISSIONS: bool = False
ENABLE_TOOL_CALL_LABELS: bool = False
ENABLE_TOOL_PERMISSIONS_AUTO: bool = False
TOOL_PERMISSIONS_AUTO_DECISIONS_API_BASE_URL: str = Field(
default='https://api.openai.com/v1',
validation_alias=AliasChoices(
'TOOL_PERMISSIONS_AUTO_DECISIONS_API_BASE_URL', 'TOOL_PERMISSIONS_DECISIONS_API_BASE_URL'
),
)
TOOL_PERMISSIONS_AUTO_DECISIONS_API_KEY: str = Field(
default='',
validation_alias=AliasChoices('TOOL_PERMISSIONS_AUTO_DECISIONS_API_KEY', 'TOOL_PERMISSIONS_DECISIONS_API_KEY'),
)
TOOL_PERMISSIONS_AUTO_DECISIONS_MODEL: str = Field(
default='gpt-6-luna',
validation_alias=AliasChoices('TOOL_PERMISSIONS_AUTO_DECISIONS_MODEL', 'TOOL_PERMISSIONS_DECISIONS_MODEL'),
)
TOOL_PERMISSIONS_AUTO_DECISIONS_THRESHOLD: float = Field(
default=0.5,
validation_alias=AliasChoices(
'TOOL_PERMISSIONS_AUTO_DECISIONS_THRESHOLD', 'TOOL_PERMISSIONS_DECISIONS_THRESHOLD'
),
ge=0,
le=1,
allow_inf_nan=False,
)
TOOL_PERMISSIONS_AUTO_ENGINE: Literal['', 'decisions'] = ''
TOOL_PERMISSIONS_AUTO_PROMPT: str = Field(
default='', validation_alias=AliasChoices('TOOL_PERMISSIONS_AUTO_PROMPT', 'TOOL_PERMISSIONS_DECISIONS_PROMPT')
)
TOOL_PERMISSIONS_AUTO_PROMPT: str = ''
TOOL_PERMISSIONS_AUTO_DECISIONS_API_BASE_URL: str = 'https://api.openai.com/v1'
TOOL_PERMISSIONS_AUTO_DECISIONS_API_KEY: str = ''
TOOL_PERMISSIONS_AUTO_DECISIONS_MODEL: str = 'gpt-6-luna'
TOOL_PERMISSIONS_AUTO_DECISIONS_THRESHOLD: float = Field(default=0.5, ge=0, le=1, allow_inf_nan=False)
ENABLE_TOOL_SEARCH: bool = False
TOOL_SEARCH_ENGINE: Literal['bm25', 'embeddings', 'decisions'] = 'bm25'
TOOL_SEARCH_DECISIONS_API_BASE_URL: str = 'https://api.openai.com/v1'
@@ -303,13 +282,6 @@ class ChatConfigForm(BaseModel):
class ChatConfigResponse(ChatConfigForm):
DEFAULT_TOOL_PERMISSIONS_AUTO_PROMPT: str = DEFAULT_TOOL_PERMISSIONS_AUTO_PROMPT
TOOL_PERMISSIONS_DECISIONS_API_BASE_URL: str = 'https://api.openai.com/v1'
TOOL_PERMISSIONS_DECISIONS_API_KEY: str = ''
TOOL_PERMISSIONS_DECISIONS_MODEL: str = 'gpt-6-luna'
TOOL_PERMISSIONS_DECISIONS_THRESHOLD: float = 0.5
# Deprecated response aliases for older settings clients.
TOOL_PERMISSIONS_DECISIONS_PROMPT: str = ''
DEFAULT_TOOL_PERMISSIONS_DECISIONS_PROMPT: str = DEFAULT_TOOL_PERMISSIONS_AUTO_PROMPT
DEFAULT_TOOL_SEARCH_DECISIONS_PROMPT: str = DEFAULT_TOOL_SEARCH_DECISIONS_PROMPT
TOOL_SEARCH_EMBEDDING_MODEL: str = ''
@@ -329,12 +301,9 @@ async def get_chat_config_values() -> dict:
config['CONTEXT_COMPACTION_RETENTION_PERCENTAGE'] = 40
auto_config = await get_tool_permissions_auto_config()
config['TOOL_PERMISSIONS_AUTO_PROMPT'] = auto_config['prompt']
config['TOOL_PERMISSIONS_DECISIONS_PROMPT'] = auto_config['prompt']
for key in ('api_base_url', 'api_key', 'model', 'threshold'):
config[f'TOOL_PERMISSIONS_AUTO_DECISIONS_{key.upper()}'] = auto_config[key]
config[f'TOOL_PERMISSIONS_DECISIONS_{key.upper()}'] = auto_config[key]
config['DEFAULT_TOOL_PERMISSIONS_AUTO_PROMPT'] = DEFAULT_TOOL_PERMISSIONS_AUTO_PROMPT
config['DEFAULT_TOOL_PERMISSIONS_DECISIONS_PROMPT'] = DEFAULT_TOOL_PERMISSIONS_AUTO_PROMPT
config['DEFAULT_TOOL_SEARCH_DECISIONS_PROMPT'] = DEFAULT_TOOL_SEARCH_DECISIONS_PROMPT
config['TOOL_SEARCH_EMBEDDING_MODEL'] = values.get('rag.embedding_model') or ''
return config
@@ -958,7 +927,7 @@ async def set_chat_config(form_data: ChatConfigForm, user=Depends(get_admin_user
for field in ChatConfigForm.model_fields:
if (
field in {'TOOL_SEARCH_ENGINE', 'ENABLE_TOOL_PERMISSIONS_AUTO', 'ENABLE_TOOL_CALL_LABELS'}
or field.startswith(('TOOL_SEARCH_DECISIONS_', 'TOOL_PERMISSIONS_DECISIONS_', 'TOOL_PERMISSIONS_AUTO_'))
or field.startswith(('TOOL_SEARCH_DECISIONS_', 'TOOL_PERMISSIONS_AUTO_'))
) and (field not in form_data.model_fields_set):
updates[field] = current.get(field, updates[field])
try:
+6 -2
View File
@@ -239,7 +239,9 @@ async def browser_ws(ws: WebSocket, server_id: str, sid: str, ticket: str, kind:
headers['Authorization'] = data['relay']['authorization']
tasks = []
try:
async with get_terminal_client(base, user.id, cookies=data['relay'].get('cookies', {}), trust_env=True) as client:
async with get_terminal_client(
base, user.id, cookies=data['relay'].get('cookies', {}), trust_env=True
) as client:
async with client.ws_connect(
url,
headers=headers,
@@ -398,7 +400,9 @@ async def proxy_terminal(
if content_type:
headers['Content-Type'] = content_type
session = get_terminal_client(base_url, user.id,
session = get_terminal_client(
base_url,
user.id,
timeout=aiohttp.ClientTimeout(total=300, connect=10),
trust_env=True,
)
+4 -1
View File
@@ -123,7 +123,10 @@ async def connect(ws: WebSocket):
if hub.redis:
replaced = await hub.redis.eval(
"if redis.call('get', KEYS[1]) == ARGV[1] then redis.call('set', KEYS[1], ARGV[2], 'EX', 60); return 1 else return 0 end",
1, key, record, updated_record,
1,
key,
record,
updated_record,
)
if not replaced:
raise ValueError('Tether connection was replaced')
+28 -11
View File
@@ -87,9 +87,7 @@ class Stream:
def set_timeout(self, timeout):
self.read_timeout = timeout.sock_read if timeout.sock_read and timeout.sock_read > 0 else None
if timeout.total and timeout.total > 0:
self.timeout_handle = asyncio.get_running_loop().call_later(
timeout.total, self.fail, 'Request timed out'
)
self.timeout_handle = asyncio.get_running_loop().call_later(timeout.total, self.fail, 'Request timed out')
async def send(self, operation, data=b'', **fields):
while len(data) > self.credit and not self.closed:
@@ -355,20 +353,39 @@ async def notify(user_id, notification_id, title, body, path):
if not presences:
return
user = await Users.get_user_by_id(user_id)
if not user or user.role == 'pending' or not user.settings or not (user.settings.ui or {}).get('notificationEnabled', False):
if (
not user
or user.role == 'pending'
or not user.settings
or not (user.settings.ui or {}).get('notificationEnabled', False)
):
return
payload = json.dumps({
'id': str(notification_id)[:200], 'user_id': user_id,
'title': str(title)[:256], 'body': str(body)[:4096], 'path': path,
}).encode()
payload = json.dumps(
{
'id': str(notification_id)[:200],
'user_id': user_id,
'title': str(title)[:256],
'body': str(body)[:4096],
'path': path,
}
).encode()
for presence in presences:
row = await AuthSessions.get(presence['session'])
if not row or row.user_id != user_id or row.refresh_token_id is None or row.expires_at <= time.time():
continue
with suppress(Exception):
await asyncio.wait_for(hub.send(presence, {
'type': 'notification', 'stream': str(uuid4()), 'sequence': 0,
}, payload), 10)
await asyncio.wait_for(
hub.send(
presence,
{
'type': 'notification',
'stream': str(uuid4()),
'sequence': 0,
},
payload,
),
10,
)
async def get_terminal_connections(user):
+5 -10
View File
@@ -28,16 +28,11 @@ async def get_tool_permissions_auto_config():
storage_keys = {
key: f'chat.tool_permissions.auto.{"prompt" if key == "prompt" else "decisions." + key}' for key in keys
}
values = await Config.get_many(*storage_keys.values(), *(f'chat.tool_permissions.decisions.{key}' for key in keys))
settings = {}
for key, storage_key in storage_keys.items():
value = values.get(storage_key)
if value is None:
value = Config.default_value(storage_key)
if value is None:
value = values.get(f'chat.tool_permissions.decisions.{key}', '' if key == 'prompt' else None)
settings[key] = value
return settings
values = await Config.get_many(*storage_keys.values())
return {
key: values[storage_key] if values.get(storage_key) is not None else Config.default_value(storage_key)
for key, storage_key in storage_keys.items()
}
def set_tool_approval_prompt(messages, policy=None):
+17 -6
View File
@@ -1378,7 +1378,9 @@ async def get_terminal_cwd(
"""Fetch the current working directory from a terminal server."""
try:
cwd_url = f'{base_url.rstrip("/")}/files/cwd'
async with get_terminal_client(base_url, headers.get('X-User-Id'),
async with get_terminal_client(
base_url,
headers.get('X-User-Id'),
timeout=aiohttp.ClientTimeout(total=5),
trust_env=True,
) as session:
@@ -1406,7 +1408,9 @@ async def get_terminal_system_prompt(
"""
base = base_url.rstrip('/')
try:
async with get_terminal_client(base_url, headers.get('X-User-Id'),
async with get_terminal_client(
base_url,
headers.get('X-User-Id'),
timeout=aiohttp.ClientTimeout(total=3),
trust_env=True,
) as session:
@@ -1563,8 +1567,12 @@ async def get_terminal_tools(
if connection.get('owner_id') == user.id:
url = connection['url']
openapi = await get_tool_server_data(url + '/openapi.json', {'X-User-Id': user.id})
server_data = {'id': terminal_id, 'url': url, 'openapi': openapi,
'specs': convert_openapi_to_tool_payload(openapi)}
server_data = {
'id': terminal_id,
'url': url,
'openapi': openapi,
'specs': convert_openapi_to_tool_payload(openapi),
}
else:
terminal_servers = await get_terminal_servers(request)
server_data = next((server for server in terminal_servers if server.get('id') == terminal_id), None)
@@ -1918,8 +1926,11 @@ async def execute_tool_server(
else:
body_params = params
async with get_terminal_client(url, (headers or {}).get('X-User-Id'),
trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER)
async with get_terminal_client(
url,
(headers or {}).get('X-User-Id'),
trust_env=True,
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER),
) as session:
request_method = getattr(session, http_method.lower())
@@ -82,23 +82,11 @@
const {
DEFAULT_TOOL_SEARCH_DECISIONS_PROMPT = '',
DEFAULT_TOOL_PERMISSIONS_AUTO_PROMPT = '',
DEFAULT_TOOL_PERMISSIONS_DECISIONS_PROMPT: _legacyDefaultPrompt,
TOOL_PERMISSIONS_DECISIONS_API_BASE_URL: _legacyAPI_BASE_URL,
TOOL_PERMISSIONS_DECISIONS_API_KEY: _legacyAPI_KEY,
TOOL_PERMISSIONS_DECISIONS_MODEL: _legacyMODEL,
TOOL_PERMISSIONS_DECISIONS_THRESHOLD: _legacyTHRESHOLD,
TOOL_PERMISSIONS_DECISIONS_PROMPT: _legacyPrompt,
TOOL_SEARCH_EMBEDDING_MODEL = '',
...settings
} = chatConfig as typeof chatConfig & {
DEFAULT_TOOL_SEARCH_DECISIONS_PROMPT?: string;
DEFAULT_TOOL_PERMISSIONS_AUTO_PROMPT?: string;
DEFAULT_TOOL_PERMISSIONS_DECISIONS_PROMPT?: string;
TOOL_PERMISSIONS_DECISIONS_API_BASE_URL?: string;
TOOL_PERMISSIONS_DECISIONS_API_KEY?: string;
TOOL_PERMISSIONS_DECISIONS_MODEL?: string;
TOOL_PERMISSIONS_DECISIONS_THRESHOLD?: number;
TOOL_PERMISSIONS_DECISIONS_PROMPT?: string;
TOOL_SEARCH_EMBEDDING_MODEL?: string;
};
defaultDecisionsPrompt = DEFAULT_TOOL_SEARCH_DECISIONS_PROMPT;