mirror of
https://github.com/open-webui/open-webui.git
synced 2026-07-26 16:24:34 +00:00
When an upstream provider rejects a request (e.g. a 400 for a max_tokens value above the model's ceiling), the actionable error message was only published to event sinks, which are invisible unless an event function or webhook is configured. Admins had to query the provider's API directly to diagnose failures (open-webui#27237). Add a single log line in publish_model_provider_request_failed — the chokepoint every upstream failure path (OpenAI-compatible chat, embeddings, responses, token counting, and Ollama) already routes through — recording status, provider, url, model, error code, and the upstream message truncated to 1000 chars. 4xx logs at WARNING, 5xx at ERROR. Client-facing responses are unchanged, so no additional error detail is exposed in the chat. Claude-Session: https://claude.ai/code/session_018VecyiPejru1EVF5yfe2sU Co-authored-by: Claude <noreply@anthropic.com>
1232 lines
50 KiB
Python
1232 lines
50 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import inspect
|
|
import logging
|
|
import time
|
|
import uuid
|
|
from types import SimpleNamespace
|
|
from typing import Any
|
|
|
|
from open_webui.env import ENABLE_PLUGINS, VERSION
|
|
from open_webui.models.config import Config
|
|
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
from open_webui.retrieval.web.utils import validate_url
|
|
from open_webui.utils.webhook import post_webhook
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
MAX_STRING_LENGTH = 1000
|
|
EVENT_WEBHOOKS_CONFIG_KEY = 'events.webhooks'
|
|
LEGACY_WEBHOOK_CONFIG_KEY = 'webhook_url'
|
|
DEFAULT_WEBHOOK_ID = 'default'
|
|
|
|
|
|
class EventDefinition(BaseModel):
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
name: str
|
|
description: str | None = None
|
|
message: str | None = None
|
|
|
|
@model_validator(mode='after')
|
|
def defaults(self) -> 'EventDefinition':
|
|
title = self.name.replace('.', ' ').replace('_', ' ').title()
|
|
if self.description is None:
|
|
object.__setattr__(self, 'description', f'{title}.')
|
|
if self.message is None:
|
|
object.__setattr__(self, 'message', title)
|
|
return self
|
|
|
|
|
|
class EventDefinitions(BaseModel):
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
SYSTEM_STARTUP_STARTED: EventDefinition = EventDefinition(
|
|
name='system.startup.started', description='Application startup began.', message='Startup started'
|
|
)
|
|
SYSTEM_STARTUP_COMPLETED: EventDefinition = EventDefinition(
|
|
name='system.startup.completed', description='Application startup completed.', message='Startup completed'
|
|
)
|
|
SYSTEM_SHUTDOWN_STARTED: EventDefinition = EventDefinition(
|
|
name='system.shutdown.started', description='Application shutdown began.', message='Shutdown started'
|
|
)
|
|
SYSTEM_SHUTDOWN_COMPLETED: EventDefinition = EventDefinition(
|
|
name='system.shutdown.completed', description='Application shutdown completed.', message='Shutdown completed'
|
|
)
|
|
CONFIG_IMPORTED: EventDefinition = EventDefinition(
|
|
name='config.imported', description='Configuration was imported.', message='Config imported'
|
|
)
|
|
CONFIG_UPDATED: EventDefinition = EventDefinition(
|
|
name='config.updated', description='Configuration was updated.', message='Config updated'
|
|
)
|
|
CONFIG_WEBHOOK_UPDATED: EventDefinition = EventDefinition(
|
|
name='config.webhook.updated',
|
|
description='Event webhook configuration was updated.',
|
|
message='Webhook configuration updated',
|
|
)
|
|
CONFIG_CONNECTIONS_UPDATED: EventDefinition = EventDefinition(
|
|
name='config.connections.updated',
|
|
description='Connection configuration was updated.',
|
|
message='Config Connections updated',
|
|
)
|
|
CONFIG_TOOL_SERVERS_UPDATED: EventDefinition = EventDefinition(
|
|
name='config.tool_servers.updated',
|
|
description='Tool server configuration was updated.',
|
|
message='Config Tool Servers updated',
|
|
)
|
|
CONFIG_TERMINAL_SERVERS_UPDATED: EventDefinition = EventDefinition(
|
|
name='config.terminal_servers.updated',
|
|
description='Terminal server configuration was updated.',
|
|
message='Config Terminal Servers updated',
|
|
)
|
|
CONFIG_CODE_EXECUTION_UPDATED: EventDefinition = EventDefinition(
|
|
name='config.code_execution.updated',
|
|
description='Code execution configuration was updated.',
|
|
message='Config Code Execution updated',
|
|
)
|
|
CONFIG_MODELS_UPDATED: EventDefinition = EventDefinition(
|
|
name='config.models.updated', description='Model configuration was updated.', message='Config Models updated'
|
|
)
|
|
CONFIG_BANNERS_UPDATED: EventDefinition = EventDefinition(
|
|
name='config.banners.updated', description='Banner configuration was updated.', message='Config Banners updated'
|
|
)
|
|
CONFIG_SUGGESTIONS_UPDATED: EventDefinition = EventDefinition(
|
|
name='config.suggestions.updated',
|
|
description='Suggestion configuration was updated.',
|
|
message='Config Suggestions updated',
|
|
)
|
|
AUTH_SIGNUP: EventDefinition = EventDefinition(
|
|
name='auth.signup', description='A user account was created through signup.', message='User signed up'
|
|
)
|
|
AUTH_LOGIN: EventDefinition = EventDefinition(
|
|
name='auth.login', description='A user successfully logged in.', message='User logged in'
|
|
)
|
|
AUTH_LOGOUT: EventDefinition = EventDefinition(
|
|
name='auth.logout', description='A user logged out.', message='User logged out'
|
|
)
|
|
AUTH_PASSWORD_CHANGED: EventDefinition = EventDefinition(
|
|
name='auth.password_changed', description='A user password was changed.', message='Password changed'
|
|
)
|
|
AUTH_API_KEY_CREATED: EventDefinition = EventDefinition(
|
|
name='auth.api_key.created', description='A user API key was created.', message='API key created'
|
|
)
|
|
AUTH_API_KEY_DELETED: EventDefinition = EventDefinition(
|
|
name='auth.api_key.deleted', description='A user API key was deleted.', message='API key deleted'
|
|
)
|
|
AUTH_OAUTH_SESSION_DELETED: EventDefinition = EventDefinition(
|
|
name='auth.oauth_session.deleted', description='An OAuth session was deleted.', message='OAuth session deleted'
|
|
)
|
|
USER_CREATED: EventDefinition = EventDefinition(
|
|
name='user.created', description='A user account was created.', message='User created'
|
|
)
|
|
USER_UPDATED: EventDefinition = EventDefinition(
|
|
name='user.updated', description='A user account was updated.', message='User updated'
|
|
)
|
|
USER_DELETED: EventDefinition = EventDefinition(
|
|
name='user.deleted', description='A user account was deleted.', message='User deleted'
|
|
)
|
|
USER_ROLE_UPDATED: EventDefinition = EventDefinition(
|
|
name='user.role_updated', description='A user role was updated.', message='User role updated'
|
|
)
|
|
USER_STATUS_UPDATED: EventDefinition = EventDefinition(
|
|
name='user.status_updated', description='A user status was updated.', message='User status updated'
|
|
)
|
|
USER_SETTINGS_UPDATED: EventDefinition = EventDefinition(
|
|
name='user.settings_updated', description='A user settings object was updated.', message='User settings updated'
|
|
)
|
|
USER_PROFILE_UPDATED: EventDefinition = EventDefinition(
|
|
name='user.profile_updated', description='A user profile was updated.', message='User profile updated'
|
|
)
|
|
USER_PERMISSIONS_UPDATED: EventDefinition = EventDefinition(
|
|
name='user.permissions_updated',
|
|
description='A user permissions object was updated.',
|
|
message='User permissions updated',
|
|
)
|
|
GROUP_CREATED: EventDefinition = EventDefinition(
|
|
name='group.created', description='A group was created.', message='Group created'
|
|
)
|
|
GROUP_UPDATED: EventDefinition = EventDefinition(
|
|
name='group.updated', description='A group was updated.', message='Group updated'
|
|
)
|
|
GROUP_DELETED: EventDefinition = EventDefinition(
|
|
name='group.deleted', description='A group was deleted.', message='Group deleted'
|
|
)
|
|
GROUP_MEMBER_ADDED: EventDefinition = EventDefinition(
|
|
name='group.member_added', description='A user was added to a group.', message='Group member added'
|
|
)
|
|
GROUP_MEMBER_REMOVED: EventDefinition = EventDefinition(
|
|
name='group.member_removed', description='A user was removed from a group.', message='Group member removed'
|
|
)
|
|
CHAT_CREATED: EventDefinition = EventDefinition(
|
|
name='chat.created', description='A chat was created.', message='Chat created'
|
|
)
|
|
CHAT_FINISHED: EventDefinition = EventDefinition(
|
|
name='chat.finished', description='A chat response finished.', message='Chat finished'
|
|
)
|
|
CHAT_FAILED: EventDefinition = EventDefinition(
|
|
name='chat.failed', description='A chat response failed.', message='Chat failed'
|
|
)
|
|
CHAT_IMPORTED: EventDefinition = EventDefinition(
|
|
name='chat.imported', description='A chat was imported.', message='Chat imported'
|
|
)
|
|
CHAT_UPDATED: EventDefinition = EventDefinition(
|
|
name='chat.updated', description='A chat was updated.', message='Chat updated'
|
|
)
|
|
CHAT_DELETED: EventDefinition = EventDefinition(
|
|
name='chat.deleted', description='A chat was deleted.', message='Chat deleted'
|
|
)
|
|
CHAT_DELETED_ALL: EventDefinition = EventDefinition(
|
|
name='chat.deleted_all', description='All chats for a scope were deleted.', message='Chat deleted all'
|
|
)
|
|
CHAT_COMPACTED: EventDefinition = EventDefinition(
|
|
name='chat.compacted', description='A chat was compacted.', message='Chat compacted'
|
|
)
|
|
CHAT_PINNED: EventDefinition = EventDefinition(
|
|
name='chat.pinned', description='A chat was pinned.', message='Chat pinned'
|
|
)
|
|
CHAT_UNPINNED: EventDefinition = EventDefinition(
|
|
name='chat.unpinned', description='A chat was unpinned.', message='Chat unpinned'
|
|
)
|
|
CHAT_CLONED: EventDefinition = EventDefinition(
|
|
name='chat.cloned', description='A chat was cloned.', message='Chat cloned'
|
|
)
|
|
CHAT_ARCHIVED: EventDefinition = EventDefinition(
|
|
name='chat.archived', description='A chat was archived.', message='Chat archived'
|
|
)
|
|
CHAT_UNARCHIVED: EventDefinition = EventDefinition(
|
|
name='chat.unarchived', description='A chat was unarchived.', message='Chat unarchived'
|
|
)
|
|
CHAT_SHARED: EventDefinition = EventDefinition(
|
|
name='chat.shared', description='A chat was shared.', message='Chat shared'
|
|
)
|
|
CHAT_UNSHARED: EventDefinition = EventDefinition(
|
|
name='chat.unshared', description='A chat was unshared.', message='Chat unshared'
|
|
)
|
|
CHAT_FOLDER_UPDATED: EventDefinition = EventDefinition(
|
|
name='chat.folder_updated', description='A chat folder assignment was updated.', message='Chat folder updated'
|
|
)
|
|
CHAT_TAG_ADDED: EventDefinition = EventDefinition(
|
|
name='chat.tag_added', description='A tag was added to a chat.', message='Chat tag added'
|
|
)
|
|
CHAT_TAG_REMOVED: EventDefinition = EventDefinition(
|
|
name='chat.tag_removed', description='A tag was removed from a chat.', message='Chat tag removed'
|
|
)
|
|
MESSAGE_CREATED: EventDefinition = EventDefinition(
|
|
name='message.created', description='A message was created.', message='Message created'
|
|
)
|
|
MESSAGE_UPDATED: EventDefinition = EventDefinition(
|
|
name='message.updated', description='A message was updated.', message='Message updated'
|
|
)
|
|
MESSAGE_DELETED: EventDefinition = EventDefinition(
|
|
name='message.deleted', description='A message was deleted.', message='Message deleted'
|
|
)
|
|
MESSAGE_EVENT_RECEIVED: EventDefinition = EventDefinition(
|
|
name='message.event_received',
|
|
description='A message-level event was received.',
|
|
message='Message event received',
|
|
)
|
|
MESSAGE_REACTION_ADDED: EventDefinition = EventDefinition(
|
|
name='message.reaction_added',
|
|
description='A reaction was added to a message.',
|
|
message='Message reaction added',
|
|
)
|
|
MESSAGE_REACTION_REMOVED: EventDefinition = EventDefinition(
|
|
name='message.reaction_removed',
|
|
description='A reaction was removed from a message.',
|
|
message='Message reaction removed',
|
|
)
|
|
MESSAGE_PINNED: EventDefinition = EventDefinition(
|
|
name='message.pinned', description='A message was pinned.', message='Message pinned'
|
|
)
|
|
MESSAGE_UNPINNED: EventDefinition = EventDefinition(
|
|
name='message.unpinned', description='A message was unpinned.', message='Message unpinned'
|
|
)
|
|
CHANNEL_CREATED: EventDefinition = EventDefinition(
|
|
name='channel.created', description='A channel was created.', message='Channel created'
|
|
)
|
|
CHANNEL_UPDATED: EventDefinition = EventDefinition(
|
|
name='channel.updated', description='A channel was updated.', message='Channel updated'
|
|
)
|
|
CHANNEL_DELETED: EventDefinition = EventDefinition(
|
|
name='channel.deleted', description='A channel was deleted.', message='Channel deleted'
|
|
)
|
|
CHANNEL_MEMBER_ADDED: EventDefinition = EventDefinition(
|
|
name='channel.member_added', description='A member was added to a channel.', message='Channel member added'
|
|
)
|
|
CHANNEL_MEMBER_REMOVED: EventDefinition = EventDefinition(
|
|
name='channel.member_removed',
|
|
description='A member was removed from a channel.',
|
|
message='Channel member removed',
|
|
)
|
|
CHANNEL_MEMBER_ACTIVE_UPDATED: EventDefinition = EventDefinition(
|
|
name='channel.member_active_updated',
|
|
description='A channel member active state was updated.',
|
|
message='Channel member active updated',
|
|
)
|
|
CHANNEL_MESSAGE: EventDefinition = EventDefinition(
|
|
name='channel.message',
|
|
description='A channel message was posted.',
|
|
message='Channel message',
|
|
)
|
|
CHANNEL_WEBHOOK_CREATED: EventDefinition = EventDefinition(
|
|
name='channel.webhook.created',
|
|
description='A channel incoming webhook was created.',
|
|
message='Channel Webhook created',
|
|
)
|
|
CHANNEL_WEBHOOK_UPDATED: EventDefinition = EventDefinition(
|
|
name='channel.webhook.updated',
|
|
description='A channel incoming webhook was updated.',
|
|
message='Channel Webhook updated',
|
|
)
|
|
CHANNEL_WEBHOOK_DELETED: EventDefinition = EventDefinition(
|
|
name='channel.webhook.deleted',
|
|
description='A channel incoming webhook was deleted.',
|
|
message='Channel Webhook deleted',
|
|
)
|
|
FILE_UPLOADED: EventDefinition = EventDefinition(
|
|
name='file.uploaded', description='A file was uploaded.', message='File uploaded'
|
|
)
|
|
FILE_CONTENT_UPDATED: EventDefinition = EventDefinition(
|
|
name='file.content_updated', description='File content was updated.', message='File content updated'
|
|
)
|
|
FILE_RENAMED: EventDefinition = EventDefinition(
|
|
name='file.renamed', description='A file was renamed.', message='File renamed'
|
|
)
|
|
FILE_DELETED: EventDefinition = EventDefinition(
|
|
name='file.deleted', description='A file was deleted.', message='File deleted'
|
|
)
|
|
FILE_DELETED_ALL: EventDefinition = EventDefinition(
|
|
name='file.deleted_all', description='All files for a scope were deleted.', message='File deleted all'
|
|
)
|
|
FOLDER_CREATED: EventDefinition = EventDefinition(
|
|
name='folder.created', description='A folder was created.', message='Folder created'
|
|
)
|
|
FOLDER_UPDATED: EventDefinition = EventDefinition(
|
|
name='folder.updated', description='A folder was updated.', message='Folder updated'
|
|
)
|
|
FOLDER_PARENT_UPDATED: EventDefinition = EventDefinition(
|
|
name='folder.parent_updated', description='A folder parent was updated.', message='Folder parent updated'
|
|
)
|
|
FOLDER_ACCESS_UPDATED: EventDefinition = EventDefinition(
|
|
name='folder.access_updated', description='Folder access was updated.', message='Folder access updated'
|
|
)
|
|
FOLDER_DELETED: EventDefinition = EventDefinition(
|
|
name='folder.deleted', description='A folder was deleted.', message='Folder deleted'
|
|
)
|
|
NOTE_CREATED: EventDefinition = EventDefinition(
|
|
name='note.created', description='A note was created.', message='Note created'
|
|
)
|
|
NOTE_UPDATED: EventDefinition = EventDefinition(
|
|
name='note.updated', description='A note was updated.', message='Note updated'
|
|
)
|
|
NOTE_ACCESS_UPDATED: EventDefinition = EventDefinition(
|
|
name='note.access_updated', description='Note access was updated.', message='Note access updated'
|
|
)
|
|
NOTE_PINNED: EventDefinition = EventDefinition(
|
|
name='note.pinned', description='A note was pinned.', message='Note pinned'
|
|
)
|
|
NOTE_UNPINNED: EventDefinition = EventDefinition(
|
|
name='note.unpinned', description='A note was unpinned.', message='Note unpinned'
|
|
)
|
|
NOTE_DELETED: EventDefinition = EventDefinition(
|
|
name='note.deleted', description='A note was deleted.', message='Note deleted'
|
|
)
|
|
MEMORY_CREATED: EventDefinition = EventDefinition(
|
|
name='memory.created', description='A memory was created.', message='Memory created'
|
|
)
|
|
MEMORY_UPDATED: EventDefinition = EventDefinition(
|
|
name='memory.updated', description='A memory was updated.', message='Memory updated'
|
|
)
|
|
MEMORY_DELETED: EventDefinition = EventDefinition(
|
|
name='memory.deleted', description='A memory was deleted.', message='Memory deleted'
|
|
)
|
|
MEMORY_RESET: EventDefinition = EventDefinition(
|
|
name='memory.reset', description='A memory was reset.', message='Memory reset'
|
|
)
|
|
KNOWLEDGE_CREATED: EventDefinition = EventDefinition(
|
|
name='knowledge.created', description='A knowledge was created.', message='Knowledge created'
|
|
)
|
|
KNOWLEDGE_UPDATED: EventDefinition = EventDefinition(
|
|
name='knowledge.updated', description='A knowledge was updated.', message='Knowledge updated'
|
|
)
|
|
KNOWLEDGE_DELETED: EventDefinition = EventDefinition(
|
|
name='knowledge.deleted', description='A knowledge was deleted.', message='Knowledge deleted'
|
|
)
|
|
KNOWLEDGE_RESET: EventDefinition = EventDefinition(
|
|
name='knowledge.reset', description='A knowledge was reset.', message='Knowledge reset'
|
|
)
|
|
KNOWLEDGE_REINDEXED: EventDefinition = EventDefinition(
|
|
name='knowledge.reindexed', description='A knowledge was reindexed.', message='Knowledge reindexed'
|
|
)
|
|
KNOWLEDGE_ACCESS_UPDATED: EventDefinition = EventDefinition(
|
|
name='knowledge.access_updated', description='Knowledge access was updated.', message='Knowledge access updated'
|
|
)
|
|
KNOWLEDGE_FILE_ADDED: EventDefinition = EventDefinition(
|
|
name='knowledge.file.added', description='A file was added to a knowledge base.', message='Knowledge File added'
|
|
)
|
|
KNOWLEDGE_FILE_UPDATED: EventDefinition = EventDefinition(
|
|
name='knowledge.file.updated', description='A knowledge file was updated.', message='Knowledge File updated'
|
|
)
|
|
KNOWLEDGE_FILE_REMOVED: EventDefinition = EventDefinition(
|
|
name='knowledge.file.removed',
|
|
description='A file was removed from a knowledge base.',
|
|
message='Knowledge File removed',
|
|
)
|
|
KNOWLEDGE_FILE_MOVED: EventDefinition = EventDefinition(
|
|
name='knowledge.file.moved', description='A knowledge file was moved.', message='Knowledge File moved'
|
|
)
|
|
KNOWLEDGE_DIRECTORY_CREATED: EventDefinition = EventDefinition(
|
|
name='knowledge.directory.created',
|
|
description='A knowledge directory was created.',
|
|
message='Knowledge Directory created',
|
|
)
|
|
KNOWLEDGE_DIRECTORY_UPDATED: EventDefinition = EventDefinition(
|
|
name='knowledge.directory.updated',
|
|
description='A knowledge directory was updated.',
|
|
message='Knowledge Directory updated',
|
|
)
|
|
KNOWLEDGE_DIRECTORY_DELETED: EventDefinition = EventDefinition(
|
|
name='knowledge.directory.deleted',
|
|
description='A knowledge directory was deleted.',
|
|
message='Knowledge Directory deleted',
|
|
)
|
|
KNOWLEDGE_EXTERNAL_CONNECTION_CREATED: EventDefinition = EventDefinition(
|
|
name='knowledge.external_connection.created',
|
|
description='A knowledge external connection was created.',
|
|
message='Knowledge External Connection created',
|
|
)
|
|
KNOWLEDGE_EXTERNAL_CONNECTION_UPDATED: EventDefinition = EventDefinition(
|
|
name='knowledge.external_connection.updated',
|
|
description='A knowledge external connection was updated.',
|
|
message='Knowledge External Connection updated',
|
|
)
|
|
KNOWLEDGE_EXTERNAL_CONNECTION_DELETED: EventDefinition = EventDefinition(
|
|
name='knowledge.external_connection.deleted',
|
|
description='A knowledge external connection was deleted.',
|
|
message='Knowledge External Connection deleted',
|
|
)
|
|
RETRIEVAL_CONTENT_PROCESSED: EventDefinition = EventDefinition(
|
|
name='retrieval.content.processed',
|
|
description='Retrieval content was processed.',
|
|
message='Retrieval Content processed',
|
|
)
|
|
RETRIEVAL_COLLECTION_DELETED: EventDefinition = EventDefinition(
|
|
name='retrieval.collection.deleted',
|
|
description='A retrieval collection was deleted.',
|
|
message='Retrieval Collection deleted',
|
|
)
|
|
RETRIEVAL_VECTOR_DB_RESET: EventDefinition = EventDefinition(
|
|
name='retrieval.vector_db.reset',
|
|
description='The retrieval vector database was reset.',
|
|
message='Retrieval Vector Db reset',
|
|
)
|
|
RETRIEVAL_UPLOADS_RESET: EventDefinition = EventDefinition(
|
|
name='retrieval.uploads.reset', description='Retrieval uploads were reset.', message='Retrieval Uploads reset'
|
|
)
|
|
MODEL_CREATED: EventDefinition = EventDefinition(
|
|
name='model.created', description='A model was created.', message='Model created'
|
|
)
|
|
MODEL_IMPORTED: EventDefinition = EventDefinition(
|
|
name='model.imported', description='A model was imported.', message='Model imported'
|
|
)
|
|
MODEL_SYNCED: EventDefinition = EventDefinition(
|
|
name='model.synced', description='A model was synced.', message='Model synced'
|
|
)
|
|
MODEL_UPDATED: EventDefinition = EventDefinition(
|
|
name='model.updated', description='A model was updated.', message='Model updated'
|
|
)
|
|
MODEL_DELETED: EventDefinition = EventDefinition(
|
|
name='model.deleted', description='A model was deleted.', message='Model deleted'
|
|
)
|
|
MODEL_ENABLED: EventDefinition = EventDefinition(
|
|
name='model.enabled', description='A model was enabled.', message='Model enabled'
|
|
)
|
|
MODEL_DISABLED: EventDefinition = EventDefinition(
|
|
name='model.disabled', description='A model was disabled.', message='Model disabled'
|
|
)
|
|
MODEL_ACCESS_UPDATED: EventDefinition = EventDefinition(
|
|
name='model.access_updated', description='Model access was updated.', message='Model access updated'
|
|
)
|
|
MODEL_PROVIDER_CONFIG_UPDATED: EventDefinition = EventDefinition(
|
|
name='model.provider_config.updated',
|
|
description='Model provider configuration was updated.',
|
|
message='Model Provider Config updated',
|
|
)
|
|
MODEL_PROVIDER_REQUEST_FAILED: EventDefinition = EventDefinition(
|
|
name='model.provider_request.failed',
|
|
description='A model provider request failed.',
|
|
message='Model provider request failed',
|
|
)
|
|
MODEL_PROVIDER_MODEL_CREATED: EventDefinition = EventDefinition(
|
|
name='model.provider_model.created',
|
|
description='A provider model was created.',
|
|
message='Provider model created',
|
|
)
|
|
MODEL_PROVIDER_MODEL_DELETED: EventDefinition = EventDefinition(
|
|
name='model.provider_model.deleted',
|
|
description='A provider model was deleted.',
|
|
message='Provider model deleted',
|
|
)
|
|
FUNCTION_CREATED: EventDefinition = EventDefinition(
|
|
name='function.created', description='A function was created.', message='Function created'
|
|
)
|
|
FUNCTION_UPDATED: EventDefinition = EventDefinition(
|
|
name='function.updated', description='A function was updated.', message='Function updated'
|
|
)
|
|
FUNCTION_DELETED: EventDefinition = EventDefinition(
|
|
name='function.deleted', description='A function was deleted.', message='Function deleted'
|
|
)
|
|
FUNCTION_ENABLED: EventDefinition = EventDefinition(
|
|
name='function.enabled', description='A function was enabled.', message='Function enabled'
|
|
)
|
|
FUNCTION_DISABLED: EventDefinition = EventDefinition(
|
|
name='function.disabled', description='A function was disabled.', message='Function disabled'
|
|
)
|
|
FUNCTION_VALVES_UPDATED: EventDefinition = EventDefinition(
|
|
name='function.valves_updated', description='Function valves were updated.', message='Function valves updated'
|
|
)
|
|
TOOL_CREATED: EventDefinition = EventDefinition(
|
|
name='tool.created', description='A tool was created.', message='Tool created'
|
|
)
|
|
TOOL_UPDATED: EventDefinition = EventDefinition(
|
|
name='tool.updated', description='A tool was updated.', message='Tool updated'
|
|
)
|
|
TOOL_DELETED: EventDefinition = EventDefinition(
|
|
name='tool.deleted', description='A tool was deleted.', message='Tool deleted'
|
|
)
|
|
TOOL_ACCESS_UPDATED: EventDefinition = EventDefinition(
|
|
name='tool.access_updated', description='Tool access was updated.', message='Tool access updated'
|
|
)
|
|
TOOL_VALVES_UPDATED: EventDefinition = EventDefinition(
|
|
name='tool.valves_updated', description='Tool valves were updated.', message='Tool valves updated'
|
|
)
|
|
SKILL_CREATED: EventDefinition = EventDefinition(
|
|
name='skill.created', description='A skill was created.', message='Skill created'
|
|
)
|
|
SKILL_UPDATED: EventDefinition = EventDefinition(
|
|
name='skill.updated', description='A skill was updated.', message='Skill updated'
|
|
)
|
|
SKILL_DELETED: EventDefinition = EventDefinition(
|
|
name='skill.deleted', description='A skill was deleted.', message='Skill deleted'
|
|
)
|
|
SKILL_ENABLED: EventDefinition = EventDefinition(
|
|
name='skill.enabled', description='A skill was enabled.', message='Skill enabled'
|
|
)
|
|
SKILL_DISABLED: EventDefinition = EventDefinition(
|
|
name='skill.disabled', description='A skill was disabled.', message='Skill disabled'
|
|
)
|
|
PROMPT_CREATED: EventDefinition = EventDefinition(
|
|
name='prompt.created', description='A prompt was created.', message='Prompt created'
|
|
)
|
|
PROMPT_UPDATED: EventDefinition = EventDefinition(
|
|
name='prompt.updated', description='A prompt was updated.', message='Prompt updated'
|
|
)
|
|
PROMPT_DELETED: EventDefinition = EventDefinition(
|
|
name='prompt.deleted', description='A prompt was deleted.', message='Prompt deleted'
|
|
)
|
|
PROMPT_ENABLED: EventDefinition = EventDefinition(
|
|
name='prompt.enabled', description='A prompt was enabled.', message='Prompt enabled'
|
|
)
|
|
PROMPT_DISABLED: EventDefinition = EventDefinition(
|
|
name='prompt.disabled', description='A prompt was disabled.', message='Prompt disabled'
|
|
)
|
|
PROMPT_VERSION_UPDATED: EventDefinition = EventDefinition(
|
|
name='prompt.version_updated', description='A prompt version was updated.', message='Prompt version updated'
|
|
)
|
|
PROMPT_ACCESS_UPDATED: EventDefinition = EventDefinition(
|
|
name='prompt.access_updated', description='Prompt access was updated.', message='Prompt access updated'
|
|
)
|
|
PIPELINE_UPLOADED: EventDefinition = EventDefinition(
|
|
name='pipeline.uploaded', description='A pipeline was uploaded.', message='Pipeline uploaded'
|
|
)
|
|
PIPELINE_ADDED: EventDefinition = EventDefinition(
|
|
name='pipeline.added', description='A pipeline was added.', message='Pipeline added'
|
|
)
|
|
PIPELINE_DELETED: EventDefinition = EventDefinition(
|
|
name='pipeline.deleted', description='A pipeline was deleted.', message='Pipeline deleted'
|
|
)
|
|
PIPELINE_VALVES_UPDATED: EventDefinition = EventDefinition(
|
|
name='pipeline.valves_updated', description='Pipeline valves were updated.', message='Pipeline valves updated'
|
|
)
|
|
CALENDAR_CREATED: EventDefinition = EventDefinition(
|
|
name='calendar.created', description='A calendar was created.', message='Calendar created'
|
|
)
|
|
CALENDAR_UPDATED: EventDefinition = EventDefinition(
|
|
name='calendar.updated', description='A calendar was updated.', message='Calendar updated'
|
|
)
|
|
CALENDAR_DELETED: EventDefinition = EventDefinition(
|
|
name='calendar.deleted', description='A calendar was deleted.', message='Calendar deleted'
|
|
)
|
|
CALENDAR_DEFAULT_UPDATED: EventDefinition = EventDefinition(
|
|
name='calendar.default_updated',
|
|
description='The default calendar was updated.',
|
|
message='Calendar default updated',
|
|
)
|
|
CALENDAR_EVENT_CREATED: EventDefinition = EventDefinition(
|
|
name='calendar.event.created', description='A calendar event was created.', message='Calendar Event created'
|
|
)
|
|
CALENDAR_EVENT_UPDATED: EventDefinition = EventDefinition(
|
|
name='calendar.event.updated', description='A calendar event was updated.', message='Calendar Event updated'
|
|
)
|
|
CALENDAR_EVENT_DELETED: EventDefinition = EventDefinition(
|
|
name='calendar.event.deleted', description='A calendar event was deleted.', message='Calendar Event deleted'
|
|
)
|
|
CALENDAR_EVENT_RSVP_UPDATED: EventDefinition = EventDefinition(
|
|
name='calendar.event.rsvp_updated',
|
|
description='A calendar event RSVP was updated.',
|
|
message='Calendar Event rsvp updated',
|
|
)
|
|
CALENDAR_ALERT: EventDefinition = EventDefinition(
|
|
name='calendar.alert',
|
|
description='A calendar event alert was triggered.',
|
|
message='Calendar alert',
|
|
)
|
|
AUTOMATION_CREATED: EventDefinition = EventDefinition(
|
|
name='automation.created', description='An automation was created.', message='Automation created'
|
|
)
|
|
AUTOMATION_UPDATED: EventDefinition = EventDefinition(
|
|
name='automation.updated', description='An automation was updated.', message='Automation updated'
|
|
)
|
|
AUTOMATION_ENABLED: EventDefinition = EventDefinition(
|
|
name='automation.enabled', description='An automation was enabled.', message='Automation enabled'
|
|
)
|
|
AUTOMATION_DISABLED: EventDefinition = EventDefinition(
|
|
name='automation.disabled', description='An automation was disabled.', message='Automation disabled'
|
|
)
|
|
AUTOMATION_DELETED: EventDefinition = EventDefinition(
|
|
name='automation.deleted', description='An automation was deleted.', message='Automation deleted'
|
|
)
|
|
AUTOMATION_RUN_STARTED: EventDefinition = EventDefinition(
|
|
name='automation.run_started', description='An automation run started.', message='Automation run started'
|
|
)
|
|
AUTOMATION_RUN_COMPLETED: EventDefinition = EventDefinition(
|
|
name='automation.run_completed', description='An automation run completed.', message='Automation run completed'
|
|
)
|
|
AUTOMATION_RUN_FAILED: EventDefinition = EventDefinition(
|
|
name='automation.run_failed', description='An automation run failed.', message='Automation run failed'
|
|
)
|
|
FEEDBACK_CREATED: EventDefinition = EventDefinition(
|
|
name='feedback.created', description='A feedback was created.', message='Feedback created'
|
|
)
|
|
FEEDBACK_UPDATED: EventDefinition = EventDefinition(
|
|
name='feedback.updated', description='A feedback was updated.', message='Feedback updated'
|
|
)
|
|
FEEDBACK_DELETED: EventDefinition = EventDefinition(
|
|
name='feedback.deleted', description='A feedback was deleted.', message='Feedback deleted'
|
|
)
|
|
FEEDBACK_DELETED_ALL: EventDefinition = EventDefinition(
|
|
name='feedback.deleted_all', description='All feedback for a scope was deleted.', message='Feedback deleted all'
|
|
)
|
|
IMAGE_GENERATED: EventDefinition = EventDefinition(
|
|
name='image.generated', description='An image was generated.', message='Image generated'
|
|
)
|
|
IMAGE_EDITED: EventDefinition = EventDefinition(
|
|
name='image.edited', description='An image was edited.', message='Image edited'
|
|
)
|
|
AUDIO_SPEECH_REQUESTED: EventDefinition = EventDefinition(
|
|
name='audio.speech_requested', description='Speech generation was requested.', message='Speech requested'
|
|
)
|
|
AUDIO_TRANSCRIPTION_REQUESTED: EventDefinition = EventDefinition(
|
|
name='audio.transcription_requested',
|
|
description='Audio transcription was requested.',
|
|
message='Transcription requested',
|
|
)
|
|
TERMINAL_SESSION_OPENED: EventDefinition = EventDefinition(
|
|
name='terminal.session.opened', description='A terminal session was opened.', message='Terminal Session opened'
|
|
)
|
|
TERMINAL_SESSION_CLOSED: EventDefinition = EventDefinition(
|
|
name='terminal.session.closed', description='A terminal session was closed.', message='Terminal Session closed'
|
|
)
|
|
NOTIFICATION_TEST: EventDefinition = EventDefinition(
|
|
name='notification.test', description='A notification target test was sent.', message='Notification test'
|
|
)
|
|
NOTIFICATION_MANUAL: EventDefinition = EventDefinition(
|
|
name='notification.manual', description='A manual notification was sent.', message='Notification sent'
|
|
)
|
|
|
|
|
|
EVENTS = EventDefinitions()
|
|
EVENT_DEFINITIONS = tuple(getattr(EVENTS, field_name) for field_name in EventDefinitions.model_fields)
|
|
EVENT_DEFINITIONS_BY_NAME = {definition.name: definition for definition in EVENT_DEFINITIONS}
|
|
EVENT_CATALOG = tuple(definition.name for definition in EVENT_DEFINITIONS)
|
|
EVENT_CATALOG_SET = set(EVENT_CATALOG)
|
|
NOTIFICATION_EVENTS = (
|
|
EVENTS.CHAT_FINISHED.name,
|
|
EVENTS.CHAT_FAILED.name,
|
|
EVENTS.CHANNEL_MESSAGE.name,
|
|
EVENTS.CALENDAR_ALERT.name,
|
|
)
|
|
|
|
|
|
def get_event_catalog() -> list[dict[str, str]]:
|
|
return [
|
|
{
|
|
'event': definition.name,
|
|
'description': definition.description,
|
|
'message': definition.message,
|
|
}
|
|
for definition in EVENT_DEFINITIONS
|
|
]
|
|
|
|
|
|
SENSITIVE_KEYS = {
|
|
'password',
|
|
'hashed_password',
|
|
'token',
|
|
'access_token',
|
|
'refresh_token',
|
|
'id_token',
|
|
'api_key',
|
|
'secret',
|
|
'key',
|
|
'authorization',
|
|
'cookie',
|
|
'webhook_token',
|
|
}
|
|
|
|
SAFE_ACTOR_FIELDS = ('id', 'name', 'email', 'role', 'created_at', 'updated_at')
|
|
|
|
|
|
def normalize_event_webhook(webhook: dict[str, Any], *, create: bool = False) -> dict[str, Any]:
|
|
now = int(time.time())
|
|
webhook_id = str(webhook.get('id') or uuid.uuid4())
|
|
url = str(webhook.get('url') or '').strip()
|
|
|
|
events = [str(event).strip() for event in (webhook.get('events') or ['*']) if str(event).strip()]
|
|
events = events or ['*']
|
|
for event_filter in events:
|
|
if event_filter == '*':
|
|
continue
|
|
if event_filter.endswith('.*'):
|
|
prefix = event_filter[:-2]
|
|
if prefix and any(event.startswith(f'{prefix}.') for event in EVENT_CATALOG):
|
|
continue
|
|
raise ValueError(f'Invalid event pattern: {event_filter}')
|
|
if event_filter not in EVENT_CATALOG_SET:
|
|
raise ValueError(f'Invalid event: {event_filter}')
|
|
|
|
targets = normalize_event_targets(webhook.get('targets'))
|
|
|
|
return {
|
|
'id': webhook_id,
|
|
'name': str(webhook.get('name') or ('Default webhook' if webhook_id == DEFAULT_WEBHOOK_ID else 'Webhook')),
|
|
'url': url,
|
|
'enabled': bool(webhook.get('enabled', True)),
|
|
'events': events,
|
|
'targets': targets,
|
|
'created_at': int(webhook.get('created_at') or now),
|
|
'updated_at': now if create or webhook.get('updated_at') is None else int(webhook.get('updated_at') or now),
|
|
}
|
|
|
|
|
|
def normalize_event_targets(targets: Any) -> list[dict[str, str]] | None:
|
|
if targets is None:
|
|
return None
|
|
if not isinstance(targets, list):
|
|
raise ValueError('Invalid targets')
|
|
|
|
normalized = []
|
|
seen = set()
|
|
for target in targets:
|
|
if not isinstance(target, dict):
|
|
raise ValueError('Invalid target')
|
|
|
|
target_type = str(target.get('type') or '').strip()
|
|
target_id = str(target.get('id') or '').strip()
|
|
if target_type not in {'user', 'group'} or not target_id:
|
|
raise ValueError('Invalid target')
|
|
|
|
key = (target_type, target_id)
|
|
if key in seen:
|
|
continue
|
|
|
|
normalized.append({'type': target_type, 'id': target_id})
|
|
seen.add(key)
|
|
|
|
return normalized
|
|
|
|
|
|
def event_filter_matches(webhook: dict[str, Any], event_name: str) -> bool:
|
|
if not webhook.get('enabled', True):
|
|
return False
|
|
|
|
for event_filter in webhook.get('events') or ['*']:
|
|
if event_filter == '*':
|
|
return True
|
|
if event_filter.endswith('.*') and event_name.startswith(f'{event_filter[:-2]}.'):
|
|
return True
|
|
if event_name == event_filter:
|
|
return True
|
|
return False
|
|
|
|
|
|
def event_user_ids(event: 'Event') -> set[str]:
|
|
user_ids = set()
|
|
actor = event.actor or {}
|
|
subject = event.subject or {}
|
|
data = event.data or {}
|
|
|
|
if actor.get('id'):
|
|
user_ids.add(str(actor['id']))
|
|
|
|
if subject.get('type') == 'user' and subject.get('id'):
|
|
user_ids.add(str(subject['id']))
|
|
|
|
if data.get('user_id'):
|
|
user_ids.add(str(data['user_id']))
|
|
|
|
for user_id in data.get('user_ids') or []:
|
|
if user_id:
|
|
user_ids.add(str(user_id))
|
|
|
|
return user_ids
|
|
|
|
|
|
async def event_target_matches(
|
|
targets: list[dict[str, str]] | None,
|
|
user_ids: set[str],
|
|
user_group_ids: dict[str, set[str]] | None = None,
|
|
) -> bool:
|
|
if targets is None:
|
|
return True
|
|
if not targets:
|
|
return not user_ids
|
|
if not user_ids:
|
|
return False
|
|
|
|
target_user_ids = {target['id'] for target in targets if target.get('type') == 'user'}
|
|
if target_user_ids.intersection(user_ids):
|
|
return True
|
|
|
|
target_group_ids = {target['id'] for target in targets if target.get('type') == 'group'}
|
|
if not target_group_ids:
|
|
return False
|
|
|
|
if user_group_ids is None:
|
|
from open_webui.models.groups import Groups
|
|
|
|
groups_by_user = await Groups.get_groups_by_member_ids(list(user_ids))
|
|
user_group_ids = {user_id: {group.id for group in groups} for user_id, groups in groups_by_user.items()}
|
|
|
|
return any(group_ids.intersection(target_group_ids) for group_ids in user_group_ids.values())
|
|
|
|
|
|
async def event_webhook_matches(webhook: dict[str, Any], event: 'Event') -> bool:
|
|
if not event_filter_matches(webhook, event.event):
|
|
return False
|
|
|
|
return await event_target_matches(webhook.get('targets'), event_user_ids(event))
|
|
|
|
|
|
async def get_event_webhooks() -> list[dict[str, Any]]:
|
|
webhooks = await Config.get(EVENT_WEBHOOKS_CONFIG_KEY, []) or []
|
|
if not isinstance(webhooks, list):
|
|
return []
|
|
|
|
normalized = []
|
|
for webhook in webhooks:
|
|
if not isinstance(webhook, dict):
|
|
continue
|
|
try:
|
|
normalized.append(normalize_event_webhook(webhook))
|
|
except ValueError:
|
|
log.exception('Invalid event webhook config skipped')
|
|
return normalized
|
|
|
|
|
|
async def migrate_legacy_webhook_config() -> list[dict[str, Any]]:
|
|
webhooks = await get_event_webhooks()
|
|
if any(webhook.get('id') == DEFAULT_WEBHOOK_ID for webhook in webhooks):
|
|
return webhooks
|
|
|
|
now = int(time.time())
|
|
legacy_url = await Config.get(LEGACY_WEBHOOK_CONFIG_KEY) or ''
|
|
if not legacy_url:
|
|
return webhooks
|
|
|
|
webhooks = [
|
|
{
|
|
'id': DEFAULT_WEBHOOK_ID,
|
|
'name': 'Default webhook',
|
|
'url': legacy_url,
|
|
'enabled': True,
|
|
'events': ['*'],
|
|
'targets': None,
|
|
'created_at': now,
|
|
'updated_at': now,
|
|
},
|
|
*webhooks,
|
|
]
|
|
await Config.upsert({EVENT_WEBHOOKS_CONFIG_KEY: webhooks})
|
|
return webhooks
|
|
|
|
|
|
async def upsert_event_webhook(webhook: dict[str, Any]) -> dict[str, Any]:
|
|
webhooks = await get_event_webhooks()
|
|
url = str(webhook.get('url') or '').strip()
|
|
if url:
|
|
validate_url(url)
|
|
|
|
normalized = normalize_event_webhook(webhook, create=True)
|
|
replaced = False
|
|
next_webhooks = []
|
|
|
|
for existing in webhooks:
|
|
if existing.get('id') == normalized['id']:
|
|
next_webhooks.append(
|
|
{
|
|
**existing,
|
|
**normalized,
|
|
'created_at': existing.get('created_at') or normalized['created_at'],
|
|
}
|
|
)
|
|
replaced = True
|
|
else:
|
|
next_webhooks.append(existing)
|
|
|
|
if not replaced:
|
|
next_webhooks.append(normalized)
|
|
|
|
await Config.upsert({EVENT_WEBHOOKS_CONFIG_KEY: next_webhooks})
|
|
return next(webhook for webhook in next_webhooks if webhook.get('id') == normalized['id'])
|
|
|
|
|
|
async def delete_event_webhook(webhook_id: str) -> bool:
|
|
webhooks = await get_event_webhooks()
|
|
next_webhooks = [webhook for webhook in webhooks if webhook.get('id') != webhook_id]
|
|
if len(next_webhooks) == len(webhooks):
|
|
return False
|
|
|
|
values = {EVENT_WEBHOOKS_CONFIG_KEY: next_webhooks}
|
|
if webhook_id == DEFAULT_WEBHOOK_ID:
|
|
values[LEGACY_WEBHOOK_CONFIG_KEY] = ''
|
|
|
|
await Config.upsert(values)
|
|
return True
|
|
|
|
|
|
class Event(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
schema_: str = Field(alias='schema')
|
|
id: str
|
|
event: str
|
|
resource: str
|
|
operation: str
|
|
created_at: int
|
|
instance_id: str | None
|
|
version: str
|
|
source: str
|
|
actor: dict[str, Any] | None = None
|
|
subject: dict[str, Any] | None = None
|
|
data: dict[str, Any] = Field(default_factory=dict)
|
|
message: str | None = None
|
|
|
|
def model_dump(self, *args, **kwargs) -> dict[str, Any]:
|
|
kwargs.setdefault('by_alias', True)
|
|
return super().model_dump(*args, **kwargs)
|
|
|
|
|
|
def _sensitive(key: Any) -> bool:
|
|
normalized = str(key).lower().replace('-', '_')
|
|
return (
|
|
normalized in SENSITIVE_KEYS
|
|
or normalized.endswith('_token')
|
|
or normalized.endswith('_secret')
|
|
or normalized.endswith('_api_key')
|
|
or normalized.endswith('_key')
|
|
)
|
|
|
|
|
|
def _sanitize(value: Any) -> Any:
|
|
if hasattr(value, 'model_dump'):
|
|
value = value.model_dump()
|
|
|
|
if isinstance(value, dict):
|
|
return {key: _sanitize(item) for key, item in value.items() if not _sensitive(key)}
|
|
|
|
if isinstance(value, (list, tuple, set)):
|
|
return [_sanitize(item) for item in value]
|
|
|
|
if isinstance(value, str) and len(value) > MAX_STRING_LENGTH:
|
|
return f'{value[:MAX_STRING_LENGTH]}...'
|
|
|
|
return value
|
|
|
|
|
|
def _actor(actor: Any | None) -> dict[str, Any] | None:
|
|
actor = _sanitize(actor)
|
|
if not actor:
|
|
return None
|
|
|
|
get = actor.get if isinstance(actor, dict) else lambda key: getattr(actor, key, None)
|
|
data = {field: get(field) for field in SAFE_ACTOR_FIELDS if get(field) is not None}
|
|
if not data:
|
|
return None
|
|
|
|
data['type'] = get('type') or 'user'
|
|
return data
|
|
|
|
|
|
def event_name(event: EventDefinition | str) -> str:
|
|
name = event.name if isinstance(event, EventDefinition) else str(event)
|
|
if name not in EVENT_CATALOG_SET:
|
|
raise ValueError(f'Unknown event: {name}')
|
|
return name
|
|
|
|
|
|
def build_event(
|
|
request_or_app: Any,
|
|
event: EventDefinition | str,
|
|
*,
|
|
actor: Any | None = None,
|
|
subject_id: Any | None = None,
|
|
subject_type: str | None = None,
|
|
source: str = 'api',
|
|
data: dict | None = None,
|
|
message: str | None = None,
|
|
) -> Event:
|
|
event_name_value = event_name(event)
|
|
app = getattr(request_or_app, 'app', request_or_app)
|
|
parts = event_name_value.split('.')
|
|
resource = '.'.join(parts[:-1])
|
|
instance_id = getattr(getattr(app, 'state', None), 'instance_id', None)
|
|
subject = (
|
|
{'type': subject_type or resource, 'id': subject_id}
|
|
if subject_id is not None or subject_type is not None
|
|
else None
|
|
)
|
|
|
|
return Event(
|
|
schema=VERSION,
|
|
id=str(uuid.uuid4()),
|
|
event=event_name_value,
|
|
resource=resource,
|
|
operation=parts[-1],
|
|
created_at=int(time.time()),
|
|
instance_id=instance_id,
|
|
version=VERSION,
|
|
source=source,
|
|
actor=_actor(actor),
|
|
subject=_sanitize(subject) if subject else None,
|
|
data=_sanitize(data or {}),
|
|
message=message,
|
|
)
|
|
|
|
|
|
async def dispatch_webhook_event(app: Any, event: Event) -> None:
|
|
name = getattr(getattr(app, 'state', None), 'WEBUI_NAME', 'Open WebUI')
|
|
subject = event.subject or {}
|
|
subject_id = subject.get('id')
|
|
definition = EVENT_DEFINITIONS_BY_NAME.get(event.event)
|
|
message = event.message or (definition.message if definition else event.event)
|
|
if subject_id:
|
|
message = f'{message} ({subject_id})'
|
|
|
|
for webhook in await get_event_webhooks():
|
|
if not webhook.get('url') or not await event_webhook_matches(webhook, event):
|
|
continue
|
|
|
|
try:
|
|
await post_webhook(
|
|
name,
|
|
webhook['url'],
|
|
message,
|
|
event.model_dump(),
|
|
description=definition.description if definition else None,
|
|
)
|
|
except Exception:
|
|
log.exception('Event webhook failed for %s', webhook.get('id'))
|
|
|
|
|
|
def schedule_webhook_dispatch(app: Any, event: Event) -> None:
|
|
try:
|
|
asyncio.create_task(dispatch_webhook_event(app, event))
|
|
except RuntimeError:
|
|
log.exception('Event webhook delivery could not be scheduled for %s', event.event)
|
|
|
|
|
|
class WebhookEventSink:
|
|
async def handle_event(self, app: Any, event: Event, request: Any | None = None) -> None:
|
|
schedule_webhook_dispatch(app, event)
|
|
|
|
|
|
def schedule_notification_dispatch(app: Any, event: Event) -> None:
|
|
try:
|
|
from open_webui.utils.notifications import dispatch_notification_event
|
|
|
|
asyncio.create_task(dispatch_notification_event(app, event))
|
|
except RuntimeError:
|
|
log.exception('Notification delivery could not be scheduled for %s', event.event)
|
|
|
|
|
|
class NotificationEventSink:
|
|
async def handle_event(self, app: Any, event: Event, request: Any | None = None) -> None:
|
|
if event.event in NOTIFICATION_EVENTS:
|
|
schedule_notification_dispatch(app, event)
|
|
|
|
|
|
async def dispatch_event_functions(app: Any, event: Event, request: Any | None = None) -> None:
|
|
if not ENABLE_PLUGINS:
|
|
return
|
|
|
|
from open_webui.models.functions import Functions
|
|
from open_webui.utils.plugin import get_function_module_from_cache
|
|
|
|
context = request or SimpleNamespace(app=app)
|
|
event_payload = event.model_dump()
|
|
|
|
try:
|
|
event_functions = await Functions.get_functions_by_type('event', active_only=True)
|
|
except Exception:
|
|
log.exception('Event functions could not be loaded for %s', event.event)
|
|
return
|
|
|
|
for function in event_functions:
|
|
try:
|
|
function_module, _, _ = await get_function_module_from_cache(context, function.id, function=function)
|
|
handler = getattr(function_module, 'event', None)
|
|
if not handler:
|
|
continue
|
|
|
|
if hasattr(function_module, 'valves') and hasattr(function_module, 'Valves'):
|
|
valves = await Functions.get_function_valves_by_id(function.id)
|
|
function_module.valves = function_module.Valves(**(valves if valves else {}))
|
|
|
|
sig = inspect.signature(handler)
|
|
accepts_kwargs = any(param.kind == inspect.Parameter.VAR_KEYWORD for param in sig.parameters.values())
|
|
extra_params = {
|
|
'event': event_payload,
|
|
'__id__': function.id,
|
|
'__event__': event,
|
|
'__event_id__': event.id,
|
|
'__event_name__': event.event,
|
|
'__app__': app,
|
|
'__request__': request,
|
|
}
|
|
params = {key: value for key, value in extra_params.items() if accepts_kwargs or key in sig.parameters}
|
|
|
|
if inspect.iscoroutinefunction(handler):
|
|
await handler(**params)
|
|
else:
|
|
handler(**params)
|
|
except Exception:
|
|
log.exception('Event function failed for %s', function.id)
|
|
|
|
|
|
def schedule_event_function_dispatch(app: Any, event: Event, request: Any | None = None) -> None:
|
|
try:
|
|
asyncio.create_task(dispatch_event_functions(app, event, request))
|
|
except RuntimeError:
|
|
log.exception('Event functions could not be scheduled for %s', event.event)
|
|
|
|
|
|
class EventFunctionSink:
|
|
async def handle_event(self, app: Any, event: Event, request: Any | None = None) -> None:
|
|
schedule_event_function_dispatch(app, event, request)
|
|
|
|
|
|
EVENT_SINKS = [EventFunctionSink(), WebhookEventSink(), NotificationEventSink()]
|
|
|
|
|
|
async def publish_event(
|
|
request_or_app: Any,
|
|
event: EventDefinition | str,
|
|
*,
|
|
actor: Any | None = None,
|
|
subject_id: Any | None = None,
|
|
subject_type: str | None = None,
|
|
source: str = 'api',
|
|
data: dict | None = None,
|
|
message: str | None = None,
|
|
) -> None:
|
|
app = getattr(request_or_app, 'app', request_or_app)
|
|
request = request_or_app if hasattr(request_or_app, 'app') else None
|
|
event_payload = build_event(
|
|
request_or_app,
|
|
event,
|
|
actor=actor,
|
|
subject_id=subject_id,
|
|
subject_type=subject_type,
|
|
source=source,
|
|
data=data,
|
|
message=message,
|
|
)
|
|
|
|
for sink in EVENT_SINKS:
|
|
try:
|
|
await sink.handle_event(app, event_payload, request=request)
|
|
except Exception:
|
|
log.exception('Event sink failed for %s', event_payload.event)
|
|
|
|
|
|
async def publish_model_provider_request_failed(
|
|
request_or_app: Any,
|
|
*,
|
|
actor: Any | None,
|
|
provider: str,
|
|
base_url: str,
|
|
status: int,
|
|
requested_model: str | None = None,
|
|
api_key: str | None = None,
|
|
upstream_error: Any = None,
|
|
) -> None:
|
|
error = upstream_error.get('error') if isinstance(upstream_error, dict) else upstream_error
|
|
error_code = None
|
|
if isinstance(error, dict):
|
|
error_code = error.get('code') or error.get('type') or error.get('error_code')
|
|
error = error.get('message') or error.get('detail') or error
|
|
|
|
error_text = str(error or '')
|
|
marker = f'{error_code or ""} {error_text}'.lower()
|
|
error_type = (
|
|
'model_not_found'
|
|
if status == 404
|
|
and any(value in marker for value in ('model_not_found', 'model not found', 'does not exist', 'no such model'))
|
|
else 'authentication_failed'
|
|
if status in (401, 403)
|
|
else 'rate_limited'
|
|
if status == 429
|
|
else 'server_failed'
|
|
if status >= 500
|
|
else 'upstream_error'
|
|
)
|
|
|
|
# Server-log only; the upstream error body is otherwise invisible to admins
|
|
# (event sinks require an event function or webhook to be configured).
|
|
log.log(
|
|
logging.ERROR if status >= 500 else logging.WARNING,
|
|
'Upstream %s request failed: HTTP %d (%s) url=%s model=%s code=%s message=%s',
|
|
provider,
|
|
status,
|
|
error_type,
|
|
base_url,
|
|
requested_model or '-',
|
|
error_code or '-',
|
|
error_text[:MAX_STRING_LENGTH] or '-',
|
|
)
|
|
|
|
data = {
|
|
'error_type': error_type,
|
|
'status': status,
|
|
'provider': provider,
|
|
'base_url': base_url,
|
|
}
|
|
if requested_model:
|
|
data['requested_model'] = requested_model
|
|
if api_key:
|
|
data['api_key_suffix'] = f'...{api_key[-4:]}'
|
|
if error_code:
|
|
data['upstream_error_code'] = error_code
|
|
if error:
|
|
data['upstream_message'] = error
|
|
|
|
await publish_event(
|
|
request_or_app,
|
|
EVENTS.MODEL_PROVIDER_REQUEST_FAILED,
|
|
actor=actor,
|
|
subject_id=requested_model,
|
|
subject_type='model',
|
|
data=data,
|
|
)
|