diff --git a/backend/open_webui/retrieval/loaders/main.py b/backend/open_webui/retrieval/loaders/main.py index e49faff051..74221f1aea 100644 --- a/backend/open_webui/retrieval/loaders/main.py +++ b/backend/open_webui/retrieval/loaders/main.py @@ -91,6 +91,7 @@ known_source_ext = [ 'yaml', 'yml', 'toml', + 'svg', ] known_archive_ext = {'docx', 'epub', 'odt', 'pptx', 'xlsx'} diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 5b59fbd7ad..472c74be1d 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -117,6 +117,7 @@ from open_webui.utils.misc import ( get_response_error_detail, get_reasoning_details, get_system_message, + is_raster_image_content_type, is_string_allowed, merge_system_messages, prepend_to_first_user_message_content, @@ -1725,7 +1726,7 @@ def get_images_from_messages(message_list): for file in message.get('files', []): if file.get('type') == 'image': message_images.append(file.get('url')) - elif file.get('content_type', '').startswith('image/'): + elif is_raster_image_content_type(file.get('content_type')): message_images.append(file.get('url')) if message_images: @@ -2440,7 +2441,7 @@ async def process_chat_payload(request, form_data, user, metadata, model): image_files = [ f for f in message.get('files', []) - if f.get('type') == 'image' or (f.get('content_type') or '').startswith('image/') + if f.get('type') == 'image' or is_raster_image_content_type(f.get('content_type')) ] if message.get('role') == 'user' and image_files: text_content = message.get('content', '') diff --git a/backend/open_webui/utils/misc.py b/backend/open_webui/utils/misc.py index 247ab72441..ad959d3bc7 100644 --- a/backend/open_webui/utils/misc.py +++ b/backend/open_webui/utils/misc.py @@ -1223,6 +1223,12 @@ def strict_match_mime_type(supported: list[str] | str, header: str) -> str | Non return None +def is_raster_image_content_type(content_type: str | None) -> bool: + """Return True if the content type is an image that decodes as a bitmap; SVG is XML.""" + base_content_type = (content_type or '').split(';')[0].strip().lower() + return base_content_type.startswith('image/') and base_content_type != 'image/svg+xml' + + def extract_urls(text: str) -> list[str]: # Regex pattern to match URLs url_pattern = re.compile(r'(https?://[^\s]+)', re.IGNORECASE) # Matches http and https URLs diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index a5df89c3a1..b966c3ef7b 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -64,7 +64,8 @@ removeAllDetails, getCodeBlockContents, displayFileHandler, - getUsageTokenCount + getUsageTokenCount, + isRasterImageContentType } from '$lib/utils'; import { AudioQueue } from '$lib/utils/audio'; import { createTemporaryChatId, isTemporaryChatId } from '$lib/utils/chatId'; @@ -2886,7 +2887,7 @@ ..._files.filter( (item) => ['doc', 'text', 'note', 'chat', 'folder', 'collection'].includes(item.type) || - (item.type === 'file' && !(item?.content_type ?? '').startsWith('image/')) + (item.type === 'file' && !isRasterImageContentType(item?.content_type)) ) ); chatFiles = chatFiles.filter( @@ -3323,7 +3324,7 @@ if (model) { const hasImages = createMessagesList(_history, parentId).some((message) => message.files?.some( - (file) => file.type === 'image' || (file?.content_type ?? '').startsWith('image/') + (file) => file.type === 'image' || isRasterImageContentType(file?.content_type) ) ); @@ -3445,7 +3446,7 @@ ...(userMessage?.files ?? []).filter( (item) => ['doc', 'text', 'note', 'chat', 'collection', 'folder'].includes(item.type) || - (item.type === 'file' && !(item?.content_type ?? '').startsWith('image/')) + (item.type === 'file' && !isRasterImageContentType(item?.content_type)) ) ); // Remove duplicates @@ -3496,7 +3497,7 @@ messages = messages .map((message) => { const imageFiles = (message?.files ?? []).filter( - (file) => file.type === 'image' || (file?.content_type ?? '').startsWith('image/') + (file) => file.type === 'image' || isRasterImageContentType(file?.content_type) ); if (message.output && message.role === 'assistant') { diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index d3cf5da88d..02988b0506 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -58,7 +58,8 @@ getUsageTokenCount, getUserPosition, getUserTimezone, - getWeekday + getWeekday, + isRasterImageContentType } from '$lib/utils'; import { uploadFile } from '$lib/apis/files'; import { getCwd, uploadNewFileToTerminal } from '$lib/apis/terminal'; @@ -1122,7 +1123,7 @@ return; } - if (file['type'].startsWith('image/')) { + if (isRasterImageContentType(file['type'])) { if (visionCapableModels.length === 0) { toast.error($i18n.t('Selected model(s) do not support image inputs')); return; @@ -1929,7 +1930,7 @@ dir={$settings?.chatDirection ?? 'auto'} > {#each files as file, fileIdx} - {#if file.type === 'image' || (file?.content_type ?? '').startsWith('image/')} + {#if file.type === 'image' || isRasterImageContentType(file?.content_type)} {@const fileUrl = file.url.startsWith('data') || file.url.startsWith('http') ? file.url diff --git a/src/lib/components/chat/MessageInput/InputMenu/Files.svelte b/src/lib/components/chat/MessageInput/InputMenu/Files.svelte index 4f0f495aaf..b387bc61d7 100644 --- a/src/lib/components/chat/MessageInput/InputMenu/Files.svelte +++ b/src/lib/components/chat/MessageInput/InputMenu/Files.svelte @@ -1,6 +1,8 @@