Timothy Jaeryang Baek 1 mēnesi atpakaļ
vecāks
revīzija
0c0505e1cd

+ 11 - 0
backend/open_webui/config.py

@@ -881,6 +881,17 @@ except Exception:
     pass
     pass
 OPENAI_API_BASE_URL = "https://api.openai.com/v1"
 OPENAI_API_BASE_URL = "https://api.openai.com/v1"
 
 
+####################################
+# TOOL_SERVERS
+####################################
+
+
+TOOL_SERVER_CONNECTIONS = PersistentConfig(
+    "TOOL_SERVER_CONNECTIONS",
+    "tool_server.connections",
+    [],
+)
+
 ####################################
 ####################################
 # WEBUI
 # WEBUI
 ####################################
 ####################################

+ 16 - 0
backend/open_webui/main.py

@@ -105,6 +105,8 @@ from open_webui.config import (
     OPENAI_API_CONFIGS,
     OPENAI_API_CONFIGS,
     # Direct Connections
     # Direct Connections
     ENABLE_DIRECT_CONNECTIONS,
     ENABLE_DIRECT_CONNECTIONS,
+    # Tool Server Configs
+    TOOL_SERVER_CONNECTIONS,
     # Code Execution
     # Code Execution
     ENABLE_CODE_EXECUTION,
     ENABLE_CODE_EXECUTION,
     CODE_EXECUTION_ENGINE,
     CODE_EXECUTION_ENGINE,
@@ -356,6 +358,7 @@ from open_webui.utils.access_control import has_access
 
 
 from open_webui.utils.auth import (
 from open_webui.utils.auth import (
     get_license_data,
     get_license_data,
+    get_http_authorization_cred,
     decode_token,
     decode_token,
     get_admin_user,
     get_admin_user,
     get_verified_user,
     get_verified_user,
@@ -478,6 +481,15 @@ app.state.config.OPENAI_API_CONFIGS = OPENAI_API_CONFIGS
 
 
 app.state.OPENAI_MODELS = {}
 app.state.OPENAI_MODELS = {}
 
 
+########################################
+#
+# TOOL SERVERS
+#
+########################################
+
+app.state.config.TOOL_SERVER_CONNECTIONS = TOOL_SERVER_CONNECTIONS
+app.state.TOOL_SERVERS = []
+
 ########################################
 ########################################
 #
 #
 # DIRECT CONNECTIONS
 # DIRECT CONNECTIONS
@@ -864,6 +876,10 @@ async def commit_session_after_request(request: Request, call_next):
 @app.middleware("http")
 @app.middleware("http")
 async def check_url(request: Request, call_next):
 async def check_url(request: Request, call_next):
     start_time = int(time.time())
     start_time = int(time.time())
+    request.state.token = get_http_authorization_cred(
+        request.headers.get("Authorization")
+    )
+
     request.state.enable_api_key = app.state.config.ENABLE_API_KEY
     request.state.enable_api_key = app.state.config.ENABLE_API_KEY
     response = await call_next(request)
     response = await call_next(request)
     process_time = int(time.time()) - start_time
     process_time = int(time.time()) - start_time

+ 71 - 2
backend/open_webui/routers/configs.py

@@ -1,5 +1,5 @@
-from fastapi import APIRouter, Depends, Request
-from pydantic import BaseModel
+from fastapi import APIRouter, Depends, Request, HTTPException
+from pydantic import BaseModel, ConfigDict
 
 
 from typing import Optional
 from typing import Optional
 
 
@@ -7,6 +7,8 @@ from open_webui.utils.auth import get_admin_user, get_verified_user
 from open_webui.config import get_config, save_config
 from open_webui.config import get_config, save_config
 from open_webui.config import BannerModel
 from open_webui.config import BannerModel
 
 
+from open_webui.utils.tools import get_tool_server_data, get_tool_servers_data
+
 
 
 router = APIRouter()
 router = APIRouter()
 
 
@@ -66,6 +68,73 @@ async def set_direct_connections_config(
     }
     }
 
 
 
 
+############################
+# ToolServers Config
+############################
+
+
+class ToolServerConnection(BaseModel):
+    url: str
+    path: str
+    auth_type: Optional[str]
+    key: Optional[str]
+    config: Optional[dict]
+
+    model_config = ConfigDict(extra="allow")
+
+
+class ToolServersConfigForm(BaseModel):
+    TOOL_SERVER_CONNECTIONS: list[ToolServerConnection]
+
+
+@router.get("/tool_servers", response_model=ToolServersConfigForm)
+async def get_tool_servers_config(request: Request, user=Depends(get_admin_user)):
+    return {
+        "TOOL_SERVER_CONNECTIONS": request.app.state.config.TOOL_SERVER_CONNECTIONS,
+    }
+
+
+@router.post("/tool_servers", response_model=ToolServersConfigForm)
+async def set_tool_servers_config(
+    request: Request,
+    form_data: ToolServersConfigForm,
+    user=Depends(get_admin_user),
+):
+    request.app.state.config.TOOL_SERVER_CONNECTIONS = form_data.TOOL_SERVER_CONNECTIONS
+
+    request.app.state.TOOL_SERVERS = await get_tool_servers_data(
+        request.app.state.config.TOOL_SERVER_CONNECTIONS
+    )
+
+    return {
+        "TOOL_SERVER_CONNECTIONS": request.app.state.config.TOOL_SERVER_CONNECTIONS,
+    }
+
+
+@router.post("/tool_servers/verify")
+async def verify_tool_servers_config(
+    request: Request, form_data: ToolServerConnection, user=Depends(get_admin_user)
+):
+    """
+    Verify the connection to the tool server.
+    """
+    try:
+
+        token = None
+        if form_data.auth_type == "bearer":
+            token = form_data.key
+        elif form_data.auth_type == "session":
+            token = request.state.token.credentials
+
+        url = f"{form_data.url}/{form_data.path}"
+        return await get_tool_server_data(token, url)
+    except Exception as e:
+        raise HTTPException(
+            status_code=400,
+            detail=f"Failed to connect to the tool server: {str(e)}",
+        )
+
+
 ############################
 ############################
 # CodeInterpreterConfig
 # CodeInterpreterConfig
 ############################
 ############################

+ 13 - 1
backend/open_webui/routers/tools.py

@@ -18,6 +18,8 @@ from open_webui.utils.auth import get_admin_user, get_verified_user
 from open_webui.utils.access_control import has_access, has_permission
 from open_webui.utils.access_control import has_access, has_permission
 from open_webui.env import SRC_LOG_LEVELS
 from open_webui.env import SRC_LOG_LEVELS
 
 
+from open_webui.utils.tools import get_tool_servers_data
+
 log = logging.getLogger(__name__)
 log = logging.getLogger(__name__)
 log.setLevel(SRC_LOG_LEVELS["MAIN"])
 log.setLevel(SRC_LOG_LEVELS["MAIN"])
 
 
@@ -30,7 +32,17 @@ router = APIRouter()
 
 
 
 
 @router.get("/", response_model=list[ToolUserResponse])
 @router.get("/", response_model=list[ToolUserResponse])
-async def get_tools(user=Depends(get_verified_user)):
+async def get_tools(request: Request, user=Depends(get_verified_user)):
+
+    if not request.app.state.TOOL_SERVERS:
+        # If the tool servers are not set, we need to set them
+        # This is done only once when the server starts
+        # This is done to avoid loading the tool servers every time
+
+        request.app.state.TOOL_SERVERS = await get_tool_servers_data(
+            request.app.state.config.TOOL_SERVER_CONNECTIONS
+        )
+
     if user.role == "admin":
     if user.role == "admin":
         tools = Tools.get_tools()
         tools = Tools.get_tools()
     else:
     else:

+ 4 - 2
backend/open_webui/utils/auth.py

@@ -143,12 +143,14 @@ def create_api_key():
     return f"sk-{key}"
     return f"sk-{key}"
 
 
 
 
-def get_http_authorization_cred(auth_header: str):
+def get_http_authorization_cred(auth_header: Optional[str]):
+    if not auth_header:
+        return None
     try:
     try:
         scheme, credentials = auth_header.split(" ")
         scheme, credentials = auth_header.split(" ")
         return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
         return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
     except Exception:
     except Exception:
-        raise ValueError(ERROR_MESSAGES.INVALID_TOKEN)
+        return None
 
 
 
 
 def get_current_user(
 def get_current_user(

+ 260 - 2
backend/open_webui/utils/tools.py

@@ -2,9 +2,10 @@ import inspect
 import logging
 import logging
 import re
 import re
 import inspect
 import inspect
-import uuid
+import aiohttp
+import asyncio
 
 
-from typing import Any, Awaitable, Callable, get_type_hints
+from typing import Any, Awaitable, Callable, get_type_hints, Dict, List, Union
 from functools import update_wrapper, partial
 from functools import update_wrapper, partial
 
 
 
 
@@ -217,3 +218,260 @@ def get_tools_specs(tool_class: object) -> list[dict]:
     function_list = get_callable_attributes(tool_class)
     function_list = get_callable_attributes(tool_class)
     models = map(function_to_pydantic_model, function_list)
     models = map(function_to_pydantic_model, function_list)
     return [convert_to_openai_function(tool) for tool in models]
     return [convert_to_openai_function(tool) for tool in models]
+
+
+import copy
+
+
+def resolve_schema(schema, components):
+    """
+    Recursively resolves a JSON schema using OpenAPI components.
+    """
+    if not schema:
+        return {}
+
+    if "$ref" in schema:
+        ref_path = schema["$ref"]
+        ref_parts = ref_path.strip("#/").split("/")
+        resolved = components
+        for part in ref_parts[1:]:  # Skip the initial 'components'
+            resolved = resolved.get(part, {})
+        return resolve_schema(resolved, components)
+
+    resolved_schema = copy.deepcopy(schema)
+
+    # Recursively resolve inner schemas
+    if "properties" in resolved_schema:
+        for prop, prop_schema in resolved_schema["properties"].items():
+            resolved_schema["properties"][prop] = resolve_schema(
+                prop_schema, components
+            )
+
+    if "items" in resolved_schema:
+        resolved_schema["items"] = resolve_schema(resolved_schema["items"], components)
+
+    return resolved_schema
+
+
+def convert_openapi_to_tool_payload(openapi_spec):
+    """
+    Converts an OpenAPI specification into a custom tool payload structure.
+
+    Args:
+        openapi_spec (dict): The OpenAPI specification as a Python dict.
+
+    Returns:
+        list: A list of tool payloads.
+    """
+    tool_payload = []
+
+    for path, methods in openapi_spec.get("paths", {}).items():
+        for method, operation in methods.items():
+            tool = {
+                "type": "function",
+                "name": operation.get("operationId"),
+                "description": operation.get("summary", "No description available."),
+                "parameters": {"type": "object", "properties": {}, "required": []},
+            }
+
+            # Extract path and query parameters
+            for param in operation.get("parameters", []):
+                param_name = param["name"]
+                param_schema = param.get("schema", {})
+                tool["parameters"]["properties"][param_name] = {
+                    "type": param_schema.get("type"),
+                    "description": param_schema.get("description", ""),
+                }
+                if param.get("required"):
+                    tool["parameters"]["required"].append(param_name)
+
+            # Extract and resolve requestBody if available
+            request_body = operation.get("requestBody")
+            if request_body:
+                content = request_body.get("content", {})
+                json_schema = content.get("application/json", {}).get("schema")
+                if json_schema:
+                    resolved_schema = resolve_schema(
+                        json_schema, openapi_spec.get("components", {})
+                    )
+
+                    if resolved_schema.get("properties"):
+                        tool["parameters"]["properties"].update(
+                            resolved_schema["properties"]
+                        )
+                        if "required" in resolved_schema:
+                            tool["parameters"]["required"] = list(
+                                set(
+                                    tool["parameters"]["required"]
+                                    + resolved_schema["required"]
+                                )
+                            )
+                    elif resolved_schema.get("type") == "array":
+                        tool["parameters"] = resolved_schema  # special case for array
+
+            tool_payload.append(tool)
+
+    return tool_payload
+
+
+async def get_tool_server_data(token: str, url: str) -> Dict[str, Any]:
+    headers = {
+        "Accept": "application/json",
+        "Content-Type": "application/json",
+    }
+    if token:
+        headers["Authorization"] = f"Bearer {token}"
+
+    error = None
+    try:
+        async with aiohttp.ClientSession() as session:
+            async with session.get(url, headers=headers) as response:
+                if response.status != 200:
+                    error_body = await response.json()
+                    raise Exception(error_body)
+                res = await response.json()
+    except Exception as err:
+        print("Error:", err)
+        if isinstance(err, dict) and "detail" in err:
+            error = err["detail"]
+        else:
+            error = str(err)
+        raise Exception(error)
+
+    data = {
+        "openapi": res,
+        "info": res.get("info", {}),
+        "specs": convert_openapi_to_tool_payload(res),
+    }
+
+    print("Fetched data:", data)
+    return data
+
+
+async def get_tool_servers_data(servers: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
+    enabled_servers = [
+        server for server in servers if server.get("config", {}).get("enable")
+    ]
+
+    urls = [
+        (
+            server,
+            f"{server.get('url')}/{server.get('path', 'openapi.json')}",
+            server.get("key", ""),
+        )
+        for server in enabled_servers
+    ]
+
+    tasks = [get_tool_server_data(token, url) for _, url, token in urls]
+
+    results: List[Dict[str, Any]] = []
+
+    responses = await asyncio.gather(*tasks, return_exceptions=True)
+
+    for (server, _, _), response in zip(urls, responses):
+        if isinstance(response, Exception):
+            url_path = server.get("path", "openapi.json")
+            full_url = f"{server.get('url')}/{url_path}"
+            print(f"Failed to connect to {full_url} OpenAPI tool server")
+        else:
+            results.append(
+                {
+                    "url": server.get("url"),
+                    "openapi": response["openapi"],
+                    "info": response["info"],
+                    "specs": response["specs"],
+                }
+            )
+
+    return results
+
+
+async def execute_tool_server(
+    token: str, url: str, name: str, params: Dict[str, Any], server_data: Dict[str, Any]
+) -> Any:
+    error = None
+    try:
+        openapi = server_data.get("openapi", {})
+        paths = openapi.get("paths", {})
+
+        matching_route = None
+        for route_path, methods in paths.items():
+            for http_method, operation in methods.items():
+                if isinstance(operation, dict) and operation.get("operationId") == name:
+                    matching_route = (route_path, methods)
+                    break
+            if matching_route:
+                break
+
+        if not matching_route:
+            raise Exception(f"No matching route found for operationId: {name}")
+
+        route_path, methods = matching_route
+
+        method_entry = None
+        for http_method, operation in methods.items():
+            if operation.get("operationId") == name:
+                method_entry = (http_method.lower(), operation)
+                break
+
+        if not method_entry:
+            raise Exception(f"No matching method found for operationId: {name}")
+
+        http_method, operation = method_entry
+
+        path_params = {}
+        query_params = {}
+        body_params = {}
+
+        for param in operation.get("parameters", []):
+            param_name = param["name"]
+            param_in = param["in"]
+            if param_name in params:
+                if param_in == "path":
+                    path_params[param_name] = params[param_name]
+                elif param_in == "query":
+                    query_params[param_name] = params[param_name]
+
+        final_url = f"{url}{route_path}"
+        for key, value in path_params.items():
+            final_url = final_url.replace(f"{{{key}}}", str(value))
+
+        if query_params:
+            query_string = "&".join(f"{k}={v}" for k, v in query_params.items())
+            final_url = f"{final_url}?{query_string}"
+
+        if operation.get("requestBody", {}).get("content"):
+            if params:
+                body_params = params
+            else:
+                raise Exception(
+                    f"Request body expected for operation '{name}' but none found."
+                )
+
+        headers = {"Content-Type": "application/json"}
+
+        if token:
+            headers["Authorization"] = f"Bearer {token}"
+
+        async with aiohttp.ClientSession() as session:
+            request_method = getattr(session, http_method.lower())
+
+            if http_method in ["post", "put", "patch"]:
+                async with request_method(
+                    final_url, json=body_params, headers=headers
+                ) as response:
+                    if response.status >= 400:
+                        text = await response.text()
+                        raise Exception(f"HTTP error {response.status}: {text}")
+                    return await response.json()
+            else:
+                async with request_method(final_url, headers=headers) as response:
+                    if response.status >= 400:
+                        text = await response.text()
+                        raise Exception(f"HTTP error {response.status}: {text}")
+                    return await response.json()
+
+    except Exception as err:
+        error = str(err)
+        print("API Request Error:", error)
+        return {"error": error}

+ 87 - 0
src/lib/apis/configs/index.ts

@@ -115,6 +115,93 @@ export const setDirectConnectionsConfig = async (token: string, config: object)
 	return res;
 	return res;
 };
 };
 
 
+export const getToolServerConnections = async (token: string) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/configs/tool_servers`, {
+		method: 'GET',
+		headers: {
+			'Content-Type': 'application/json',
+			Authorization: `Bearer ${token}`
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.catch((err) => {
+			console.log(err);
+			error = err.detail;
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+export const setToolServerConnections = async (token: string, connections: object) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/configs/tool_servers`, {
+		method: 'POST',
+		headers: {
+			'Content-Type': 'application/json',
+			Authorization: `Bearer ${token}`
+		},
+		body: JSON.stringify({
+			...connections
+		})
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.catch((err) => {
+			console.log(err);
+			error = err.detail;
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+export const verifyToolServerConnection = async (token: string, connection: object) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/configs/tool_servers/verify`, {
+		method: 'POST',
+		headers: {
+			'Content-Type': 'application/json',
+			Authorization: `Bearer ${token}`
+		},
+		body: JSON.stringify({
+			...connection
+		})
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.catch((err) => {
+			console.log(err);
+			error = err.detail;
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
 export const getCodeExecutionConfig = async (token: string) => {
 export const getCodeExecutionConfig = async (token: string) => {
 	let error = null;
 	let error = null;
 
 

+ 2 - 2
src/lib/apis/index.ts

@@ -306,11 +306,11 @@ export const getToolServersData = async (i18n, servers: object[]) => {
 				.map(async (server) => {
 				.map(async (server) => {
 					const data = await getToolServerData(
 					const data = await getToolServerData(
 						server?.key,
 						server?.key,
-						server?.url + (server?.path ?? '/openapi.json')
+						server?.url + '/' + (server?.path ?? 'openapi.json')
 					).catch((err) => {
 					).catch((err) => {
 						toast.error(
 						toast.error(
 							i18n.t(`Failed to connect to {{URL}} OpenAPI tool server`, {
 							i18n.t(`Failed to connect to {{URL}} OpenAPI tool server`, {
-								URL: server?.url + (server?.path ?? '/openapi.json')
+								URL: server?.url + '/' + (server?.path ?? 'openapi.json')
 							})
 							})
 						);
 						);
 						return null;
 						return null;

+ 89 - 16
src/lib/components/AddServerModal.svelte

@@ -15,6 +15,8 @@
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import Switch from '$lib/components/common/Switch.svelte';
 	import Switch from '$lib/components/common/Switch.svelte';
 	import Tags from './common/Tags.svelte';
 	import Tags from './common/Tags.svelte';
+	import { getToolServerData } from '$lib/apis';
+	import { verifyToolServerConnection } from '$lib/apis/configs';
 
 
 	export let onSubmit: Function = () => {};
 	export let onSubmit: Function = () => {};
 	export let onDelete: Function = () => {};
 	export let onDelete: Function = () => {};
@@ -22,10 +24,12 @@
 	export let show = false;
 	export let show = false;
 	export let edit = false;
 	export let edit = false;
 
 
+	export let direct = false;
+
 	export let connection = null;
 	export let connection = null;
 
 
 	let url = '';
 	let url = '';
-	let path = '/openapi.json';
+	let path = 'openapi.json';
 
 
 	let auth_type = 'bearer';
 	let auth_type = 'bearer';
 	let key = '';
 	let key = '';
@@ -34,6 +38,49 @@
 
 
 	let loading = false;
 	let loading = false;
 
 
+	const verifyHandler = async () => {
+		if (url === '') {
+			toast.error($i18n.t('Please enter a valid URL'));
+			return;
+		}
+
+		if (path === '') {
+			toast.error($i18n.t('Please enter a valid path'));
+			return;
+		}
+
+		if (direct) {
+			const res = await getToolServerData(
+				auth_type === 'bearer' ? key : localStorage.token,
+				`${url}/${path}`
+			).catch((err) => {
+				toast.error($i18n.t('Connection failed'));
+			});
+
+			if (res) {
+				toast.success($i18n.t('Connection successful'));
+				console.debug('Connection successful', res);
+			}
+		} else {
+			const res = await verifyToolServerConnection(localStorage.token, {
+				url,
+				path,
+				auth_type,
+				key,
+				config: {
+					enable: enable
+				}
+			}).catch((err) => {
+				toast.error($i18n.t('Connection failed'));
+			});
+
+			if (res) {
+				toast.success($i18n.t('Connection successful'));
+				console.debug('Connection successful', res);
+			}
+		}
+	};
+
 	const submitHandler = async () => {
 	const submitHandler = async () => {
 		loading = true;
 		loading = true;
 
 
@@ -56,7 +103,7 @@
 		show = false;
 		show = false;
 
 
 		url = '';
 		url = '';
-		path = '/openapi.json';
+		path = 'openapi.json';
 		key = '';
 		key = '';
 		auth_type = 'bearer';
 		auth_type = 'bearer';
 
 
@@ -66,7 +113,7 @@
 	const init = () => {
 	const init = () => {
 		if (connection) {
 		if (connection) {
 			url = connection.url;
 			url = connection.url;
-			path = connection?.path ?? '/openapi.json';
+			path = connection?.path ?? 'openapi.json';
 
 
 			auth_type = connection?.auth_type ?? 'bearer';
 			auth_type = connection?.auth_type ?? 'bearer';
 			key = connection?.key ?? '';
 			key = connection?.key ?? '';
@@ -125,20 +172,53 @@
 					<div class="px-1">
 					<div class="px-1">
 						<div class="flex gap-2">
 						<div class="flex gap-2">
 							<div class="flex flex-col w-full">
 							<div class="flex flex-col w-full">
-								<div class=" mb-0.5 text-xs text-gray-500">{$i18n.t('URL')}</div>
+								<div class="flex justify-between mb-0.5">
+									<div class=" text-xs text-gray-500">{$i18n.t('URL')}</div>
+								</div>
 
 
-								<div class="flex-1">
+								<div class="flex flex-1 items-center">
 									<input
 									<input
-										class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
+										class="w-full flex-1 text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
 										type="text"
 										type="text"
 										bind:value={url}
 										bind:value={url}
 										placeholder={$i18n.t('API Base URL')}
 										placeholder={$i18n.t('API Base URL')}
 										autocomplete="off"
 										autocomplete="off"
 										required
 										required
 									/>
 									/>
+
+									<Tooltip
+										content={$i18n.t('Verify Connection')}
+										className="shrink-0 flex items-center mr-1"
+									>
+										<button
+											class="self-center p-1 bg-transparent hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition"
+											on:click={() => {
+												verifyHandler();
+											}}
+											type="button"
+										>
+											<svg
+												xmlns="http://www.w3.org/2000/svg"
+												viewBox="0 0 20 20"
+												fill="currentColor"
+												class="w-4 h-4"
+											>
+												<path
+													fill-rule="evenodd"
+													d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
+													clip-rule="evenodd"
+												/>
+											</svg>
+										</button>
+									</Tooltip>
+
+									<Tooltip content={enable ? $i18n.t('Enabled') : $i18n.t('Disabled')}>
+										<Switch bind:state={enable} />
+									</Tooltip>
 								</div>
 								</div>
 
 
-								<div class="flex-1">
+								<div class="flex-1 flex items-center">
+									<div class="text-sm">/</div>
 									<input
 									<input
 										class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
 										class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
 										type="text"
 										type="text"
@@ -149,18 +229,11 @@
 									/>
 									/>
 								</div>
 								</div>
 							</div>
 							</div>
-
-							<div class="flex flex-col shrink-0 self-start">
-								<Tooltip content={enable ? $i18n.t('Enabled') : $i18n.t('Disabled')}>
-									<Switch bind:state={enable} />
-								</Tooltip>
-							</div>
 						</div>
 						</div>
 
 
 						<div class="text-xs text-gray-500 mt-1">
 						<div class="text-xs text-gray-500 mt-1">
-							{$i18n.t(`WebUI will make requests to "{{url}}{{path}}"`, {
-								url: url,
-								path: path
+							{$i18n.t(`WebUI will make requests to "{{url}}"`, {
+								url: `${url}/${path}`
 							})}
 							})}
 						</div>
 						</div>
 
 

+ 54 - 338
src/lib/components/admin/Settings/Tools.svelte

@@ -1,317 +1,66 @@
 <script lang="ts">
 <script lang="ts">
 	import { toast } from 'svelte-sonner';
 	import { toast } from 'svelte-sonner';
 	import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
 	import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
+	import { getModels as _getModels } from '$lib/apis';
 
 
 	const dispatch = createEventDispatcher();
 	const dispatch = createEventDispatcher();
+	const i18n = getContext('i18n');
 
 
-	import { getOllamaConfig, updateOllamaConfig } from '$lib/apis/ollama';
-	import { getOpenAIConfig, updateOpenAIConfig, getOpenAIModels } from '$lib/apis/openai';
-	import { getModels as _getModels } from '$lib/apis';
-	import { getDirectConnectionsConfig, setDirectConnectionsConfig } from '$lib/apis/configs';
-
-	import { config, models, settings, user } from '$lib/stores';
+	import { models, settings, user } from '$lib/stores';
 
 
 	import Switch from '$lib/components/common/Switch.svelte';
 	import Switch from '$lib/components/common/Switch.svelte';
 	import Spinner from '$lib/components/common/Spinner.svelte';
 	import Spinner from '$lib/components/common/Spinner.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import Plus from '$lib/components/icons/Plus.svelte';
 	import Plus from '$lib/components/icons/Plus.svelte';
+	import Connection from '$lib/components/chat/Settings/Tools/Connection.svelte';
 
 
-	import OpenAIConnection from './Connections/OpenAIConnection.svelte';
-	import AddConnectionModal from '$lib/components/AddConnectionModal.svelte';
-	import OllamaConnection from './Connections/OllamaConnection.svelte';
-
-	const i18n = getContext('i18n');
-
-	const getModels = async () => {
-		const models = await _getModels(
-			localStorage.token,
-			$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
-		);
-		return models;
-	};
-
-	// External
-	let OLLAMA_BASE_URLS = [''];
-	let OLLAMA_API_CONFIGS = {};
-
-	let OPENAI_API_KEYS = [''];
-	let OPENAI_API_BASE_URLS = [''];
-	let OPENAI_API_CONFIGS = {};
-
-	let ENABLE_OPENAI_API: null | boolean = null;
-	let ENABLE_OLLAMA_API: null | boolean = null;
-
-	let directConnectionsConfig = null;
-
-	let pipelineUrls = {};
-	let showAddOpenAIConnectionModal = false;
-	let showAddOllamaConnectionModal = false;
-
-	const updateOpenAIHandler = async () => {
-		if (ENABLE_OPENAI_API !== null) {
-			// Remove trailing slashes
-			OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.map((url) => url.replace(/\/$/, ''));
-
-			// Check if API KEYS length is same than API URLS length
-			if (OPENAI_API_KEYS.length !== OPENAI_API_BASE_URLS.length) {
-				// if there are more keys than urls, remove the extra keys
-				if (OPENAI_API_KEYS.length > OPENAI_API_BASE_URLS.length) {
-					OPENAI_API_KEYS = OPENAI_API_KEYS.slice(0, OPENAI_API_BASE_URLS.length);
-				}
-
-				// if there are more urls than keys, add empty keys
-				if (OPENAI_API_KEYS.length < OPENAI_API_BASE_URLS.length) {
-					const diff = OPENAI_API_BASE_URLS.length - OPENAI_API_KEYS.length;
-					for (let i = 0; i < diff; i++) {
-						OPENAI_API_KEYS.push('');
-					}
-				}
-			}
-
-			const res = await updateOpenAIConfig(localStorage.token, {
-				ENABLE_OPENAI_API: ENABLE_OPENAI_API,
-				OPENAI_API_BASE_URLS: OPENAI_API_BASE_URLS,
-				OPENAI_API_KEYS: OPENAI_API_KEYS,
-				OPENAI_API_CONFIGS: OPENAI_API_CONFIGS
-			}).catch((error) => {
-				toast.error(`${error}`);
-			});
-
-			if (res) {
-				toast.success($i18n.t('OpenAI API settings updated'));
-				await models.set(await getModels());
-			}
-		}
-	};
+	import AddServerModal from '$lib/components/AddServerModal.svelte';
 
 
-	const updateOllamaHandler = async () => {
-		if (ENABLE_OLLAMA_API !== null) {
-			// Remove trailing slashes
-			OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.map((url) => url.replace(/\/$/, ''));
+	export let saveSettings: Function;
 
 
-			const res = await updateOllamaConfig(localStorage.token, {
-				ENABLE_OLLAMA_API: ENABLE_OLLAMA_API,
-				OLLAMA_BASE_URLS: OLLAMA_BASE_URLS,
-				OLLAMA_API_CONFIGS: OLLAMA_API_CONFIGS
-			}).catch((error) => {
-				toast.error(`${error}`);
-			});
+	let servers = null;
+	let showConnectionModal = false;
 
 
-			if (res) {
-				toast.success($i18n.t('Ollama API settings updated'));
-				await models.set(await getModels());
-			}
-		}
+	const addConnectionHandler = async (server) => {
+		servers = [...servers, server];
+		await updateHandler();
 	};
 	};
 
 
-	const updateDirectConnectionsHandler = async () => {
-		const res = await setDirectConnectionsConfig(localStorage.token, directConnectionsConfig).catch(
-			(error) => {
-				toast.error(`${error}`);
-			}
-		);
-
-		if (res) {
-			toast.success($i18n.t('Direct Connections settings updated'));
-			await models.set(await getModels());
-		}
-	};
-
-	const addOpenAIConnectionHandler = async (connection) => {
-		OPENAI_API_BASE_URLS = [...OPENAI_API_BASE_URLS, connection.url];
-		OPENAI_API_KEYS = [...OPENAI_API_KEYS, connection.key];
-		OPENAI_API_CONFIGS[OPENAI_API_BASE_URLS.length - 1] = connection.config;
-
-		await updateOpenAIHandler();
-	};
-
-	const addOllamaConnectionHandler = async (connection) => {
-		OLLAMA_BASE_URLS = [...OLLAMA_BASE_URLS, connection.url];
-		OLLAMA_API_CONFIGS[OLLAMA_BASE_URLS.length - 1] = {
-			...connection.config,
-			key: connection.key
-		};
-
-		await updateOllamaHandler();
-	};
+	const updateHandler = async () => {};
 
 
 	onMount(async () => {
 	onMount(async () => {
-		if ($user?.role === 'admin') {
-			let ollamaConfig = {};
-			let openaiConfig = {};
-
-			await Promise.all([
-				(async () => {
-					ollamaConfig = await getOllamaConfig(localStorage.token);
-				})(),
-				(async () => {
-					openaiConfig = await getOpenAIConfig(localStorage.token);
-				})(),
-				(async () => {
-					directConnectionsConfig = await getDirectConnectionsConfig(localStorage.token);
-				})()
-			]);
-
-			ENABLE_OPENAI_API = openaiConfig.ENABLE_OPENAI_API;
-			ENABLE_OLLAMA_API = ollamaConfig.ENABLE_OLLAMA_API;
-
-			OPENAI_API_BASE_URLS = openaiConfig.OPENAI_API_BASE_URLS;
-			OPENAI_API_KEYS = openaiConfig.OPENAI_API_KEYS;
-			OPENAI_API_CONFIGS = openaiConfig.OPENAI_API_CONFIGS;
-
-			OLLAMA_BASE_URLS = ollamaConfig.OLLAMA_BASE_URLS;
-			OLLAMA_API_CONFIGS = ollamaConfig.OLLAMA_API_CONFIGS;
-
-			if (ENABLE_OPENAI_API) {
-				// get url and idx
-				for (const [idx, url] of OPENAI_API_BASE_URLS.entries()) {
-					if (!OPENAI_API_CONFIGS[idx]) {
-						// Legacy support, url as key
-						OPENAI_API_CONFIGS[idx] = OPENAI_API_CONFIGS[url] || {};
-					}
-				}
-
-				OPENAI_API_BASE_URLS.forEach(async (url, idx) => {
-					OPENAI_API_CONFIGS[idx] = OPENAI_API_CONFIGS[idx] || {};
-					if (!(OPENAI_API_CONFIGS[idx]?.enable ?? true)) {
-						return;
-					}
-					const res = await getOpenAIModels(localStorage.token, idx);
-					if (res.pipelines) {
-						pipelineUrls[url] = true;
-					}
-				});
-			}
-
-			if (ENABLE_OLLAMA_API) {
-				for (const [idx, url] of OLLAMA_BASE_URLS.entries()) {
-					if (!OLLAMA_API_CONFIGS[idx]) {
-						OLLAMA_API_CONFIGS[idx] = OLLAMA_API_CONFIGS[url] || {};
-					}
-				}
-			}
-		}
+		servers = [];
 	});
 	});
-
-	const submitHandler = async () => {
-		updateOpenAIHandler();
-		updateOllamaHandler();
-		updateDirectConnectionsHandler();
-
-		dispatch('save');
-	};
 </script>
 </script>
 
 
-<AddConnectionModal
-	bind:show={showAddOpenAIConnectionModal}
-	onSubmit={addOpenAIConnectionHandler}
-/>
-
-<AddConnectionModal
-	ollama
-	bind:show={showAddOllamaConnectionModal}
-	onSubmit={addOllamaConnectionHandler}
-/>
+<AddServerModal bind:show={showConnectionModal} onSubmit={addConnectionHandler} />
 
 
-<form class="flex flex-col h-full justify-between text-sm" on:submit|preventDefault={submitHandler}>
+<form
+	class="flex flex-col h-full justify-between text-sm"
+	on:submit|preventDefault={() => {
+		updateHandler();
+	}}
+>
 	<div class=" overflow-y-scroll scrollbar-hidden h-full">
 	<div class=" overflow-y-scroll scrollbar-hidden h-full">
-		{#if ENABLE_OPENAI_API !== null && ENABLE_OLLAMA_API !== null && directConnectionsConfig !== null}
-			<div class="my-2">
-				<div class="mt-2 space-y-2 pr-1.5">
-					<div class="flex justify-between items-center text-sm">
-						<div class="  font-medium">{$i18n.t('OpenAI API')}</div>
-
-						<div class="flex items-center">
-							<div class="">
-								<Switch
-									bind:state={ENABLE_OPENAI_API}
-									on:change={async () => {
-										updateOpenAIHandler();
-									}}
-								/>
-							</div>
-						</div>
-					</div>
-
-					{#if ENABLE_OPENAI_API}
-						<hr class=" border-gray-100 dark:border-gray-850" />
-
-						<div class="">
-							<div class="flex justify-between items-center">
-								<div class="font-medium">{$i18n.t('Manage OpenAI API Connections')}</div>
-
-								<Tooltip content={$i18n.t(`Add Connection`)}>
-									<button
-										class="px-1"
-										on:click={() => {
-											showAddOpenAIConnectionModal = true;
-										}}
-										type="button"
-									>
-										<Plus />
-									</button>
-								</Tooltip>
-							</div>
-
-							<div class="flex flex-col gap-1.5 mt-1.5">
-								{#each OPENAI_API_BASE_URLS as url, idx}
-									<OpenAIConnection
-										pipeline={pipelineUrls[url] ? true : false}
-										bind:url
-										bind:key={OPENAI_API_KEYS[idx]}
-										bind:config={OPENAI_API_CONFIGS[idx]}
-										onSubmit={() => {
-											updateOpenAIHandler();
-										}}
-										onDelete={() => {
-											OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.filter(
-												(url, urlIdx) => idx !== urlIdx
-											);
-											OPENAI_API_KEYS = OPENAI_API_KEYS.filter((key, keyIdx) => idx !== keyIdx);
-
-											let newConfig = {};
-											OPENAI_API_BASE_URLS.forEach((url, newIdx) => {
-												newConfig[newIdx] = OPENAI_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
-											});
-											OPENAI_API_CONFIGS = newConfig;
-											updateOpenAIHandler();
-										}}
-									/>
-								{/each}
-							</div>
-						</div>
-					{/if}
-				</div>
-			</div>
+		{#if servers !== null}
+			<div class="">
+				<div class="mb-3">
+					<div class=" mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
 
 
-			<hr class=" border-gray-100 dark:border-gray-850" />
-
-			<div class="pr-1.5 my-2">
-				<div class="flex justify-between items-center text-sm mb-2">
-					<div class="  font-medium">{$i18n.t('Ollama API')}</div>
-
-					<div class="mt-1">
-						<Switch
-							bind:state={ENABLE_OLLAMA_API}
-							on:change={async () => {
-								updateOllamaHandler();
-							}}
-						/>
-					</div>
-				</div>
-
-				{#if ENABLE_OLLAMA_API}
 					<hr class=" border-gray-100 dark:border-gray-850 my-2" />
 					<hr class=" border-gray-100 dark:border-gray-850 my-2" />
 
 
-					<div class="">
-						<div class="flex justify-between items-center">
-							<div class="font-medium">{$i18n.t('Manage Ollama API Connections')}</div>
+					<div class="mb-2.5 flex flex-col w-full justify-between">
+						<!-- {$i18n.t(`Failed to connect to {{URL}} OpenAPI tool server`, {
+							URL: 'server?.url'
+						})} -->
+						<div class="flex justify-between items-center mb-0.5">
+							<div class="font-medium">{$i18n.t('Manage Tool Servers')}</div>
 
 
 							<Tooltip content={$i18n.t(`Add Connection`)}>
 							<Tooltip content={$i18n.t(`Add Connection`)}>
 								<button
 								<button
 									class="px-1"
 									class="px-1"
 									on:click={() => {
 									on:click={() => {
-										showAddOllamaConnectionModal = true;
+										showConnectionModal = true;
 									}}
 									}}
 									type="button"
 									type="button"
 								>
 								>
@@ -320,68 +69,35 @@
 							</Tooltip>
 							</Tooltip>
 						</div>
 						</div>
 
 
-						<div class="flex w-full gap-1.5">
-							<div class="flex-1 flex flex-col gap-1.5 mt-1.5">
-								{#each OLLAMA_BASE_URLS as url, idx}
-									<OllamaConnection
-										bind:url
-										bind:config={OLLAMA_API_CONFIGS[idx]}
-										{idx}
-										onSubmit={() => {
-											updateOllamaHandler();
-										}}
-										onDelete={() => {
-											OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url, urlIdx) => idx !== urlIdx);
-
-											let newConfig = {};
-											OLLAMA_BASE_URLS.forEach((url, newIdx) => {
-												newConfig[newIdx] = OLLAMA_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
-											});
-											OLLAMA_API_CONFIGS = newConfig;
-										}}
-									/>
-								{/each}
-							</div>
+						<div class="flex flex-col gap-1.5">
+							{#each servers as server, idx}
+								<Connection
+									bind:connection={server}
+									onSubmit={() => {
+										updateHandler();
+									}}
+									onDelete={() => {
+										servers = servers.filter((_, i) => i !== idx);
+										updateHandler();
+									}}
+								/>
+							{/each}
 						</div>
 						</div>
 
 
-						<div class="mt-1 text-xs text-gray-400 dark:text-gray-500">
-							{$i18n.t('Trouble accessing Ollama?')}
-							<a
-								class=" text-gray-300 font-medium underline"
-								href="https://github.com/open-webui/open-webui#troubleshooting"
-								target="_blank"
-							>
-								{$i18n.t('Click here for help.')}
-							</a>
+						<div class="my-1.5">
+							<div class="text-xs text-gray-500">
+								{$i18n.t('Connect to your own OpenAPI compatible external tool servers.')}
+							</div>
 						</div>
 						</div>
 					</div>
 					</div>
-				{/if}
-			</div>
 
 
-			<hr class=" border-gray-100 dark:border-gray-850" />
+					<!-- <div class="mb-2.5 flex w-full justify-between">
+						<div class=" text-xs font-medium">{$i18n.t('Arena Models')}</div>
 
 
-			<div class="pr-1.5 my-2">
-				<div class="flex justify-between items-center text-sm">
-					<div class="  font-medium">{$i18n.t('Direct Connections')}</div>
-
-					<div class="flex items-center">
-						<div class="">
-							<Switch
-								bind:state={directConnectionsConfig.ENABLE_DIRECT_CONNECTIONS}
-								on:change={async () => {
-									updateDirectConnectionsHandler();
-								}}
-							/>
-						</div>
-					</div>
-				</div>
-
-				<div class="mt-1.5">
-					<div class="text-xs text-gray-500">
-						{$i18n.t(
-							'Direct Connections allow users to connect to their own OpenAI compatible API endpoints.'
-						)}
-					</div>
+						<Tooltip content={$i18n.t(`Message rating should be enabled to use this feature`)}>
+							<Switch bind:state={evaluationConfig.ENABLE_EVALUATION_ARENA_MODELS} />
+						</Tooltip>
+					</div> -->
 				</div>
 				</div>
 			</div>
 			</div>
 		{:else}
 		{:else}

+ 2 - 1
src/lib/components/chat/Settings/Tools.svelte

@@ -39,7 +39,7 @@
 	});
 	});
 </script>
 </script>
 
 
-<AddServerModal bind:show={showConnectionModal} onSubmit={addConnectionHandler} />
+<AddServerModal bind:show={showConnectionModal} onSubmit={addConnectionHandler} direct />
 
 
 <form
 <form
 	class="flex flex-col h-full justify-between text-sm"
 	class="flex flex-col h-full justify-between text-sm"
@@ -75,6 +75,7 @@
 							{#each servers as server, idx}
 							{#each servers as server, idx}
 								<Connection
 								<Connection
 									bind:connection={server}
 									bind:connection={server}
+									direct
 									onSubmit={() => {
 									onSubmit={() => {
 										updateHandler();
 										updateHandler();
 									}}
 									}}

+ 3 - 3
src/lib/components/chat/Settings/Tools/Connection.svelte

@@ -12,6 +12,7 @@
 	export let onSubmit = () => {};
 	export let onSubmit = () => {};
 
 
 	export let connection = null;
 	export let connection = null;
+	export let direct = false;
 
 
 	let showConfigModal = false;
 	let showConfigModal = false;
 	let showDeleteConfirmDialog = false;
 	let showDeleteConfirmDialog = false;
@@ -42,9 +43,8 @@
 <div class="flex w-full gap-2 items-center">
 <div class="flex w-full gap-2 items-center">
 	<Tooltip
 	<Tooltip
 		className="w-full relative"
 		className="w-full relative"
-		content={$i18n.t(`WebUI will make requests to "{{url}}{{path}}"`, {
-			url: connection?.url,
-			path: connection?.path ?? '/openapi.json'
+		content={$i18n.t(`WebUI will make requests to "{{url}}"`, {
+			url: `${connection?.url}/${connection?.path ?? 'openapi.json'}`
 		})}
 		})}
 		placement="top-start"
 		placement="top-start"
 	>
 	>