diff --git a/.env.example b/.env.example index df63e08e0e9..55bf5d386f3 100644 --- a/.env.example +++ b/.env.example @@ -13,6 +13,9 @@ OPENAI_API_KEY='' # CORS_ALLOW_ORIGIN='http://localhost:5173;http://localhost:8080' CORS_ALLOW_ORIGIN='*' +# Set to false to keep memory tools enabled without adding memory context to the system context. +ENABLE_MEMORY_SYSTEM_CONTEXT=true + # For production you should set this to match the proxy configuration (127.0.0.1) FORWARDED_ALLOW_IPS='*' diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index ef274fa9184..f06ce4bdb99 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -github: tjbck +github: open-webui diff --git a/CHANGELOG.md b/CHANGELOG.md index 1685ad71dcd..c94a202490e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,45 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.10.2] - 2026-07-01 + +### Added + +- 💭 **Streamed reasoning display.** Models that emit thinking or reasoning now show that content as it streams, and it renders correctly in the chat overview and in exported conversations. [Commit](https://github.com/open-webui/open-webui/commit/0b75445ff9a42e37640c034812d0de9b84039e60), [Commit](https://github.com/open-webui/open-webui/commit/af1c0eee89810fa4c36e3eb7e4eba6de685bd7ca), [Commit](https://github.com/open-webui/open-webui/commit/4b08d65597e5b634b7191b0bd6d28feeafcc2a48), [Commit](https://github.com/open-webui/open-webui/commit/fa2abe4cb6a085a4c9045bf8d8ff4b6beffdef6a) +- 🗂️ **Folder uploads to knowledge bases.** Dragging a folder into a knowledge base, or syncing one, now recreates its subfolder structure instead of flattening everything into loose files. [#26130](https://github.com/open-webui/open-webui/issues/26130), [Commit](https://github.com/open-webui/open-webui/commit/2ed8934f5b2bc8a11c74ef2f34cdb62ef024809e) +- 🧠 **Memory system context toggle.** Administrators can now keep memory tools available while choosing not to add stored memories to the system context, using the new 'Memory System Context' toggle in admin settings. [Commit](https://github.com/open-webui/open-webui/commit/4067e357b2ff9e2fb59866d24656e832908fb6fe) +- 🧹 **Tidier automatic memories.** Automatically saved memories now focus on enduring details like preferences and goals and skip one-off things like meals, routine events, or passing mood unless you ask to remember them. [Commit](https://github.com/open-webui/open-webui/commit/80af65c24adac5140a39a2b5687a3b669b86719f) +- 🎙️ **Speech-to-text request format.** OpenAI-compatible speech-to-text can now send audio as either a multipart upload or base64 JSON, selectable in admin audio settings. [Commit](https://github.com/open-webui/open-webui/commit/989c6c13f5d4c5cc255aec77aea14725104d9cb3) +- 🧰 **API configs via environment.** Administrators can now set per-connection Ollama and OpenAI API configurations through the "OLLAMA_API_CONFIGS" and "OPENAI_API_CONFIGS" environment variables. [Commit](https://github.com/open-webui/open-webui/commit/19d8f03bd2c64013b510f2a0eeb3513d452814a3) +- 📡 **Provider failure events.** Failed Ollama and OpenAI-compatible provider requests now emit a structured event describing the error type, provider, and status, giving administrators clearer visibility into upstream failures. [Commit](https://github.com/open-webui/open-webui/commit/4351c78b1e45bb0c5824f9d9eb911b395e343646) +- 🏟️ **Arena models via environment.** Administrators can now define evaluation arena models through the "EVALUATION_ARENA_MODELS" environment variable. [#26174](https://github.com/open-webui/open-webui/issues/26174) +- ♿ **Clearer high-contrast sidebar selection.** With high-contrast mode enabled, the currently selected chat in the sidebar now stands out with stronger colors, making it easier to tell which chat is active. [#26469](https://github.com/open-webui/open-webui/issues/26469), [Commit](https://github.com/open-webui/open-webui/commit/52ee5cb1b3aa8a8ad40b23d72d82db7c3522dbf8) +- 🔄 **General improvements.** Various improvements were implemented across the application to enhance performance, stability, and security. +- 🌐 **Translation updates.** Translations for Thai, Portuguese (Brazil), Catalan, and Spanish were enhanced and expanded. + +### Fixed + +- 🛡️ **Security Advisory**: This release includes security and access-control fixes. We recommend updating production deployments at your earliest convenience. Not all security fixes in this version may be enumerated in the fixed section — some may be withheld for a short time to give administrators time to upgrade. [Advisories](https://github.com/open-webui/open-webui/security) +- 🔐 **Fewer unexpected logouts.** A single request returning an authorization error no longer signs you out while your session is still valid, since the app now confirms the session status before redirecting to login. [Commit](https://github.com/open-webui/open-webui/commit/56ee875e21cb3b137a5aa1eca6eb3c0731b14045) +- 🔒 **Web search domain filtering.** Domain allow and block rules for web search results are now matched against the host, closing a gap where some URLs could slip past the filter. [Commit](https://github.com/open-webui/open-webui/commit/688bda09fbe26619613aa487beda8059a5fd55ef) +- 🕵️ **Image prompt log privacy.** Image generation workflows are no longer written to server logs at the default log level, keeping user-authored prompt content out of operator-visible logs. [#26400](https://github.com/open-webui/open-webui/issues/26400), [Commit](https://github.com/open-webui/open-webui/commit/64b92ff08a09b1bf338d5d95b7bf6f9db0d7e857) +- 🗄️ **Safer database upgrades.** Upgrading an existing SQLite database no longer crashes during the user-table migration or corrupts saved user settings, resolving failures that could block startup or break login after an upgrade. [#26403](https://github.com/open-webui/open-webui/issues/26403), [Commit](https://github.com/open-webui/open-webui/commit/c416c6cad69cc4ce44faacdad87cc65862625fb4) +- ⚙️ **Saving settings as a non-admin.** Non-admin users can once again save their interface settings, such as the default model and theme, which previously failed with a server error while the interface incorrectly reported success. [#26627](https://github.com/open-webui/open-webui/issues/26627), [Commit](https://github.com/open-webui/open-webui/commit/9866a02863c12c466100aed832f0006225759493) +- 🕒 **Sidebar chat timestamps.** Chats in the sidebar now show when they were last active instead of when they were created, so the time label matches their position in the list and refreshes after each new message. [#26454](https://github.com/open-webui/open-webui/pull/26454), [#26451](https://github.com/open-webui/open-webui/issues/26451) +- 🎯 **Default model after refresh.** Your selected model is no longer cleared when you reload the page, as model selection now waits for the model list to finish loading. [Commit](https://github.com/open-webui/open-webui/commit/b6d4baeb7ea9bc83e5486ca98b3cae07d3869aa7) +- ⏳ **Tool dialogs no longer hang.** Dismissing a tool or function input dialog by clicking outside it now cancels the pending request instead of leaving the chat spinning indefinitely. [#26417](https://github.com/open-webui/open-webui/issues/26417), [Commit](https://github.com/open-webui/open-webui/commit/0016266c0652757e32aa44f2e1fb7311ac08db51) +- 🐍 **Reliable code execution loading.** Running Python code in chat now loads its runtime reliably, fixing sandbox startup failures that broke code execution and the Pyodide file viewer in recent releases. [#26625](https://github.com/open-webui/open-webui/pull/26625), [#26390](https://github.com/open-webui/open-webui/issues/26390) +- 🤖 **Models with null capabilities.** Chatting with a model whose capabilities are unset no longer fails with an error when the memory feature or automations are involved. [#26412](https://github.com/open-webui/open-webui/issues/26412), [Commit](https://github.com/open-webui/open-webui/commit/650b81792582268345073ce3757ac14356189d75), [Commit](https://github.com/open-webui/open-webui/commit/0016266c0652757e32aa44f2e1fb7311ac08db51) +- 🔎 **Searchable responses.** Chat search again finds assistant messages whose text is stored as structured output, which were previously skipped. [#26405](https://github.com/open-webui/open-webui/pull/26405) +- 🔔 **Chat notification previews.** Background chat completion notifications and toasts now show a clean response preview instead of appearing blank for messages stored as structured output. [Commit](https://github.com/open-webui/open-webui/commit/c98d8ecaccc7284573b050e88b3857c4aeba3860) +- 💾 **Banner and config startup.** Setting configuration such as "WEBUI_BANNERS" no longer causes a startup failure, since admin configuration values are now stored correctly regardless of their data type. [#26431](https://github.com/open-webui/open-webui/issues/26431), [Commit](https://github.com/open-webui/open-webui/commit/ab22fe64bdd10ba86845dcd46dfd7f619b22366f) +- 📑 **RAG Template visibility.** The RAG Template editor now stays visible in admin document settings even when Bypass Embedding and Retrieval is enabled, since the template still applies to document content in that mode. [#26126](https://github.com/open-webui/open-webui/issues/26126), [Commit](https://github.com/open-webui/open-webui/commit/8fe480250f649824441a7b4d725a502683134713) +- 🧬 **Editing derived models.** Editing a workspace model no longer clears its base model, including when that base is a preset or the model itself. [Commit](https://github.com/open-webui/open-webui/commit/092b5857bbda01be53619e26f42b1fa9b64a2b44), [Commit](https://github.com/open-webui/open-webui/commit/54f31c630afa75e9c81acaa048e1bd901254ba92) + +### Changed + +- ⚠️ **Database Migrations**: This release includes database schema changes; we strongly recommend backing up your database and all associated data before upgrading in production environments. If you are running a multi-worker, multi-server, or load-balanced deployment, all instances must be updated simultaneously, rolling updates are not supported and will cause application failures due to schema incompatibility. + ## [0.10.1] - 2026-06-29 ### Fixed diff --git a/README.md b/README.md index 66c49eb3281..bc504ce6411 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ![GitHub top language](https://img.shields.io/github/languages/top/open-webui/open-webui) ![GitHub last commit](https://img.shields.io/github/last-commit/open-webui/open-webui?color=red) [![Discord](https://img.shields.io/badge/Discord-Open_WebUI-blue?logo=discord&logoColor=white)](https://discord.gg/5rJgQTnV4s) -[![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/tjbck) +[![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/open-webui) ![Open WebUI Banner](./banner.png) diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 7cadde33fef..f89738b0db3 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -290,6 +290,16 @@ def reachable(host: str, port: int) -> bool: OLLAMA_BASE_URLS = OLLAMA_BASE_URLS OLLAMA_API_CONFIGS = {} +_ollama_api_configs = os.getenv('OLLAMA_API_CONFIGS', '') +if _ollama_api_configs: + try: + parsed = json.loads(_ollama_api_configs) + if isinstance(parsed, dict): + OLLAMA_API_CONFIGS = parsed + else: + log.warning('OLLAMA_API_CONFIGS must be a JSON object, ignoring') + except (json.JSONDecodeError, TypeError): + log.warning('OLLAMA_API_CONFIGS is not valid JSON, ignoring') #################################### # OPENAI_API @@ -327,6 +337,16 @@ def reachable(host: str, port: int) -> bool: OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS OPENAI_API_CONFIGS = {} +_openai_api_configs = os.getenv('OPENAI_API_CONFIGS', '') +if _openai_api_configs: + try: + parsed = json.loads(_openai_api_configs) + if isinstance(parsed, dict): + OPENAI_API_CONFIGS = parsed + else: + log.warning('OPENAI_API_CONFIGS must be a JSON object, ignoring') + except (json.JSONDecodeError, TypeError): + log.warning('OPENAI_API_CONFIGS is not valid JSON, ignoring') # Get the actual OpenAI API key based on the base URL OPENAI_API_KEY = '' @@ -394,6 +414,7 @@ def reachable(host: str, port: int) -> bool: ENABLE_CODE_INTERPRETER = os.getenv('ENABLE_CODE_INTERPRETER', 'True').lower() == 'true' ENABLE_MEMORIES = os.getenv('ENABLE_MEMORIES', 'True').lower() == 'true' +ENABLE_MEMORY_SYSTEM_CONTEXT = os.getenv('ENABLE_MEMORY_SYSTEM_CONTEXT', 'True').lower() == 'true' ENABLE_MEMORY_BACKGROUND_REVIEW = os.getenv('ENABLE_MEMORY_BACKGROUND_REVIEW', 'False').lower() == 'true' MEMORIES_REVIEW_INTERVAL_TURNS = int(os.getenv('MEMORIES_REVIEW_INTERVAL_TURNS', '10')) MEMORIES_USER_CHAR_LIMIT = int(os.getenv('MEMORIES_USER_CHAR_LIMIT', '2000')) @@ -1511,6 +1532,8 @@ def reachable(host: str, port: int) -> bool: AUDIO_STT_OPENAI_API_KEY = os.getenv('AUDIO_STT_OPENAI_API_KEY', OPENAI_API_KEY) +AUDIO_STT_OPENAI_API_REQUEST_FORMAT = os.getenv('AUDIO_STT_OPENAI_API_REQUEST_FORMAT', 'multipart') + AUDIO_STT_ENGINE = os.getenv('AUDIO_STT_ENGINE', '') AUDIO_STT_MODEL = os.getenv('AUDIO_STT_MODEL', '') @@ -2741,6 +2764,7 @@ def feishu_oauth_register(oauth: OAuth): 'code_execution.jupyter.timeout': CODE_EXECUTION_JUPYTER_TIMEOUT, 'code_interpreter.enable': ENABLE_CODE_INTERPRETER, 'memories.enable': ENABLE_MEMORIES, + 'memories.system_context.enable': ENABLE_MEMORY_SYSTEM_CONTEXT, 'memories.background_review.enable': ENABLE_MEMORY_BACKGROUND_REVIEW, 'memories.review_interval_turns': MEMORIES_REVIEW_INTERVAL_TURNS, 'memories.user_char_limit': MEMORIES_USER_CHAR_LIMIT, @@ -2944,6 +2968,7 @@ def feishu_oauth_register(oauth: OAuth): 'audio.stt.deepgram.api_key': DEEPGRAM_API_KEY, 'audio.stt.openai.api_base_url': AUDIO_STT_OPENAI_API_BASE_URL, 'audio.stt.openai.api_key': AUDIO_STT_OPENAI_API_KEY, + 'audio.stt.openai.api_request_format': AUDIO_STT_OPENAI_API_REQUEST_FORMAT, 'audio.stt.engine': AUDIO_STT_ENGINE, 'audio.stt.model': AUDIO_STT_MODEL, 'audio.stt.supported_content_types': AUDIO_STT_SUPPORTED_CONTENT_TYPES, diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py index e4b1e57b328..cd97bda4a88 100644 --- a/backend/open_webui/env.py +++ b/backend/open_webui/env.py @@ -444,17 +444,16 @@ def parse_section(section): if WEBSOCKET_REDIS_OPTIONS == '': + WEBSOCKET_REDIS_OPTIONS = {'socket_timeout': None} if REDIS_SOCKET_CONNECT_TIMEOUT: - WEBSOCKET_REDIS_OPTIONS = {'socket_connect_timeout': REDIS_SOCKET_CONNECT_TIMEOUT} - else: - log.debug('No WEBSOCKET_REDIS_OPTIONS provided, defaulting to None') - WEBSOCKET_REDIS_OPTIONS = None + WEBSOCKET_REDIS_OPTIONS['socket_connect_timeout'] = REDIS_SOCKET_CONNECT_TIMEOUT else: try: WEBSOCKET_REDIS_OPTIONS = json.loads(WEBSOCKET_REDIS_OPTIONS) + WEBSOCKET_REDIS_OPTIONS.setdefault('socket_timeout', None) except Exception: - log.warning('Invalid WEBSOCKET_REDIS_OPTIONS, defaulting to None') - WEBSOCKET_REDIS_OPTIONS = None + log.warning('Invalid WEBSOCKET_REDIS_OPTIONS, defaulting to socket_timeout=None') + WEBSOCKET_REDIS_OPTIONS = {'socket_timeout': None} WEBSOCKET_REDIS_URL = os.getenv('WEBSOCKET_REDIS_URL', REDIS_URL) WEBSOCKET_REDIS_CLUSTER = os.getenv('WEBSOCKET_REDIS_CLUSTER', str(REDIS_CLUSTER)).lower() == 'true' diff --git a/backend/open_webui/events.py b/backend/open_webui/events.py index 1e87d912bdc..71f9f15f1b2 100644 --- a/backend/open_webui/events.py +++ b/backend/open_webui/events.py @@ -442,6 +442,11 @@ class EventDefinitions(BaseModel): 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.', @@ -1108,3 +1113,60 @@ async def publish_event( 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' + ) + + 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, + ) diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index bd9580e9ce9..82eed2f4d66 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -1341,18 +1341,26 @@ async def run_initial_title_generation(): detail=ERROR_MESSAGES.DEFAULT(), ) - # Persist chat-level files (knowledge collections, docs, etc.) + user_message = metadata.get('user_message') or {} + selected_chat_models = user_message.get('models') if isinstance(user_message, dict) else None + if not isinstance(selected_chat_models, list) or not selected_chat_models: + selected_chat_models = [entry.get('model_id') for entry in message_ids if entry.get('model_id')] + + # Persist chat-level fields the frontend used to save on every message. # The old frontend saveChatHandler did this on every message; # now the backend owns persistence. chat_files = metadata.get('files') - if chat_files is not None: + if chat_files is not None or selected_chat_models: existing_chat = await Chats.get_chat_by_id(chat_id) if existing_chat: - updated = {**existing_chat.chat, 'files': chat_files} + updated = {**existing_chat.chat} + if chat_files is not None: + updated['files'] = chat_files + if selected_chat_models: + updated['models'] = selected_chat_models await Chats.update_chat_by_id(chat_id, updated) # Save user message to DB - user_message = metadata.get('user_message') or {} if user_message and user_message.get('id'): await Chats.upsert_message_to_chat_by_id_and_message_id( chat_id, diff --git a/backend/open_webui/migrations/versions/b10670c03dd5_update_user_table.py b/backend/open_webui/migrations/versions/b10670c03dd5_update_user_table.py index ba2ab26cca7..c3965dbd7cc 100644 --- a/backend/open_webui/migrations/versions/b10670c03dd5_update_user_table.py +++ b/backend/open_webui/migrations/versions/b10670c03dd5_update_user_table.py @@ -72,7 +72,6 @@ def _convert_column_to_json(table: str, column: str): dialect = conn.dialect.name t = sa.table(table, sa.column('id', sa.Text), sa.column(column, sa.Text)) - t_json = sa.column(f'{column}_json', sa.JSON) # SQLite cannot ALTER COLUMN → must recreate column if dialect == 'sqlite': @@ -90,9 +89,9 @@ def _convert_column_to_json(table: str, column: str): parsed = None conn.execute( - sa.update(sa.table(table, sa.column('id'), t_json)) + sa.update(sa.table(table, sa.column('id'), sa.column(f'{column}_json', sa.JSON))) .where(sa.column('id') == uid) - .values({f'{column}_json': json.dumps(parsed) if parsed else None}) + .values({f'{column}_json': parsed}) ) op.drop_column(table, column) @@ -112,8 +111,7 @@ def _convert_column_to_text(table: str, column: str): conn = op.get_bind() dialect = conn.dialect.name - t = sa.table(table, sa.column('id', sa.Text), sa.column(column)) - t_text = sa.column(f'{column}_text', sa.Text) + t = sa.table(table, sa.column('id', sa.Text), sa.column(column, sa.JSON)) if dialect == 'sqlite': op.add_column(table, sa.Column(f'{column}_text', sa.Text(), nullable=True)) @@ -122,9 +120,9 @@ def _convert_column_to_text(table: str, column: str): for uid, raw in rows: conn.execute( - sa.update(sa.table(table, sa.column('id'), t_text)) + sa.update(sa.table(table, sa.column('id'), sa.column(f'{column}_text', sa.Text))) .where(sa.column('id') == uid) - .values({f'{column}_text': json.dumps(raw) if raw else None}) + .values({f'{column}_text': json.dumps(raw) if raw is not None else None}) ) op.drop_column(table, column) diff --git a/backend/open_webui/models/config.py b/backend/open_webui/models/config.py index 86d325c8940..93a55f690d3 100644 --- a/backend/open_webui/models/config.py +++ b/backend/open_webui/models/config.py @@ -14,6 +14,7 @@ import time from typing import Any, ClassVar +from fastapi.encoders import jsonable_encoder from open_webui.internal.db import Base, get_async_db from sqlalchemy import JSON, BigInteger, Column, Text, delete, select @@ -86,6 +87,10 @@ def _assign_path(target: dict, path: list[str], value: Any) -> None: current[path[-1]] = value +def _json_value(value: Any) -> Any: + return jsonable_encoder(value) + + # ── Model ──────────────────────────────────────────────────────────────────── @@ -112,7 +117,7 @@ def configure( enable_persistent: bool = True, enable_oauth_persistent: bool = False, ) -> None: - cls.DEFAULTS = defaults or {} + cls.DEFAULTS = dict(defaults or {}) cls.PERSISTENT_ENABLED = enable_persistent cls.OAUTH_PERSISTENT_ENABLED = enable_oauth_persistent @@ -188,9 +193,20 @@ async def get_all() -> dict: @staticmethod async def upsert(updates: dict) -> None: """Upsert multiple config key-value pairs. Raises on failure.""" + persistent_updates = {} + for key, value in updates.items(): + value = _json_value(value) + if Config.persistent_enabled_for(key): + persistent_updates[key] = value + else: + Config.DEFAULTS[key] = value + + if not persistent_updates: + return + async with get_async_db() as db: now = int(time.time()) - for key, value in updates.items(): + for key, value in persistent_updates.items(): existing = await db.get(Config, key) if existing: existing.value = value @@ -232,6 +248,7 @@ async def seed_defaults(defaults: dict) -> None: new_count = 0 for key, value in defaults.items(): if key not in existing_keys: + value = _json_value(value) db.add(Config(key=key, value=value, updated_at=now)) existing_keys.add(key) new_count += 1 diff --git a/backend/open_webui/retrieval/web/main.py b/backend/open_webui/retrieval/web/main.py index 56f796db589..23cbf08aebf 100644 --- a/backend/open_webui/retrieval/web/main.py +++ b/backend/open_webui/retrieval/web/main.py @@ -19,7 +19,7 @@ def get_filtered_results(results, filter_list): if not validators.url(url): continue - domain = urlparse(url).netloc + domain = urlparse(url).hostname if not domain: continue diff --git a/backend/open_webui/routers/audio.py b/backend/open_webui/routers/audio.py index 8310f49a2cf..47dec5fa4c7 100644 --- a/backend/open_webui/routers/audio.py +++ b/backend/open_webui/routers/audio.py @@ -94,6 +94,7 @@ STT_CONFIG_KEYS = { 'OPENAI_API_BASE_URL': 'audio.stt.openai.api_base_url', 'OPENAI_API_KEY': 'audio.stt.openai.api_key', + 'OPENAI_API_REQUEST_FORMAT': 'audio.stt.openai.api_request_format', 'ENGINE': 'audio.stt.engine', 'MODEL': 'audio.stt.model', 'SUPPORTED_CONTENT_TYPES': 'audio.stt.supported_content_types', @@ -252,6 +253,7 @@ class TTSConfigForm(BaseModel): class STTConfigForm(BaseModel): OPENAI_API_BASE_URL: str OPENAI_API_KEY: str + OPENAI_API_REQUEST_FORMAT: str = 'multipart' ENGINE: str MODEL: str SUPPORTED_CONTENT_TYPES: list[str] = [] @@ -644,28 +646,47 @@ async def _transcribe_openai(request, file_path, filename, languages, file_dir, r = None try: session = await get_session() + api_key = await Config.get('audio.stt.openai.api_key') + api_base_url = await Config.get('audio.stt.openai.api_base_url') + request_format = (await Config.get('audio.stt.openai.api_request_format') or 'multipart').lower() + + headers = {'Authorization': f'Bearer {api_key}'} + if user and ENABLE_FORWARD_USER_INFO_HEADERS: + headers = include_user_info_headers(headers, user) + for language in languages: payload = {'model': await Config.get('audio.stt.model')} if language: payload['language'] = language - api_key = await Config.get('audio.stt.openai.api_key') - api_base_url = await Config.get('audio.stt.openai.api_base_url') - headers = {'Authorization': f'Bearer {api_key}'} - if user and ENABLE_FORWARD_USER_INFO_HEADERS: - headers = include_user_info_headers(headers, user) - - form_data = aiohttp.FormData() - for key, value in payload.items(): - form_data.add_field(key, str(value)) - form_data.add_field('file', open(file_path, 'rb'), filename=filename) + if request_format == 'json': + ext = os.path.splitext(filename)[1].lower().lstrip('.') or 'wav' + async with aiofiles.open(file_path, 'rb') as f: + payload['input_audio'] = { + 'data': base64.b64encode(await f.read()).decode('utf-8'), + 'format': 'ogg' if ext == 'oga' else ext, + } - r = await session.post( - url=f'{api_base_url}/audio/transcriptions', - headers=headers, - data=form_data, - ssl=AIOHTTP_CLIENT_SESSION_SSL, - ) + r = await session.post( + url=f'{api_base_url}/audio/transcriptions', + headers={**headers, 'Content-Type': 'application/json'}, + json=payload, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) + else: + form_data = aiohttp.FormData() + for key, value in payload.items(): + form_data.add_field(key, str(value)) + + with open(file_path, 'rb') as audio_file: + form_data.add_field('file', audio_file, filename=filename) + + r = await session.post( + url=f'{api_base_url}/audio/transcriptions', + headers=headers, + data=form_data, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) if r.status == 200: break diff --git a/backend/open_webui/routers/auths.py b/backend/open_webui/routers/auths.py index 9f71ecafb8b..660b9e7d96c 100644 --- a/backend/open_webui/routers/auths.py +++ b/backend/open_webui/routers/auths.py @@ -105,6 +105,7 @@ 'ENABLE_CHANNELS': 'channels.enable', 'ENABLE_CALENDAR': 'calendar.enable', 'ENABLE_MEMORIES': 'memories.enable', + 'ENABLE_MEMORY_SYSTEM_CONTEXT': 'memories.system_context.enable', 'ENABLE_NOTES': 'notes.enable', 'ENABLE_USER_WEBHOOKS': 'ui.enable_user_webhooks', 'ENABLE_USER_STATUS': 'users.enable_status', @@ -1142,6 +1143,7 @@ class AdminConfig(BaseModel): ENABLE_CHANNELS: bool ENABLE_CALENDAR: bool ENABLE_MEMORIES: bool + ENABLE_MEMORY_SYSTEM_CONTEXT: bool ENABLE_NOTES: bool ENABLE_USER_WEBHOOKS: bool ENABLE_USER_STATUS: bool diff --git a/backend/open_webui/routers/models.py b/backend/open_webui/routers/models.py index 59c025055ec..1c6369fb04c 100644 --- a/backend/open_webui/routers/models.py +++ b/backend/open_webui/routers/models.py @@ -718,6 +718,9 @@ async def update_model_by_id( db, ) + if 'base_model_id' not in form_data.model_fields_set: + form_data.base_model_id = model.base_model_id + form_data.access_grants = await filter_allowed_access_grants( await Config.get('user.permissions'), user.id, diff --git a/backend/open_webui/routers/ollama.py b/backend/open_webui/routers/ollama.py index b755acf80f2..956e310088a 100644 --- a/backend/open_webui/routers/ollama.py +++ b/backend/open_webui/routers/ollama.py @@ -20,7 +20,7 @@ from open_webui.config import UPLOAD_DIR from open_webui.constants import ERROR_MESSAGES -from open_webui.events import EVENTS, publish_event +from open_webui.events import EVENTS, publish_event, publish_model_provider_request_failed from open_webui.env import ( AIOHTTP_CLIENT_SESSION_SSL, AIOHTTP_CLIENT_TIMEOUT, @@ -133,12 +133,27 @@ async def send_request( if not r.ok: try: res = await r.json() + await publish_model_provider_request_failed( + request, + actor=user, + provider='ollama', + base_url=url, + status=r.status, + upstream_error=res, + ) if 'error' in res: raise HTTPException(status_code=r.status, detail=res['error']) except HTTPException: raise except Exception as e: log.error(f'Failed to parse error response: {e}') + await publish_model_provider_request_failed( + request, + actor=user, + provider='ollama', + base_url=url, + status=r.status, + ) raise HTTPException( status_code=r.status, detail=ERROR_MESSAGES.SERVER_CONNECTION_ERROR, diff --git a/backend/open_webui/routers/openai.py b/backend/open_webui/routers/openai.py index f7019fbf3a4..66e1932318f 100644 --- a/backend/open_webui/routers/openai.py +++ b/backend/open_webui/routers/openai.py @@ -22,7 +22,7 @@ CACHE_DIR, ) from open_webui.constants import ERROR_MESSAGES -from open_webui.events import EVENTS, publish_event +from open_webui.events import EVENTS, publish_event, publish_model_provider_request_failed from open_webui.env import ( AIOHTTP_CLIENT_SESSION_SSL, AIOHTTP_CLIENT_TIMEOUT, @@ -1236,6 +1236,7 @@ async def generate_chat_completion( request_url = f'{url}/responses' else: request_url = f'{url}/chat/completions' + requested_model = payload.get('model') # For Chat Completions, strip image parts from multimodal tool messages # (Chat Completions doesn't support images in tool content). if not is_responses and 'messages' in payload: @@ -1278,8 +1279,28 @@ async def generate_chat_completion( ) try: error_json = json.loads(error_body) + await publish_model_provider_request_failed( + request, + actor=user, + provider='openai-compatible', + base_url=url, + api_key=key, + status=r.status, + requested_model=requested_model, + upstream_error=error_json, + ) return JSONResponse(status_code=r.status, content=error_json) except json.JSONDecodeError: + await publish_model_provider_request_failed( + request, + actor=user, + provider='openai-compatible', + base_url=url, + api_key=key, + status=r.status, + requested_model=requested_model, + upstream_error=error_body, + ) return JSONResponse( status_code=r.status, content={'error': {'message': error_body, 'code': r.status}}, @@ -1299,6 +1320,16 @@ async def generate_chat_completion( response = await r.text() if r.status >= 400: + await publish_model_provider_request_failed( + request, + actor=user, + provider='openai-compatible', + base_url=url, + api_key=key, + status=r.status, + requested_model=requested_model, + upstream_error=response, + ) if isinstance(response, (dict, list)): return JSONResponse(status_code=r.status, content=response) else: @@ -1372,6 +1403,7 @@ async def embeddings(request: Request, form_data: dict, user): headers['api-version'] = api_version else: embeddings_url = f'{url}/embeddings' + requested_model = form_data.get('model') try: session = await get_session() @@ -1399,6 +1431,16 @@ async def embeddings(request: Request, form_data: dict, user): response_data = await r.text() if r.status >= 400: + await publish_model_provider_request_failed( + request, + actor=user, + provider='openai-compatible', + base_url=url, + api_key=key, + status=r.status, + requested_model=requested_model, + upstream_error=response_data, + ) if isinstance(response_data, (dict, list)): return JSONResponse(status_code=r.status, content=response_data) else: @@ -1515,6 +1557,16 @@ async def responses( response_data = await r.text() if r.status >= 400: + await publish_model_provider_request_failed( + request, + actor=user, + provider='openai-compatible', + base_url=url, + api_key=key, + status=r.status, + requested_model=payload.get('model'), + upstream_error=response_data, + ) if isinstance(response_data, (dict, list)): return JSONResponse(status_code=r.status, content=response_data) else: @@ -1569,6 +1621,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)): idx = models[model_id]['urlIdx'] url, key, api_config = await get_openai_connection(idx) + base_url = url r = None streaming = False @@ -1625,6 +1678,16 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)): response_data = await r.text() if r.status >= 400: + await publish_model_provider_request_failed( + request, + actor=user, + provider='openai-compatible', + base_url=base_url, + api_key=key, + status=r.status, + requested_model=model_id, + upstream_error=response_data, + ) if isinstance(response_data, (dict, list)): return JSONResponse(status_code=r.status, content=response_data) else: diff --git a/backend/open_webui/routers/users.py b/backend/open_webui/routers/users.py index d15708941a8..378b6823574 100644 --- a/backend/open_webui/routers/users.py +++ b/backend/open_webui/routers/users.py @@ -326,7 +326,7 @@ async def update_user_settings_by_session_user( db: AsyncSession = Depends(get_async_session), ): if user.role != 'admin' and not await has_permission( - user.id, 'settings.interface', request.app.state.config.USER_PERMISSIONS + user.id, 'settings.interface', await Config.get('user.permissions') ): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, diff --git a/backend/open_webui/tools/builtin.py b/backend/open_webui/tools/builtin.py index 6193c10c53b..f6631691d31 100644 --- a/backend/open_webui/tools/builtin.py +++ b/backend/open_webui/tools/builtin.py @@ -726,7 +726,11 @@ async def add_memory( __user__: dict = None, ) -> str: """ - Save a user-provided preference, fact, or instruction as memory for future chats. + Save enduring information that can improve future chats. + + Save stable preferences, goals, projects, relationships, habits, and standing instructions. + Do not save one-off activity, meals, routine daily events, temporary mood, or other short-lived details + unless the user explicitly asks you to remember them. :param content: The memory content to store :param type: Use "user" for facts/preferences about the user, or "context" for other durable context @@ -760,10 +764,12 @@ async def update_memory( __user__: dict = None, ) -> str: """ - Apply a batch of memory changes after learning durable information. + Apply a batch of memory changes after learning enduring information. Use type "user" for facts, preferences, or instructions about the user. Use type "context" for other durable context that may help future chats. + Do not save one-off activity, meals, routine daily events, temporary mood, or other short-lived details + unless the user explicitly asks you to remember them. Path is optional. Use it as a stable memory address to group related memories. Prefer an existing path from list_memory_paths when one fits. Leave path empty when no useful grouping is clear. diff --git a/backend/open_webui/utils/automations.py b/backend/open_webui/utils/automations.py index a2342f094b8..d4ac9c52070 100644 --- a/backend/open_webui/utils/automations.py +++ b/backend/open_webui/utils/automations.py @@ -267,7 +267,7 @@ async def _resolve_model_features(app, model_id: str) -> dict: if not default_feature_ids: return {} - capabilities = meta.get('capabilities', {}) + capabilities = meta.get('capabilities') or {} features = {} # code_interpreter is excluded: it requires the frontend event emitter diff --git a/backend/open_webui/utils/images/comfyui.py b/backend/open_webui/utils/images/comfyui.py index a07078cbb46..bbf92a65456 100644 --- a/backend/open_webui/utils/images/comfyui.py +++ b/backend/open_webui/utils/images/comfyui.py @@ -202,7 +202,7 @@ async def comfyui_create_image(model: str, payload: ComfyUICreateImageForm, clie ) as ws: log.info('WebSocket connection established.') log.info('Sending workflow to WebSocket server.') - log.info(f'Workflow: {workflow}') + log.debug(f'Workflow: {workflow}') images = await _ws_get_images(ws, workflow, client_id, base_url, api_key) except aiohttp.WSServerHandshakeError as e: log.exception(f'Failed to connect to WebSocket server: {e}') @@ -243,7 +243,7 @@ async def comfyui_edit_image(model: str, payload: ComfyUIEditImageForm, client_i ) as ws: log.info('WebSocket connection established.') log.info('Sending workflow to WebSocket server.') - log.info(f'Workflow: {workflow}') + log.debug(f'Workflow: {workflow}') images = await _ws_get_images(ws, workflow, client_id, base_url, api_key) except aiohttp.WSServerHandshakeError as e: log.exception(f'Failed to connect to WebSocket server: {e}') diff --git a/backend/open_webui/utils/memory.py b/backend/open_webui/utils/memory.py index 6d96c4cab70..5cec5153fff 100644 --- a/backend/open_webui/utils/memory.py +++ b/backend/open_webui/utils/memory.py @@ -284,7 +284,7 @@ def validate_memory_operations(form_data) -> list[dict]: def model_allows_memory(model: dict | None) -> bool: - return (model or {}).get('info', {}).get('meta', {}).get('capabilities', {}).get('memory', True) + return ((model or {}).get('info', {}).get('meta', {}).get('capabilities') or {}).get('memory', True) async def add_memory_context(request, form_data: dict, user, model: dict | None = None): @@ -535,7 +535,8 @@ async def _generate_memory_operations( - context: other durable context that may help future chats for this user account. Rules: -- Save only information likely to matter in future chats. +- Save enduring details that can improve future conversations. +- Do not save one-off activity, meals, temporary mood, routine daily events, or other short-lived details unless the user explicitly asks to remember them. - Do not save secrets, credentials, transient task steps, or unsupported guesses. - Use path when there is a clear path for the memory. - Leave path empty when there is no clear place for the memory. diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index fc8802a6839..b507aa66c5d 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -157,6 +157,29 @@ def output_id(prefix: str) -> str: return f'{prefix}_{uuid4().hex[:24]}' +def merge_streamed_reasoning_details(target: list, details) -> None: + items = details if isinstance(details, list) else [details] + for item in items: + if not isinstance(item, dict): + continue + + index = item.get('index') + existing = ( + next((detail for detail in target if detail.get('index') == index), None) + if isinstance(index, int) + else None + ) + if existing is None: + target.append(dict(item)) + continue + + for key, value in item.items(): + if key in ('text', 'summary') and isinstance(value, str) and isinstance(existing.get(key), str): + existing[key] += value + else: + existing[key] = value + + def _split_tool_calls( tool_calls: list[dict], ) -> list[dict]: @@ -2427,7 +2450,7 @@ async def process_chat_payload(request, form_data, user, metadata, model): form_data['messages'], ) - if 'memory' in features and features['memory']: + if 'memory' in features and features['memory'] and await Config.get('memories.system_context.enable'): form_data = await add_memory_context(request, form_data, user, model) if 'web_search' in features and features['web_search']: @@ -2905,6 +2928,13 @@ def update_assistant_message_from_stream(assistant_message, raw): if not isinstance(line, str): return + def append_output_text(item, text): + parts = item.setdefault('content', []) + if parts and parts[-1].get('type') == 'output_text': + parts[-1]['text'] += text + else: + parts.append({'type': 'output_text', 'text': text}) + for raw_part in line.splitlines(): part = raw_part.removeprefix('data:').strip() if not part or part == '[DONE]': @@ -2932,8 +2962,50 @@ def update_assistant_message_from_stream(assistant_message, raw): assistant_message['usage'] = merge_usage(assistant_message.get('usage'), raw_usage) for choice in data.get('choices', []): - content = (choice.get('delta', {}) or {}).get('content') + delta = choice.get('delta', {}) or {} + content = delta.get('content') + reasoning_content = delta.get('reasoning_content') or delta.get('reasoning') or delta.get('thinking') + + if reasoning_content: + output = assistant_message.setdefault('output', []) + if not output or output[-1].get('type') != 'reasoning': + output.append( + { + 'type': 'reasoning', + 'id': output_id('r'), + 'status': 'in_progress', + 'start_tag': '', + 'end_tag': '', + 'attributes': {'type': 'reasoning_content'}, + 'content': [], + 'summary': None, + 'started_at': time.time(), + } + ) + + append_output_text(output[-1], reasoning_content) + if content: + output = assistant_message.get('output') + if output: + if output[-1].get('type') == 'reasoning': + output[-1]['status'] = 'completed' + output[-1]['ended_at'] = time.time() + output[-1]['duration'] = int(output[-1]['ended_at'] - output[-1]['started_at']) + + if not output or output[-1].get('type') != 'message': + output.append( + { + 'type': 'message', + 'id': output_id('msg'), + 'status': 'in_progress', + 'role': 'assistant', + 'content': [], + } + ) + + append_output_text(output[-1], content) + assistant_message['content'] = assistant_message.get('content', '') + content @@ -3395,10 +3467,11 @@ async def non_streaming_chat_response_handler(response, ctx): ) choices = response_data.get('choices', []) - if choices and choices[0].get('message', {}).get('content'): - content = response_data['choices'][0]['message']['content'] + response_output = response_data.get('output') + content = choices[0].get('message', {}).get('content') if choices else '' - if content: + if choices and (content or response_output): + if content or response_output: await event_emitter( { 'type': 'chat:completion', @@ -3414,7 +3487,6 @@ async def non_streaming_chat_response_handler(response, ctx): # Use output from backend if provided (OR-compliant backends), # otherwise generate from response content - response_output = response_data.get('output') if not response_output: choice_message = choices[0].get('message', {}) reasoning_content = choice_message.get('reasoning_content') or choice_message.get('reasoning') @@ -3453,7 +3525,6 @@ async def non_streaming_chat_response_handler(response, ctx): 'type': 'chat:completion', 'data': { 'done': True, - 'content': content, 'output': response_output, 'title': title, }, @@ -3470,7 +3541,6 @@ async def non_streaming_chat_response_handler(response, ctx): { 'done': True, 'role': 'assistant', - 'content': content, 'output': response_output, **({'usage': usage} if usage else {}), }, @@ -4234,21 +4304,31 @@ async def queue_pending_delta_data(delta_data: dict, delta_type: str): ) reasoning_details = delta.get('reasoning_details') if reasoning_content or reasoning_details: - if not output or output[-1].get('type') != 'reasoning': - reasoning_item = { - 'type': 'reasoning', - 'id': output_id('r'), - 'status': 'in_progress', - 'start_tag': '', - 'end_tag': '', - 'attributes': {'type': 'reasoning_content'}, - 'content': [], - 'summary': None, - 'started_at': time.time(), - } - output.append(reasoning_item) - else: - reasoning_item = output[-1] + reasoning_item = ( + next( + (item for item in reversed(output) if item.get('type') == 'reasoning'), + None, + ) + if reasoning_details and not reasoning_content + else None + ) + + if reasoning_item is None: + if not output or output[-1].get('type') != 'reasoning': + reasoning_item = { + 'type': 'reasoning', + 'id': output_id('r'), + 'status': 'in_progress', + 'start_tag': '', + 'end_tag': '', + 'attributes': {'type': 'reasoning_content'}, + 'content': [], + 'summary': None, + 'started_at': time.time(), + } + output.append(reasoning_item) + else: + reasoning_item = output[-1] if reasoning_content: # Append to reasoning content @@ -4269,11 +4349,14 @@ async def queue_pending_delta_data(delta_data: dict, delta_type: str): delta_type = 'content' if reasoning_details: - reasoning_item.setdefault('reasoning_details', []).extend( - reasoning_details - if isinstance(reasoning_details, list) - else [reasoning_details] + merge_streamed_reasoning_details( + reasoning_item.setdefault('reasoning_details', []), + reasoning_details, ) + data = { + 'output': full_output(), + } + delta_type = 'content' if value: if ( diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index 67b66d1a0d2..0d72a24efeb 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -24,6 +24,8 @@ HTTPException, status, ) +from joserfc.jws import JWSRegistry +from joserfc.registry import HeaderParameter from mcp.shared.auth import ( OAuthClientMetadata as MCPOAuthClientMetadata, ) @@ -187,6 +189,14 @@ async def get_oauth_runtime_config() -> SimpleNamespace: DEFAULT_TOKEN_EXPIRY_SECONDS = 3600 +# Apereo CAS includes client_id in ID token JWS headers; Authlib 1.7/joserfc +# rejects unknown headers unless we register the provider extension. +JWSRegistry.default_header_registry.setdefault( + 'client_id', + HeaderParameter('OAuth client identifier', 'str'), +) + + def _normalize_token_expiry(token: dict) -> dict: """Ensure a token dict always has a numeric ``expires_at``. diff --git a/package-lock.json b/package-lock.json index 9772ea83728..7c572b1bf5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "open-webui", - "version": "0.10.1", + "version": "0.10.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "open-webui", - "version": "0.10.1", + "version": "0.10.2", "dependencies": { "@azure/msal-browser": "^4.5.0", "@codemirror/lang-javascript": "^6.2.2", diff --git a/package.json b/package.json index 8566845b684..aeca4abd10e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-webui", - "version": "0.10.1", + "version": "0.10.2", "private": true, "scripts": { "dev": "npm run pyodide:fetch && vite dev --host", diff --git a/src/lib/components/admin/Settings/Audio.svelte b/src/lib/components/admin/Settings/Audio.svelte index cf893a24b00..a9a7fad0010 100644 --- a/src/lib/components/admin/Settings/Audio.svelte +++ b/src/lib/components/admin/Settings/Audio.svelte @@ -43,6 +43,7 @@ let STT_OPENAI_API_BASE_URL = ''; let STT_OPENAI_API_KEY = ''; + let STT_OPENAI_API_REQUEST_FORMAT = 'multipart'; let STT_ENGINE = ''; let STT_MODEL = ''; let STT_SUPPORTED_CONTENT_TYPES = ''; @@ -150,6 +151,7 @@ stt: { OPENAI_API_BASE_URL: STT_OPENAI_API_BASE_URL, OPENAI_API_KEY: STT_OPENAI_API_KEY, + OPENAI_API_REQUEST_FORMAT: STT_OPENAI_API_REQUEST_FORMAT, ENGINE: STT_ENGINE, MODEL: STT_MODEL, SUPPORTED_CONTENT_TYPES: STT_SUPPORTED_CONTENT_TYPES.split(','), @@ -202,6 +204,7 @@ STT_OPENAI_API_BASE_URL = res.stt.OPENAI_API_BASE_URL; STT_OPENAI_API_KEY = res.stt.OPENAI_API_KEY; + STT_OPENAI_API_REQUEST_FORMAT = res.stt.OPENAI_API_REQUEST_FORMAT || 'multipart'; STT_ENGINE = res.stt.ENGINE; STT_MODEL = res.stt.MODEL; @@ -288,6 +291,21 @@
+
+
{$i18n.t('Request Format')}
+
+ +
+
+ +
+
{$i18n.t('STT Model')}
diff --git a/src/lib/components/admin/Settings/Documents.svelte b/src/lib/components/admin/Settings/Documents.svelte index ba38a056594..23e758d9ded 100644 --- a/src/lib/components/admin/Settings/Documents.svelte +++ b/src/lib/components/admin/Settings/Documents.svelte @@ -1247,12 +1247,14 @@
{/if}
+ {/if} -
-
{$i18n.t('Retrieval')}
+
+
{$i18n.t('Retrieval')}
-
+
+ {#if !RAGConfig.BYPASS_EMBEDDING_AND_RETRIEVAL}
{$i18n.t('Full Context Mode')}
@@ -1500,36 +1502,34 @@
{/if} {/if} + {/if} -
-
{$i18n.t('RAG Template')}
-
- +
{$i18n.t('RAG Template')}
+
+ +