This commit is contained in:
Timothy Jaeryang Baek
2026-05-09 06:56:22 +09:00
parent 2ba6b423aa
commit 5b80932e59
3 changed files with 154 additions and 53 deletions
+61 -16
View File
@@ -1,4 +1,5 @@
import base64
import copy
import inspect
import logging
import re
@@ -7,6 +8,7 @@ import aiohttp
import asyncio
import yaml
import json
from urllib.parse import quote, urlencode
from pydantic import BaseModel
from pydantic.fields import FieldInfo
@@ -100,7 +102,6 @@ from open_webui.tools.builtin import (
delete_calendar_event,
)
import copy
from open_webui.utils.access_control import has_permission
log = logging.getLogger(__name__)
@@ -728,7 +729,6 @@ def clean_properties(schema: dict):
def clean_openai_tool_schema(spec: dict) -> dict:
import copy
cleaned_spec = copy.deepcopy(spec)
@@ -761,6 +761,11 @@ def get_tool_specs(tool_module: object) -> list[dict]:
return specs
# Valid HTTP methods per OpenAPI 3.x – used to skip extension keys (x-*)
# and non-operation path-item fields (summary, description, servers, parameters).
OPENAPI_HTTP_METHODS = {'get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'}
def resolve_schema(schema, components, resolved_schemas=None):
"""
Recursively resolves a JSON schema using OpenAPI components.
@@ -813,7 +818,18 @@ def convert_openapi_to_tool_payload(openapi_spec):
tool_payload = []
for path, methods in openapi_spec.get('paths', {}).items():
if not isinstance(methods, dict):
continue
# Path-level parameters apply to all operations under this path
# unless overridden at the operation level (matched by name + in).
path_level_params = methods.get('parameters', [])
if not isinstance(path_level_params, list):
path_level_params = []
for method, operation in methods.items():
if method not in OPENAPI_HTTP_METHODS:
continue
if not isinstance(operation, dict):
continue
if operation.get('operationId'):
@@ -826,7 +842,21 @@ def convert_openapi_to_tool_payload(openapi_spec):
'parameters': {'type': 'object', 'properties': {}, 'required': []},
}
for param in operation.get('parameters', []):
# Merge path-level and operation-level parameters.
# Operation-level params override path-level params with the
# same (name, in) pair per the OpenAPI spec.
op_params = operation.get('parameters', [])
if not isinstance(op_params, list):
op_params = []
merged_params = {}
for param in path_level_params:
if isinstance(param, dict) and param.get('name'):
merged_params[(param['name'], param.get('in', ''))] = param
for param in op_params:
if isinstance(param, dict) and param.get('name'):
merged_params[(param['name'], param.get('in', ''))] = param
for param in merged_params.values():
param_name = param.get('name')
if not param_name:
continue
@@ -1169,22 +1199,17 @@ async def get_tool_server_data(url: str, headers: Optional[dict]) -> Dict[str, A
error_body = await response.json()
raise Exception(error_body)
text_content = None
text_content = await response.text()
# Check if URL ends with .yaml or .yml to determine format
if url.lower().endswith(('.yaml', '.yml')):
text_content = await response.text()
res = yaml.safe_load(text_content)
else:
text_content = await response.text()
try:
res = json.loads(text_content)
except json.JSONDecodeError:
try:
res = json.loads(text_content)
except json.JSONDecodeError:
# Fall back to YAML for non-.yml URLs that aren't valid JSON
res = yaml.safe_load(text_content)
except Exception as e:
raise e
except Exception as err:
log.exception(f'Could not fetch tool server spec from {url}')
@@ -1312,7 +1337,11 @@ async def execute_tool_server(
matching_route = None
for route_path, methods in paths.items():
if not isinstance(methods, dict):
continue
for http_method, operation in methods.items():
if http_method not in OPENAPI_HTTP_METHODS:
continue
if isinstance(operation, dict) and operation.get('operationId') == name:
matching_route = (route_path, methods)
break
@@ -1326,6 +1355,8 @@ async def execute_tool_server(
method_entry = None
for http_method, operation in methods.items():
if http_method not in OPENAPI_HTTP_METHODS:
continue
if not isinstance(operation, dict):
continue
if operation.get('operationId') == name:
@@ -1341,7 +1372,22 @@ async def execute_tool_server(
query_params = {}
body_params = {}
for param in operation.get('parameters', []):
# Merge path-level and operation-level parameters for execution.
path_level_params = methods.get('parameters', [])
if not isinstance(path_level_params, list):
path_level_params = []
op_params = operation.get('parameters', [])
if not isinstance(op_params, list):
op_params = []
merged_params = {}
for param in path_level_params:
if isinstance(param, dict) and param.get('name'):
merged_params[(param['name'], param.get('in', ''))] = param
for param in op_params:
if isinstance(param, dict) and param.get('name'):
merged_params[(param['name'], param.get('in', ''))] = param
for param in merged_params.values():
param_name = param.get('name')
if not param_name:
continue
@@ -1359,11 +1405,10 @@ async def execute_tool_server(
final_url = f'{url.rstrip("/")}{route_path}'
for key, value in path_params.items():
final_url = final_url.replace(f'{{{key}}}', str(value))
final_url = final_url.replace(f'{{{key}}}', quote(str(value), safe=''))
if query_params:
query_string = '&'.join(f'{k}={v}' for k, v in query_params.items())
final_url = f'{final_url}?{query_string}'
final_url = f'{final_url}?{urlencode(query_params)}'
if operation.get('requestBody', {}).get('content'):
if params: