This commit is contained in:
Cevat Batuhan Tolon 2025-12-10 11:00:40 +01:00 committed by GitHub
commit 20c3c891be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 364 additions and 4 deletions

View file

@ -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")

View file

@ -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,
@ -1139,10 +1143,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,
@ -1401,6 +1411,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
@ -1993,10 +2013,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,

View file

@ -51,6 +51,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]:
@ -195,6 +217,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

View file

@ -46,6 +46,7 @@
let auth_type = 'bearer';
let key = '';
let headers = '';
let placeholders: string[] = [];
let functionNameFilterList = '';
let accessControl = {};
@ -197,6 +198,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 ?? '';
@ -231,6 +233,7 @@
auth_type,
headers: headers ? JSON.parse(headers) : undefined,
key,
placeholders: placeholders.length > 0 ? placeholders : undefined,
info: {
id: id,
@ -302,6 +305,7 @@
headers: headers ? JSON.parse(headers) : undefined,
key,
placeholders: placeholders.length > 0 ? placeholders : undefined,
config: {
enable: enable,
function_name_filter_list: functionNameFilterList,
@ -330,6 +334,7 @@
key = '';
auth_type = 'bearer';
placeholders = [];
id = '';
name = '';
@ -355,6 +360,7 @@
headers = connection?.headers ? JSON.stringify(connection.headers, null, 2) : '';
key = connection?.key ?? '';
placeholders = connection?.placeholders ?? [];
id = connection.info?.id ?? '';
name = connection.info?.name ?? '';
@ -726,6 +732,66 @@
</div>
</div>
<div class="flex gap-2 mt-2">
<div class="flex flex-col w-full">
<div class="flex justify-between items-center mb-0.5">
<label
for="placeholders-input"
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
>
{$i18n.t('Placeholders')}
<span class="text-xs text-gray-200 dark:text-gray-800 ml-0.5">
{$i18n.t('Optional')}
</span>
</label>
<Tooltip content={$i18n.t('Add Placeholder')}>
<button
class="px-1"
on:click={() => {
placeholders = [...placeholders, ''];
}}
type="button"
>
<Plus />
</button>
</Tooltip>
</div>
<div class="text-xs text-gray-500 mb-2">
{$i18n.t(
'Define placeholder names that users will need to fill in. Use them in headers as {{PLACEHOLDER_NAME}}'
)}
</div>
{#if placeholders.length > 0}
<div class="flex flex-col gap-2">
{#each placeholders as placeholder, idx}
<div class="flex gap-2 items-center">
<input
class={`flex-1 text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
type="text"
bind:value={placeholders[idx]}
placeholder={$i18n.t('e.g., API_KEY or USER_TOKEN')}
autocomplete="off"
/>
<button
class="p-1 hover:bg-gray-100 dark:hover:bg-gray-800 rounded"
on:click={() => {
placeholders = placeholders.filter((_, i) => i !== idx);
}}
type="button"
aria-label={$i18n.t('Remove')}
>
<Minus />
</button>
</div>
{/each}
</div>
{/if}
</div>
</div>
<hr class=" border-gray-100 dark:border-gray-700/10 my-2.5 w-full" />
<div class="flex gap-2">

View file

@ -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));
};
</script>
@ -392,6 +417,28 @@
</div>
{/if}
{#if Object.keys(toolServerPlaceholders).find((serverId) => toolId.includes(serverId))}
{@const serverData = toolServerPlaceholders[
Object.keys(toolServerPlaceholders).find((serverId) => toolId.includes(serverId))
]}
<div class=" shrink-0">
<Tooltip content={$i18n.t('Configure Placeholders')}>
<button
class="self-center w-fit text-sm text-gray-600 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition rounded-full"
type="button"
on:click={(e) => {
e.stopPropagation();
e.preventDefault();
selectedServerForPlaceholder = serverData;
showPlaceholderModal = true;
}}
>
<PencilSolid />
</button>
</Tooltip>
</div>
{/if}
<div class=" shrink-0">
<Switch state={tools[toolId].enabled} />
</div>
@ -402,3 +449,10 @@
</DropdownMenu.Content>
</div>
</Dropdown>
<PlaceholderConfigModal
bind:show={showPlaceholderModal}
serverName={selectedServerForPlaceholder?.name || ''}
serverId={selectedServerForPlaceholder?.serverId || ''}
placeholders={selectedServerForPlaceholder?.placeholders || []}
/>

View file

@ -0,0 +1,116 @@
<script lang="ts">
import { getContext } from 'svelte';
import { toast } from 'svelte-sonner';
import Modal from '$lib/components/common/Modal.svelte';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
import { updateUserSettings } from '$lib/apis/users';
import { settings } from '$lib/stores';
const i18n = getContext('i18n');
export let show = false;
export let serverName: string = '';
export let serverId: string = '';
export let placeholders: string[] = [];
let placeholderValues: { [key: string]: string } = {};
$: if (show && placeholders) {
// Initialize values from settings
const savedValues = $settings?.tool_server_placeholders?.[serverId] || {};
placeholderValues = placeholders.reduce((acc, placeholder) => {
acc[placeholder] = savedValues[placeholder] || '';
return acc;
}, {} as { [key: string]: string });
}
const saveHandler = async () => {
try {
const currentPlaceholders = $settings?.tool_server_placeholders || {};
const updatedPlaceholders = {
...currentPlaceholders,
[serverId]: placeholderValues
};
await updateUserSettings(localStorage.token, {
tool_server_placeholders: updatedPlaceholders
});
// Update local settings
settings.set({
...$settings,
tool_server_placeholders: updatedPlaceholders
});
toast.success($i18n.t('Placeholder values saved'));
show = false;
} catch (error) {
toast.error($i18n.t('Failed to save placeholder values'));
console.error('Error saving placeholder values:', error);
}
};
</script>
<Modal size="sm" bind:show>
<div>
<div class="flex justify-between items-center dark:text-gray-100 px-5 pt-4 pb-2">
<h1 class="text-lg font-medium self-center font-primary">
{$i18n.t('Configure Placeholders')}
</h1>
</div>
<div class="flex flex-col px-5 pb-4">
<div class="mb-3">
<div class="text-sm font-medium mb-1">{serverName}</div>
<div class="text-xs text-gray-500">
{$i18n.t(
'These values will be used to replace placeholders in the server headers. Your values are private and only visible to you.'
)}
</div>
</div>
<form
on:submit|preventDefault={saveHandler}
class="flex flex-col gap-3"
>
{#each placeholders as placeholder}
<div class="flex flex-col gap-1">
<label
for={`placeholder-${placeholder}`}
class="text-xs font-medium text-gray-600 dark:text-gray-400"
>
{placeholder}
</label>
<SensitiveInput
id={`placeholder-${placeholder}`}
bind:value={placeholderValues[placeholder]}
placeholder={$i18n.t(`Enter value for {{placeholder}}`, { placeholder })}
required={false}
/>
<div class="text-xs text-gray-500">
{$i18n.t('Used in headers as')} <code class="text-xs">{'{{' + placeholder + '}}'}</code>
</div>
</div>
{/each}
<div class="flex justify-end pt-3 gap-2">
<button
class="px-3.5 py-1.5 text-sm font-medium dark:bg-black dark:hover:bg-gray-900 dark:text-white bg-white text-black hover:bg-gray-100 transition rounded-full"
type="button"
on:click={() => {
show = false;
}}
>
{$i18n.t('Cancel')}
</button>
<button
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
type="submit"
>
{$i18n.t('Save')}
</button>
</div>
</form>
</div>
</div>
</Modal>

View file

@ -0,0 +1,64 @@
<script lang="ts">
import { getContext } from 'svelte';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n');
export let serverName: string = '';
export let serverId: string = '';
export let placeholders: string[] = [];
export let values: { [key: string]: string } = {};
export let onChange: (values: { [key: string]: string }) => void = () => {};
// Initialize values if not set
$: if (placeholders && !Object.keys(values).length) {
values = placeholders.reduce((acc, placeholder) => {
acc[placeholder] = '';
return acc;
}, {} as { [key: string]: string });
}
const handleValueChange = (placeholder: string, value: string) => {
values[placeholder] = value;
onChange(values);
};
</script>
{#if placeholders && placeholders.length > 0}
<div class="flex flex-col gap-2 p-3 bg-gray-50 dark:bg-gray-900 rounded-lg">
<div class="flex items-center justify-between">
<div class="font-medium text-sm">{serverName}</div>
{#if serverId}
<div class="text-xs text-gray-500">{serverId}</div>
{/if}
</div>
<div class="flex flex-col gap-2">
{#each placeholders as placeholder}
<div class="flex flex-col gap-1">
<label for={`${serverId}-${placeholder}`} class="text-xs text-gray-600 dark:text-gray-400">
{placeholder}
</label>
<SensitiveInput
id={`${serverId}-${placeholder}`}
value={values[placeholder] || ''}
on:input={(e) => handleValueChange(placeholder, e.target.value)}
placeholder={$i18n.t(`Enter value for {{placeholder}}`, { placeholder })}
required={false}
/>
</div>
{/each}
</div>
<div class="text-xs text-gray-500 mt-1">
<Tooltip
content={$i18n.t(
'These values will be used to replace placeholders in the server headers (e.g., {{{{PLACEHOLDER_NAME}}}})'
)}
>
<span class="underline cursor-help">{$i18n.t('What are placeholders?')}</span>
</Tooltip>
</div>
</div>
{/if}