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:
Classic298 2026-09-06 23:01:26 +02:00 committed by GitHub
parent 66e021a926
commit 1932ca649e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 38 additions and 5 deletions

View file

@ -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(

View file

@ -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.`);

View file

@ -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']) {