diff --git a/backend/open_webui/routers/configs.py b/backend/open_webui/routers/configs.py index 0eb88e767e..b48db49222 100644 --- a/backend/open_webui/routers/configs.py +++ b/backend/open_webui/routers/configs.py @@ -147,6 +147,7 @@ class ToolServerConnection(BaseModel): headers: Optional[dict | str] = None key: Optional[str] config: Optional[dict] + placeholders: Optional[list[str]] = None model_config = ConfigDict(extra="allow") diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index eebe41d571..0be2c8b44e 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -91,7 +91,11 @@ from open_webui.utils.misc import ( convert_logit_bias_input_to_json, get_content_from_message, ) -from open_webui.utils.tools import get_tools, get_updated_tool_function +from open_webui.utils.tools import ( + get_tools, + get_updated_tool_function, + replace_placeholders_in_headers +) from open_webui.utils.plugin import load_function_module_by_id from open_webui.utils.filter import ( get_sorted_filter_ids, @@ -1142,10 +1146,16 @@ async def process_chat_payload(request, form_data, user, metadata, model): except Exception as e: log.error(f"Error getting OAuth token: {e}") + __user__ = user.model_dump() if isinstance(user, UserModel) else {} + if isinstance(user, UserModel) and user.settings: + user_settings = user.settings.model_dump() if user.settings else {} + if "tool_server_placeholders" in user_settings: + __user__["tool_server_placeholders"] = user_settings["tool_server_placeholders"] + extra_params = { "__event_emitter__": event_emitter, "__event_call__": event_caller, - "__user__": user.model_dump() if isinstance(user, UserModel) else {}, + "__user__": __user__, "__metadata__": metadata, "__oauth_token__": oauth_token, "__request__": request, @@ -1404,6 +1414,16 @@ async def process_chat_payload(request, form_data, user, metadata, model): connection_headers = mcp_server_connection.get("headers", None) if connection_headers and isinstance(connection_headers, dict): + user_placeholders = ( + extra_params.get("__user__", {}) + .get("tool_server_placeholders", {}) + .get(server_id, {}) + ) + + connection_headers = replace_placeholders_in_headers( + connection_headers, user_placeholders + ) + for key, value in connection_headers.items(): headers[key] = value @@ -1981,10 +2001,16 @@ async def process_chat_response( except Exception as e: log.error(f"Error getting OAuth token: {e}") + __user__ = user.model_dump() if isinstance(user, UserModel) else {} + if isinstance(user, UserModel) and user.settings: + user_settings = user.settings.model_dump() if user.settings else {} + if "tool_server_placeholders" in user_settings: + __user__["tool_server_placeholders"] = user_settings["tool_server_placeholders"] + extra_params = { "__event_emitter__": event_emitter, "__event_call__": event_caller, - "__user__": user.model_dump() if isinstance(user, UserModel) else {}, + "__user__": __user__, "__metadata__": metadata, "__oauth_token__": oauth_token, "__request__": request, diff --git a/backend/open_webui/utils/tools.py b/backend/open_webui/utils/tools.py index fb623ed332..e55e0b148b 100644 --- a/backend/open_webui/utils/tools.py +++ b/backend/open_webui/utils/tools.py @@ -50,6 +50,28 @@ log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["MODELS"]) +def replace_placeholders_in_headers( + headers: dict, placeholders: dict[str, str] +) -> dict: + if not headers or not placeholders: + return headers + + replaced_headers = {} + for key, value in headers.items(): + if isinstance(value, str): + replaced_value = value + for placeholder_name, placeholder_value in placeholders.items(): + placeholder_pattern = f"{{{{{placeholder_name}}}}}" + replaced_value = replaced_value.replace( + placeholder_pattern, placeholder_value + ) + replaced_headers[key] = replaced_value + else: + replaced_headers[key] = value + + return replaced_headers + + def get_async_tool_function_and_apply_extra_params( function: Callable, extra_params: dict ) -> Callable[..., Awaitable]: @@ -181,6 +203,17 @@ async def get_tools( connection_headers = tool_server_connection.get("headers", None) if connection_headers and isinstance(connection_headers, dict): + user_placeholders = ( + extra_params.get("__user__", {}) + .get("tool_server_placeholders", {}) + .get(server_id, {}) + ) + + # Replace placeholders in headers + connection_headers = replace_placeholders_in_headers( + connection_headers, user_placeholders + ) + for key, value in connection_headers.items(): headers[key] = value diff --git a/src/lib/components/AddToolServerModal.svelte b/src/lib/components/AddToolServerModal.svelte index 5515910a18..a90d02f80e 100644 --- a/src/lib/components/AddToolServerModal.svelte +++ b/src/lib/components/AddToolServerModal.svelte @@ -46,6 +46,7 @@ let auth_type = 'bearer'; let key = ''; let headers = ''; + let placeholders: string[] = []; let accessControl = {}; @@ -196,6 +197,7 @@ if (data.auth_type) auth_type = data.auth_type; if (data.headers) headers = JSON.stringify(data.headers, null, 2); if (data.key) key = data.key; + if (data.placeholders) placeholders = data.placeholders; if (data.info) { id = data.info.id ?? ''; @@ -230,6 +232,7 @@ auth_type, headers: headers ? JSON.parse(headers) : undefined, key, + placeholders: placeholders.length > 0 ? placeholders : undefined, info: { id: id, @@ -300,6 +303,7 @@ headers: headers ? JSON.parse(headers) : undefined, key, + placeholders: placeholders.length > 0 ? placeholders : undefined, config: { enable: enable, @@ -328,6 +332,7 @@ key = ''; auth_type = 'bearer'; + placeholders = []; id = ''; name = ''; @@ -351,6 +356,7 @@ headers = connection?.headers ? JSON.stringify(connection.headers, null, 2) : ''; key = connection?.key ?? ''; + placeholders = connection?.placeholders ?? []; id = connection.info?.id ?? ''; name = connection.info?.name ?? ''; @@ -721,6 +727,66 @@ +
+
+
+ + + + + +
+ +
+ {$i18n.t( + 'Define placeholder names that users will need to fill in. Use them in headers as {{PLACEHOLDER_NAME}}' + )} +
+ + {#if placeholders.length > 0} +
+ {#each placeholders as placeholder, idx} +
+ + +
+ {/each} +
+ {/if} +
+
+
diff --git a/src/lib/components/chat/MessageInput/IntegrationsMenu.svelte b/src/lib/components/chat/MessageInput/IntegrationsMenu.svelte index 348d23d073..3fabb66cfd 100644 --- a/src/lib/components/chat/MessageInput/IntegrationsMenu.svelte +++ b/src/lib/components/chat/MessageInput/IntegrationsMenu.svelte @@ -6,7 +6,7 @@ import { config, user, tools as _tools, mobile, settings, toolServers } from '$lib/stores'; - import { getOAuthClientAuthorizationUrl } from '$lib/apis/configs'; + import { getOAuthClientAuthorizationUrl, getToolServerConnections } from '$lib/apis/configs'; import { getTools } from '$lib/apis/tools'; import Knobs from '$lib/components/icons/Knobs.svelte'; @@ -21,6 +21,8 @@ import Terminal from '$lib/components/icons/Terminal.svelte'; import ChevronRight from '$lib/components/icons/ChevronRight.svelte'; import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte'; + import PencilSolid from '$lib/components/icons/PencilSolid.svelte'; + import PlaceholderConfigModal from './PlaceholderConfigModal.svelte'; const i18n = getContext('i18n'); @@ -48,6 +50,10 @@ let tab = ''; let tools = null; + let toolServerPlaceholders = {}; + + let showPlaceholderModal = false; + let selectedServerForPlaceholder = null; $: if (show) { init(); @@ -88,6 +94,25 @@ } } + try { + const res = await getToolServerConnections(localStorage.token); + const adminServers = res.TOOL_SERVER_CONNECTIONS || []; + + toolServerPlaceholders = {}; + for (const server of adminServers) { + if (server.placeholders && server.placeholders.length > 0) { + const serverId = server.info?.id || server.url; + toolServerPlaceholders[serverId] = { + name: server.info?.name || server.url, + placeholders: server.placeholders, + serverId: serverId + }; + } + } + } catch (err) { + console.error('Failed to fetch admin tool servers:', err); + } + selectedToolIds = selectedToolIds.filter((id) => Object.keys(tools).includes(id)); }; @@ -392,6 +417,28 @@
{/if} + {#if Object.keys(toolServerPlaceholders).find((serverId) => toolId.includes(serverId))} + {@const serverData = toolServerPlaceholders[ + Object.keys(toolServerPlaceholders).find((serverId) => toolId.includes(serverId)) + ]} +
+ + + +
+ {/if} +
@@ -402,3 +449,10 @@ + + diff --git a/src/lib/components/chat/MessageInput/PlaceholderConfigModal.svelte b/src/lib/components/chat/MessageInput/PlaceholderConfigModal.svelte new file mode 100644 index 0000000000..ac06aaa3e6 --- /dev/null +++ b/src/lib/components/chat/MessageInput/PlaceholderConfigModal.svelte @@ -0,0 +1,116 @@ + + + +
+
+

+ {$i18n.t('Configure Placeholders')} +

+
+ +
+
+
{serverName}
+
+ {$i18n.t( + 'These values will be used to replace placeholders in the server headers. Your values are private and only visible to you.' + )} +
+
+ +
+ {#each placeholders as placeholder} +
+ + +
+ {$i18n.t('Used in headers as')} {'{{' + placeholder + '}}'} +
+
+ {/each} + +
+ + +
+
+
+
+
diff --git a/src/lib/components/chat/Settings/Tools/PlaceholderValues.svelte b/src/lib/components/chat/Settings/Tools/PlaceholderValues.svelte new file mode 100644 index 0000000000..6e975d9948 --- /dev/null +++ b/src/lib/components/chat/Settings/Tools/PlaceholderValues.svelte @@ -0,0 +1,64 @@ + + +{#if placeholders && placeholders.length > 0} +
+
+
{serverName}
+ {#if serverId} +
{serverId}
+ {/if} +
+ +
+ {#each placeholders as placeholder} +
+ + handleValueChange(placeholder, e.target.value)} + placeholder={$i18n.t(`Enter value for {{placeholder}}`, { placeholder })} + required={false} + /> +
+ {/each} +
+ +
+ + {$i18n.t('What are placeholders?')} + +
+
+{/if}