From 71c4da8c065491a96e41da3c9f0c663e5f759468 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sun, 26 Jul 2026 21:12:14 -0400 Subject: [PATCH] refac --- backend/open_webui/main.py | 26 ++++++++-------- backend/open_webui/utils/chat_id.py | 21 ++++++++++++- .../open_webui/utils/context_compaction.py | 3 +- backend/open_webui/utils/middleware.py | 30 +++++++------------ src/lib/components/chat/Chat.svelte | 9 +++--- src/lib/components/chat/Navbar.svelte | 3 +- src/lib/utils/chatId.ts | 10 +++++++ src/routes/+layout.svelte | 3 +- 8 files changed, 65 insertions(+), 40 deletions(-) create mode 100644 src/lib/utils/chatId.ts diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index a45500a4e5..3ede68c5f7 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -220,7 +220,11 @@ from open_webui.utils.chat import ( from open_webui.utils.chat import ( generate_chat_completion as chat_completion_handler, ) -from open_webui.utils.chat_id import is_saved_chat_id +from open_webui.utils.chat_id import ( + get_temporary_chat_session_id, + is_saved_chat_id, + is_temporary_chat_id, +) from open_webui.utils.chat_variables import ( normalize_chat_variables, ) @@ -1245,9 +1249,7 @@ async def chat_completion( detail=ERROR_MESSAGES.DEFAULT(), ) - if not chat_id.startswith('local:') and not chat_id.startswith( - 'channel:' - ): # temporary/channel chats are not stored + if is_saved_chat_id(chat_id): if is_new_chat: # Build the full history upfront with ALL assistant placeholders user_message = metadata.get('user_message') or {} @@ -1577,9 +1579,7 @@ async def chat_completion( if metadata.get('chat_id') and metadata.get('message_id'): # Update the chat message with the error try: - if not metadata.get('chat_id', '').startswith('local:') and not metadata.get( - 'chat_id', '' - ).startswith('channel:'): + if is_saved_chat_id(metadata.get('chat_id')): await Chats.upsert_message_to_chat_by_id_and_message_id( metadata['chat_id'], metadata['message_id'], @@ -1930,8 +1930,8 @@ async def generate_messages( async def verify_chat_ownership(chat_id: str | None, user) -> None: - """`local:` chats are per-socket and never persisted, so they have no owner to check.""" - if not chat_id or chat_id.startswith('local:'): + """Temporary chats are per-socket and unsaved, so they have no owner to check.""" + if not chat_id or is_temporary_chat_id(chat_id): return # Channel messages need the membership and write-access gate that only /api/chat/completions has. @@ -2004,8 +2004,8 @@ async def list_tasks_endpoint(request: Request, user=Depends(get_admin_user)): @app.get('/api/tasks/chat/{chat_id:path}') async def list_tasks_by_chat_id_endpoint(request: Request, chat_id: str, user=Depends(get_verified_user)): - if chat_id.startswith('local:') or chat_id.startswith('channel:'): - socket_id = chat_id[len('local:') :] + socket_id = get_temporary_chat_session_id(chat_id) + if socket_id: owner_id = get_user_id_from_session_pool(socket_id) if owner_id != user.id and user.role != 'admin': return {'task_ids': []} @@ -2022,8 +2022,8 @@ async def list_tasks_by_chat_id_endpoint(request: Request, chat_id: str, user=De @app.post('/api/tasks/chat/{chat_id:path}/stop') async def stop_tasks_by_chat_id_endpoint(request: Request, chat_id: str, user=Depends(get_verified_user)): - if chat_id.startswith('local:') or chat_id.startswith('channel:'): - socket_id = chat_id[len('local:') :] + socket_id = get_temporary_chat_session_id(chat_id) + if socket_id: owner_id = get_user_id_from_session_pool(socket_id) if owner_id != user.id and user.role != 'admin': raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) diff --git a/backend/open_webui/utils/chat_id.py b/backend/open_webui/utils/chat_id.py index 549a6bc5bd..47b5e61174 100644 --- a/backend/open_webui/utils/chat_id.py +++ b/backend/open_webui/utils/chat_id.py @@ -1,8 +1,27 @@ from typing import Optional -NON_SAVED_CHAT_ID_PREFIXES = ('local:', 'channel:') +TEMPORARY_CHAT_ID_PREFIX = 'temporary:' +LEGACY_TEMPORARY_CHAT_ID_PREFIX = 'local:' # Legacy temporary chat prefix. +CHANNEL_CHAT_ID_PREFIX = 'channel:' + +TEMPORARY_CHAT_ID_PREFIXES = ( + TEMPORARY_CHAT_ID_PREFIX, + LEGACY_TEMPORARY_CHAT_ID_PREFIX, +) +NON_SAVED_CHAT_ID_PREFIXES = (*TEMPORARY_CHAT_ID_PREFIXES, CHANNEL_CHAT_ID_PREFIX) def is_saved_chat_id(chat_id: Optional[str]) -> bool: return bool(chat_id) and not chat_id.startswith(NON_SAVED_CHAT_ID_PREFIXES) + + +def is_temporary_chat_id(chat_id: Optional[str]) -> bool: + return bool(chat_id) and chat_id.startswith(TEMPORARY_CHAT_ID_PREFIXES) + + +def get_temporary_chat_session_id(chat_id: str) -> Optional[str]: + for prefix in TEMPORARY_CHAT_ID_PREFIXES: + if chat_id.startswith(prefix): + return chat_id.removeprefix(prefix) + return None diff --git a/backend/open_webui/utils/context_compaction.py b/backend/open_webui/utils/context_compaction.py index 1fc5296c56..a06f7c3c41 100644 --- a/backend/open_webui/utils/context_compaction.py +++ b/backend/open_webui/utils/context_compaction.py @@ -7,6 +7,7 @@ from typing import Any from fastapi.responses import JSONResponse from open_webui.models.chats import Chats from open_webui.models.config import Config +from open_webui.utils.chat_id import is_saved_chat_id from open_webui.utils.misc import get_content_from_message, get_last_user_message, get_message_list from open_webui.utils.task import ( get_task_model_id, @@ -110,7 +111,7 @@ async def compact_messages_for_request( checkpoint_message_id = ( recent_messages[0].get('id') or metadata.get('user_message_id') or metadata.get('message_id') ) - if chat_id and checkpoint_message_id and not chat_id.startswith(('local:', 'channel:')): + if is_saved_chat_id(chat_id) and checkpoint_message_id: await Chats.upsert_message_to_chat_by_id_and_message_id( chat_id, checkpoint_message_id, diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 1b577015e8..ffa4eda67d 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -1553,7 +1553,7 @@ async def add_file_context(messages: list, chat_id: str, user) -> list: """ Add file URLs to messages for native function calling. """ - if not chat_id or chat_id.startswith('local:') or chat_id.startswith('channel:'): + if not is_saved_chat_id(chat_id): return messages chat = await Chats.get_chat_by_id_and_user_id(chat_id, user.id) @@ -1609,7 +1609,7 @@ async def chat_image_generation_handler(request: Request, form_data: dict, extra if not chat_id or not isinstance(chat_id, str) or not __event_emitter__: return form_data - if chat_id.startswith('local:') or chat_id.startswith('channel:'): + if not is_saved_chat_id(chat_id): message_list = form_data.get('messages', []) else: chat = await Chats.get_chat_by_id_and_user_id(chat_id, user.id) @@ -2270,7 +2270,7 @@ async def process_chat_payload(request, form_data, user, metadata, model): chat_id = metadata.get('chat_id') user_message_id = metadata.get('user_message_id') - if chat_id and user_message_id and not chat_id.startswith('local:') and not chat_id.startswith('channel:'): + if is_saved_chat_id(chat_id) and user_message_id: db_messages = await load_messages_from_db(chat_id, user_message_id) if db_messages: # Continue: frontend sends assistant_message_id when continuing @@ -2317,7 +2317,7 @@ async def process_chat_payload(request, form_data, user, metadata, model): if regeneration_prompt: form_data['messages'].append({'role': 'user', 'content': regeneration_prompt}) - if chat_id and user_message_id and not chat_id.startswith('local:') and not chat_id.startswith('channel:'): + if is_saved_chat_id(chat_id) and user_message_id: if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'): compaction_models = { request.state.model['id']: request.state.model, @@ -2574,7 +2574,7 @@ async def process_chat_payload(request, form_data, user, metadata, model): available_skills = [] view_skill_ids = [] chat = None - if metadata.get('chat_id') and not metadata['chat_id'].startswith(('local:', 'channel:')): + if is_saved_chat_id(metadata.get('chat_id')): chat = await Chats.get_chat_by_id(metadata['chat_id']) if chat and (chat.meta or {}).get('internal') is True and (chat.meta or {}).get('type') == 'note': @@ -3138,11 +3138,7 @@ async def background_tasks_handler(ctx): message = None messages = [] - if ( - 'chat_id' in metadata - and not metadata.get('chat_id', '').startswith('local:') - and not metadata.get('chat_id', '').startswith('channel:') - ): + if is_saved_chat_id(metadata.get('chat_id')): messages_map = await Chats.get_messages_map_by_chat_id(metadata['chat_id']) if not messages_map: # Chat was deleted while the response was streaming — skip background tasks @@ -3225,9 +3221,7 @@ async def background_tasks_handler(ctx): } ) - if not metadata.get('chat_id', '').startswith('local:') and not metadata.get( - 'chat_id', '' - ).startswith('channel:'): + if is_saved_chat_id(metadata.get('chat_id')): await Chats.upsert_message_to_chat_by_id_and_message_id( metadata['chat_id'], metadata['message_id'], @@ -3240,9 +3234,7 @@ async def background_tasks_handler(ctx): except Exception as e: pass - if not metadata.get('chat_id', '').startswith('local:') and not metadata.get('chat_id', '').startswith( - 'channel:' - ): # Only update titles and tags for non-temp chats + if is_saved_chat_id(metadata.get('chat_id')): # Only update titles and tags for saved chats if TASKS.TITLE_GENERATION in tasks: user_message = get_last_user_message(messages) if user_message and len(user_message) > 100: @@ -3378,11 +3370,11 @@ async def outlet_filter_handler(ctx): if not message_id: message_id = output_id('msg') - is_temp_chat = chat_id.startswith('local:') or chat_id.startswith('channel:') + is_unsaved_chat = not is_saved_chat_id(chat_id) try: messages_map = None - if is_temp_chat or not chat_id: + if is_unsaved_chat: form_messages = ctx.get('form_data', {}).get('messages', []) assistant_message = ctx.get('assistant_message', {}) @@ -3658,7 +3650,7 @@ async def non_streaming_chat_response_handler(response, ctx): if ( getattr(request.state, 'internal', False) is not True and chat_id - and not chat_id.startswith(('channel:', 'local:')) + and is_saved_chat_id(chat_id) ): webui_url = await Config.get('webui.url') await publish_event( diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 587aa9ec17..16f30d917a 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -66,6 +66,7 @@ displayFileHandler } from '$lib/utils'; import { AudioQueue } from '$lib/utils/audio'; + import { createTemporaryChatId, isTemporaryChatId } from '$lib/utils/chatId'; import { getOutputText } from './Messages/structuredOutput'; import { @@ -451,7 +452,7 @@ const saveChatVariables = async (values) => { chatVariables = { ...chatVariables, ...values }; - if ($chatId && !$temporaryChatEnabled && !$chatId.startsWith('local:')) { + if ($chatId && !$temporaryChatEnabled && !isTemporaryChatId($chatId)) { const res = await updateChatById(localStorage.token, $chatId, {}, chatVariables).catch( (err) => { console.error('[chat variables save]', err); @@ -2845,7 +2846,7 @@ chatFiles = mergeFiles(chatFiles, createdChat?.chat?.files ?? []); await onSelectEmbeddedChat?.(_chatId); } else if ($temporaryChatEnabled) { - _chatId = `local:${$socket?.id}`; + _chatId = createTemporaryChatId($socket?.id); await chatId.set(_chatId); } await tick(); @@ -3098,7 +3099,7 @@ // Only send terminal_id if the model has terminal capability enabled const terminalEnabled = model.info?.meta?.capabilities?.terminal ?? true; const useChatVariablesFallback = - !_chatId || $temporaryChatEnabled || _chatId.startsWith('local:'); + !_chatId || $temporaryChatEnabled || isTemporaryChatId(_chatId); const res = await generateOpenAIChatCompletion( localStorage.token, @@ -3503,7 +3504,7 @@ selectedFolder.set(null); } else { - _chatId = `local:${$socket?.id}`; // Use socket id for temporary chat + _chatId = createTemporaryChatId($socket?.id); await chatId.set(_chatId); } await tick(); diff --git a/src/lib/components/chat/Navbar.svelte b/src/lib/components/chat/Navbar.svelte index fd8a46d661..2e14be8b13 100644 --- a/src/lib/components/chat/Navbar.svelte +++ b/src/lib/components/chat/Navbar.svelte @@ -35,6 +35,7 @@ import ChatPlus from '../icons/ChatPlus.svelte'; import ChatCheck from '../icons/ChatCheck.svelte'; import Knobs from '../icons/Knobs.svelte'; + import { isTemporaryChatId } from '$lib/utils/chatId'; const i18n = getContext('i18n'); @@ -247,7 +248,7 @@ - {#if $temporaryChatEnabled && ($chatId ?? '').startsWith('local:')} + {#if $temporaryChatEnabled && isTemporaryChatId($chatId)}
{$i18n.t('Temporary Chat')}
diff --git a/src/lib/utils/chatId.ts b/src/lib/utils/chatId.ts new file mode 100644 index 0000000000..4772f73a25 --- /dev/null +++ b/src/lib/utils/chatId.ts @@ -0,0 +1,10 @@ +const TEMPORARY_CHAT_ID_PREFIX = 'temporary:'; +const LEGACY_TEMPORARY_CHAT_ID_PREFIX = 'local:'; // Legacy temporary chat prefix. + +export const createTemporaryChatId = (sessionId: string | undefined) => + `${TEMPORARY_CHAT_ID_PREFIX}${sessionId}`; + +export const isTemporaryChatId = (chatId: string | null | undefined) => + !!chatId && + (chatId.startsWith(TEMPORARY_CHAT_ID_PREFIX) || + chatId.startsWith(LEGACY_TEMPORARY_CHAT_ID_PREFIX)); diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index c7fb59b6fe..4d9f68bc7b 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -54,6 +54,7 @@ import { getSessionUser, updateUserTimezone, userSignOut } from '$lib/apis/auths'; import { getAllTags } from '$lib/apis/chats'; import { chatCompletion } from '$lib/apis/openai'; + import { isTemporaryChatId } from '$lib/utils/chatId'; import { addOpenAIConnection, removeOpenAIConnection, @@ -497,7 +498,7 @@ // Skip events from temporary chats that are not the current chat. // This prevents notifications from being sent to other tabs/devices // for privacy, since temporary chats are not meant to be persisted or visible elsewhere. - const isTemporaryChat = event.chat_id?.startsWith('local:'); + const isTemporaryChat = isTemporaryChatId(event.chat_id); if (isTemporaryChat && event.chat_id !== $chatId) { return; }