fix: fix OpenAPI Schema Import Pydantic Validation Errors for Complex Default Values (#27159)

Co-authored-by: Alain <yinxulai@hoymail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Alain 2025-10-22 11:45:31 +08:00 committed by GitHub
parent bebb4ffbaa
commit 26ff59172e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 114 additions and 7 deletions

View File

@ -76,7 +76,7 @@ class PluginParameter(BaseModel):
auto_generate: PluginParameterAutoGenerate | None = None auto_generate: PluginParameterAutoGenerate | None = None
template: PluginParameterTemplate | None = None template: PluginParameterTemplate | None = None
required: bool = False required: bool = False
default: Union[float, int, str] | None = None default: Union[float, int, str, bool] | None = None
min: Union[float, int] | None = None min: Union[float, int] | None = None
max: Union[float, int] | None = None max: Union[float, int] | None = None
precision: int | None = None precision: int | None = None

View File

@ -62,6 +62,11 @@ class ApiBasedToolSchemaParser:
root = root[ref] root = root[ref]
interface["operation"]["parameters"][i] = root interface["operation"]["parameters"][i] = root
for parameter in interface["operation"]["parameters"]: for parameter in interface["operation"]["parameters"]:
# Handle complex type defaults that are not supported by PluginParameter
default_value = None
if "schema" in parameter and "default" in parameter["schema"]:
default_value = ApiBasedToolSchemaParser._sanitize_default_value(parameter["schema"]["default"])
tool_parameter = ToolParameter( tool_parameter = ToolParameter(
name=parameter["name"], name=parameter["name"],
label=I18nObject(en_US=parameter["name"], zh_Hans=parameter["name"]), label=I18nObject(en_US=parameter["name"], zh_Hans=parameter["name"]),
@ -72,9 +77,7 @@ class ApiBasedToolSchemaParser:
required=parameter.get("required", False), required=parameter.get("required", False),
form=ToolParameter.ToolParameterForm.LLM, form=ToolParameter.ToolParameterForm.LLM,
llm_description=parameter.get("description"), llm_description=parameter.get("description"),
default=parameter["schema"]["default"] default=default_value,
if "schema" in parameter and "default" in parameter["schema"]
else None,
placeholder=I18nObject( placeholder=I18nObject(
en_US=parameter.get("description", ""), zh_Hans=parameter.get("description", "") en_US=parameter.get("description", ""), zh_Hans=parameter.get("description", "")
), ),
@ -134,6 +137,11 @@ class ApiBasedToolSchemaParser:
required = body_schema.get("required", []) required = body_schema.get("required", [])
properties = body_schema.get("properties", {}) properties = body_schema.get("properties", {})
for name, property in properties.items(): for name, property in properties.items():
# Handle complex type defaults that are not supported by PluginParameter
default_value = ApiBasedToolSchemaParser._sanitize_default_value(
property.get("default", None)
)
tool = ToolParameter( tool = ToolParameter(
name=name, name=name,
label=I18nObject(en_US=name, zh_Hans=name), label=I18nObject(en_US=name, zh_Hans=name),
@ -144,12 +152,11 @@ class ApiBasedToolSchemaParser:
required=name in required, required=name in required,
form=ToolParameter.ToolParameterForm.LLM, form=ToolParameter.ToolParameterForm.LLM,
llm_description=property.get("description", ""), llm_description=property.get("description", ""),
default=property.get("default", None), default=default_value,
placeholder=I18nObject( placeholder=I18nObject(
en_US=property.get("description", ""), zh_Hans=property.get("description", "") en_US=property.get("description", ""), zh_Hans=property.get("description", "")
), ),
) )
# check if there is a type # check if there is a type
typ = ApiBasedToolSchemaParser._get_tool_parameter_type(property) typ = ApiBasedToolSchemaParser._get_tool_parameter_type(property)
if typ: if typ:
@ -197,6 +204,22 @@ class ApiBasedToolSchemaParser:
return bundles return bundles
@staticmethod
def _sanitize_default_value(value):
"""
Sanitize default values for PluginParameter compatibility.
Complex types (list, dict) are converted to None to avoid validation errors.
Args:
value: The default value from OpenAPI schema
Returns:
None for complex types (list, dict), otherwise the original value
"""
if isinstance(value, (list, dict)):
return None
return value
@staticmethod @staticmethod
def _get_tool_parameter_type(parameter: dict) -> ToolParameter.ToolParameterType | None: def _get_tool_parameter_type(parameter: dict) -> ToolParameter.ToolParameterType | None:
parameter = parameter or {} parameter = parameter or {}
@ -217,7 +240,11 @@ class ApiBasedToolSchemaParser:
return ToolParameter.ToolParameterType.STRING return ToolParameter.ToolParameterType.STRING
elif typ == "array": elif typ == "array":
items = parameter.get("items") or parameter.get("schema", {}).get("items") items = parameter.get("items") or parameter.get("schema", {}).get("items")
return ToolParameter.ToolParameterType.FILES if items and items.get("format") == "binary" else None if items and items.get("format") == "binary":
return ToolParameter.ToolParameterType.FILES
else:
# For regular arrays, return ARRAY type instead of None
return ToolParameter.ToolParameterType.ARRAY
else: else:
return None return None

View File

@ -109,3 +109,83 @@ def test_parse_openapi_to_tool_bundle_properties_all_of(app):
assert tool_bundles[0].parameters[0].llm_description == "desc prop1" assert tool_bundles[0].parameters[0].llm_description == "desc prop1"
# TODO: support enum in OpenAPI # TODO: support enum in OpenAPI
# assert set(tool_bundles[0].parameters[0].options) == {"option1", "option2", "option3"} # assert set(tool_bundles[0].parameters[0].options) == {"option1", "option2", "option3"}
def test_parse_openapi_to_tool_bundle_default_value_type_casting(app):
"""
Test that default values are properly cast to match parameter types.
This addresses the issue where array default values like [] cause validation errors
when parameter type is inferred as string/number/boolean.
"""
openapi = {
"openapi": "3.0.0",
"info": {"title": "Test API", "version": "1.0.0"},
"servers": [{"url": "https://example.com"}],
"paths": {
"/product/create": {
"post": {
"operationId": "createProduct",
"summary": "Create a product",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"categories": {
"description": "List of category identifiers",
"default": [],
"type": "array",
"items": {"type": "string"},
},
"name": {
"description": "Product name",
"default": "Default Product",
"type": "string",
},
"price": {"description": "Product price", "default": 0.0, "type": "number"},
"available": {
"description": "Product availability",
"default": True,
"type": "boolean",
},
},
}
}
}
},
"responses": {"200": {"description": "Default Response"}},
}
}
},
}
with app.test_request_context():
tool_bundles = ApiBasedToolSchemaParser.parse_openapi_to_tool_bundle(openapi)
assert len(tool_bundles) == 1
bundle = tool_bundles[0]
assert len(bundle.parameters) == 4
# Find parameters by name
params_by_name = {param.name: param for param in bundle.parameters}
# Check categories parameter (array type with [] default)
categories_param = params_by_name["categories"]
assert categories_param.type == "array" # Will be detected by _get_tool_parameter_type
assert categories_param.default is None # Array default [] is converted to None
# Check name parameter (string type with string default)
name_param = params_by_name["name"]
assert name_param.type == "string"
assert name_param.default == "Default Product"
# Check price parameter (number type with number default)
price_param = params_by_name["price"]
assert price_param.type == "number"
assert price_param.default == 0.0
# Check available parameter (boolean type with boolean default)
available_param = params_by_name["available"]
assert available_param.type == "boolean"
assert available_param.default is True