mirror of
https://github.com/open-webui/open-webui.git
synced 2026-09-07 08:27:05 +00:00
fix: stop sending OpenAPI tool server path and query parameters in the request body (#29717)
Tool calls to an OpenAPI tool server put every argument the model returned into the JSON request body, including the parameters that were already substituted into the URL. Servers that validate their input strictly (additionalProperties: false) answered 422 "unexpected property", so reads worked and every write through an endpoint with a path or query parameter failed. The body is now built from the model's arguments minus the operation's declared parameters, keeping any name the requestBody schema declares as a property of its own, so an endpoint that wants the resource id in the body as well as in the path still gets it. The filter only runs when the resolved body schema lists its properties. A free-form, composed or non-JSON body offers nothing to check a name against, so those requests go out exactly as before. src/lib/apis/index.ts carries the same request builder for direct tool server connections and had the same bug, so it gets the same fix. Fixes #29716
This commit is contained in:
parent
66e021a926
commit
1932ca649e
3 changed files with 38 additions and 5 deletions
|
|
@ -1668,6 +1668,7 @@ async def execute_tool_server(
|
|||
path_params = {}
|
||||
query_params = {}
|
||||
body_params = {}
|
||||
declared_param_names = set()
|
||||
|
||||
# Merge path-level and operation-level parameters for execution.
|
||||
path_level_params = methods.get('parameters', [])
|
||||
|
|
@ -1688,6 +1689,7 @@ async def execute_tool_server(
|
|||
param_name = param.get('name')
|
||||
if not param_name:
|
||||
continue
|
||||
declared_param_names.add(param_name)
|
||||
param_in = param.get('in')
|
||||
if param_name in params:
|
||||
if param_in == 'path':
|
||||
|
|
@ -1707,8 +1709,16 @@ async def execute_tool_server(
|
|||
if query_params:
|
||||
final_url = f'{final_url}?{urlencode(query_params)}'
|
||||
|
||||
if operation.get('requestBody', {}).get('content'):
|
||||
if params:
|
||||
request_body_content = operation.get('requestBody', {}).get('content')
|
||||
if request_body_content and params:
|
||||
json_schema = request_body_content.get('application/json', {}).get('schema')
|
||||
resolved_body_schema = resolve_schema(json_schema, openapi.get('components', {}))
|
||||
is_composed_schema = any(keyword in resolved_body_schema for keyword in ('allOf', 'anyOf', 'oneOf'))
|
||||
body_properties = {} if is_composed_schema else (resolved_body_schema.get('properties') or {})
|
||||
# Strict servers reject declared parameters in the body, unless the body schema declares them too.
|
||||
if body_properties:
|
||||
body_params = {k: v for k, v in params.items() if k in body_properties or k not in declared_param_names}
|
||||
else:
|
||||
body_params = params
|
||||
|
||||
async with aiohttp.ClientSession(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { convertOpenApiToToolPayload } from '$lib/utils';
|
||||
import { convertOpenApiToToolPayload, resolveSchema } from '$lib/utils';
|
||||
import { normalizeTags } from '$lib/utils/tags';
|
||||
import { getOpenAIModelsDirect } from './openai';
|
||||
|
||||
|
|
@ -598,10 +598,12 @@ export const executeToolServer = async (
|
|||
const pathParams: Record<string, any> = {};
|
||||
const queryParams: Record<string, any> = {};
|
||||
let bodyParams: any = {};
|
||||
const declaredParamNames = new Set<string>();
|
||||
|
||||
for (const param of mergedParams.values()) {
|
||||
const paramName = param?.name;
|
||||
if (!paramName) continue;
|
||||
declaredParamNames.add(paramName);
|
||||
const paramIn = param?.in;
|
||||
if (params.hasOwnProperty(paramName)) {
|
||||
if (paramIn === 'path') {
|
||||
|
|
@ -631,7 +633,24 @@ export const executeToolServer = async (
|
|||
if (operation.requestBody && operation.requestBody.content) {
|
||||
const contentType = Object.keys(operation.requestBody.content)[0];
|
||||
if (params !== undefined) {
|
||||
bodyParams = params;
|
||||
const jsonSchema = operation.requestBody.content['application/json']?.schema;
|
||||
const resolvedBodySchema = resolveSchema(jsonSchema, serverData.openapi.components);
|
||||
const isComposedSchema = ['allOf', 'anyOf', 'oneOf'].some(
|
||||
(keyword) => keyword in resolvedBodySchema
|
||||
);
|
||||
const bodyProperties = isComposedSchema ? {} : (resolvedBodySchema.properties ?? {});
|
||||
// Strict servers reject declared parameters in the body, unless the body schema declares them too.
|
||||
if (Object.keys(bodyProperties).length > 0) {
|
||||
bodyParams = Object.fromEntries(
|
||||
Object.entries(params).filter(
|
||||
([key]) =>
|
||||
Object.prototype.hasOwnProperty.call(bodyProperties, key) ||
|
||||
!declaredParamNames.has(key)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
bodyParams = params;
|
||||
}
|
||||
} else {
|
||||
// Optional: Fallback or explicit error if body is expected but not provided
|
||||
throw new Error(`Request body expected for operation '${name}' but none found.`);
|
||||
|
|
|
|||
|
|
@ -1464,7 +1464,11 @@ export const getLineCount = (text) => {
|
|||
};
|
||||
|
||||
// Helper function to recursively resolve OpenAPI schema into JSON schema format
|
||||
function resolveSchema(schemaRef, components, resolvedSchemas = new Set()) {
|
||||
export function resolveSchema(
|
||||
schemaRef,
|
||||
components,
|
||||
resolvedSchemas = new Set()
|
||||
): Record<string, any> {
|
||||
if (!schemaRef) return {};
|
||||
|
||||
if (schemaRef['$ref']) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue