chore: replace JSON requestBody block with query params

Signed-off-by: Sébastien Han <seb@redhat.com>
This commit is contained in:
Sébastien Han 2025-11-12 12:03:57 +01:00
parent e3d831f504
commit de4ed29310
No known key found for this signature in database
4 changed files with 185 additions and 88 deletions

View file

@ -1701,16 +1701,16 @@ paths:
schema: schema:
type: string type: string
description: 'Path parameter: response_id' description: 'Path parameter: response_id'
requestBody: - name: include
content: in: query
application/json: required: false
schema: schema:
anyOf: anyOf:
- type: array - type: array
items: items:
type: string type: string
- type: 'null' - type: 'null'
title: Include title: Include
responses: responses:
'200': '200':
description: An ListOpenAIResponseInputItem. description: An ListOpenAIResponseInputItem.
@ -4004,14 +4004,14 @@ paths:
- type: string - type: string
- type: 'null' - type: 'null'
title: Tool Group Id title: Tool Group Id
requestBody: - name: mcp_endpoint
content: in: query
application/json: required: false
schema: schema:
anyOf: anyOf:
- $ref: '#/components/schemas/URL' - $ref: '#/components/schemas/URL'
- type: 'null' - type: 'null'
title: Mcp Endpoint title: Mcp Endpoint
responses: responses:
'200': '200':
description: A ListToolDefsResponse. description: A ListToolDefsResponse.
@ -4579,16 +4579,16 @@ paths:
schema: schema:
type: string type: string
description: 'Path parameter: conversation_id' description: 'Path parameter: conversation_id'
requestBody: - name: include
content: in: query
application/json: required: false
schema: schema:
anyOf: anyOf:
- type: array - type: array
items: items:
$ref: '#/components/schemas/ConversationItemInclude' $ref: '#/components/schemas/ConversationItemInclude'
- type: 'null' - type: 'null'
title: Include title: Include
responses: responses:
'200': '200':
description: List of conversation items. description: List of conversation items.

View file

@ -309,16 +309,16 @@ paths:
schema: schema:
type: string type: string
description: 'Path parameter: response_id' description: 'Path parameter: response_id'
requestBody: - name: include
content: in: query
application/json: required: false
schema: schema:
anyOf: anyOf:
- type: array - type: array
items: items:
type: string type: string
- type: 'null' - type: 'null'
title: Include title: Include
responses: responses:
'200': '200':
description: An ListOpenAIResponseInputItem. description: An ListOpenAIResponseInputItem.
@ -1819,14 +1819,14 @@ paths:
- type: string - type: string
- type: 'null' - type: 'null'
title: Tool Group Id title: Tool Group Id
requestBody: - name: mcp_endpoint
content: in: query
application/json: required: false
schema: schema:
anyOf: anyOf:
- $ref: '#/components/schemas/URL' - $ref: '#/components/schemas/URL'
- type: 'null' - type: 'null'
title: Mcp Endpoint title: Mcp Endpoint
responses: responses:
'200': '200':
description: A ListToolDefsResponse. description: A ListToolDefsResponse.
@ -2489,16 +2489,16 @@ paths:
schema: schema:
type: string type: string
description: 'Path parameter: conversation_id' description: 'Path parameter: conversation_id'
requestBody: - name: include
content: in: query
application/json: required: false
schema: schema:
anyOf: anyOf:
- type: array - type: array
items: items:
$ref: '#/components/schemas/ConversationItemInclude' $ref: '#/components/schemas/ConversationItemInclude'
- type: 'null' - type: 'null'
title: Include title: Include
responses: responses:
'200': '200':
description: List of conversation items. description: List of conversation items.

View file

@ -1701,16 +1701,16 @@ paths:
schema: schema:
type: string type: string
description: 'Path parameter: response_id' description: 'Path parameter: response_id'
requestBody: - name: include
content: in: query
application/json: required: false
schema: schema:
anyOf: anyOf:
- type: array - type: array
items: items:
type: string type: string
- type: 'null' - type: 'null'
title: Include title: Include
responses: responses:
'200': '200':
description: An ListOpenAIResponseInputItem. description: An ListOpenAIResponseInputItem.
@ -4415,14 +4415,14 @@ paths:
- type: string - type: string
- type: 'null' - type: 'null'
title: Tool Group Id title: Tool Group Id
requestBody: - name: mcp_endpoint
content: in: query
application/json: required: false
schema: schema:
anyOf: anyOf:
- $ref: '#/components/schemas/URL' - $ref: '#/components/schemas/URL'
- type: 'null' - type: 'null'
title: Mcp Endpoint title: Mcp Endpoint
responses: responses:
'200': '200':
description: A ListToolDefsResponse. description: A ListToolDefsResponse.
@ -4990,16 +4990,16 @@ paths:
schema: schema:
type: string type: string
description: 'Path parameter: conversation_id' description: 'Path parameter: conversation_id'
requestBody: - name: include
content: in: query
application/json: required: false
schema: schema:
anyOf: anyOf:
- type: array - type: array
items: items:
$ref: '#/components/schemas/ConversationItemInclude' $ref: '#/components/schemas/ConversationItemInclude'
- type: 'null' - type: 'null'
title: Include title: Include
responses: responses:
'200': '200':
description: List of conversation items. description: List of conversation items.

View file

@ -201,6 +201,8 @@ def _create_dynamic_request_model(
try: try:
field_definitions = _build_field_definitions(query_parameters, use_any) field_definitions = _build_field_definitions(query_parameters, use_any)
if not field_definitions:
return None
clean_route = webmethod.route.replace("/", "_").replace("{", "").replace("}", "").replace("-", "_") clean_route = webmethod.route.replace("/", "_").replace("{", "").replace("}", "").replace("-", "_")
model_name = f"{clean_route}_Request" model_name = f"{clean_route}_Request"
if add_uuid: if add_uuid:
@ -238,13 +240,13 @@ def _create_fastapi_endpoint(app: FastAPI, route, webmethod, api: Api):
methods = route.methods methods = route.methods
name = route.name name = route.name
fastapi_path = path.replace("{", "{").replace("}", "}") fastapi_path = path.replace("{", "{").replace("}", "}")
is_post_put = any(method.upper() in ["POST", "PUT", "PATCH"] for method in methods)
request_model, response_model, query_parameters, file_form_params, streaming_response_model = ( request_model, response_model, query_parameters, file_form_params, streaming_response_model = (
_find_models_for_endpoint(webmethod, api, name) _find_models_for_endpoint(webmethod, api, name, is_post_put)
) )
operation_description = _extract_operation_description_from_docstring(api, name) operation_description = _extract_operation_description_from_docstring(api, name)
response_description = _extract_response_description_from_docstring(webmethod, response_model, api, name) response_description = _extract_response_description_from_docstring(webmethod, response_model, api, name)
is_post_put = any(method.upper() in ["POST", "PUT", "PATCH"] for method in methods)
# Retrieve and store extra body fields for this endpoint # Retrieve and store extra body fields for this endpoint
func = _get_protocol_method(api, name) func = _get_protocol_method(api, name)
@ -499,7 +501,7 @@ def _extract_response_models_from_union(union_type: Any) -> tuple[type | None, t
def _find_models_for_endpoint( def _find_models_for_endpoint(
webmethod, api: Api, method_name: str webmethod, api: Api, method_name: str, is_post_put: bool = False
) -> tuple[type | None, type | None, list[tuple[str, type, Any]], list[inspect.Parameter], type | None]: ) -> tuple[type | None, type | None, list[tuple[str, type, Any]], list[inspect.Parameter], type | None]:
""" """
Find appropriate request and response models for an endpoint by analyzing the actual function signature. Find appropriate request and response models for an endpoint by analyzing the actual function signature.
@ -509,6 +511,7 @@ def _find_models_for_endpoint(
webmethod: The webmethod metadata webmethod: The webmethod metadata
api: The API enum for looking up the function api: The API enum for looking up the function
method_name: The method name (function name) method_name: The method name (function name)
is_post_put: Whether this is a POST, PUT, or PATCH request (GET requests should never have request bodies)
Returns: Returns:
tuple: (request_model, response_model, query_parameters, file_form_params, streaming_response_model) tuple: (request_model, response_model, query_parameters, file_form_params, streaming_response_model)
@ -612,7 +615,8 @@ def _find_models_for_endpoint(
# If there's exactly one body parameter and it's a Pydantic model, use it directly # If there's exactly one body parameter and it's a Pydantic model, use it directly
# Otherwise, we'll create a combined request model from all parameters # Otherwise, we'll create a combined request model from all parameters
if len(query_parameters) == 1: # BUT: For GET requests, never create a request body - all parameters should be query parameters
if is_post_put and len(query_parameters) == 1:
param_name, param_type, default_value = query_parameters[0] param_name, param_type, default_value = query_parameters[0]
if hasattr(param_type, "model_json_schema"): if hasattr(param_type, "model_json_schema"):
request_model = param_type request_model = param_type
@ -1223,6 +1227,94 @@ def _remove_query_params_from_body_endpoints(openapi_schema: dict[str, Any]) ->
return openapi_schema return openapi_schema
def _remove_request_bodies_from_get_endpoints(openapi_schema: dict[str, Any]) -> dict[str, Any]:
"""
Remove request bodies from GET endpoints and convert their parameters to query parameters.
GET requests should never have request bodies - all parameters should be query parameters.
This function removes any requestBody that FastAPI may have incorrectly added to GET endpoints
and converts any parameters in the requestBody to query parameters.
"""
if "paths" not in openapi_schema:
return openapi_schema
for _path, path_item in openapi_schema["paths"].items():
if not isinstance(path_item, dict):
continue
# Check GET method specifically
if "get" in path_item:
operation = path_item["get"]
if not isinstance(operation, dict):
continue
if "requestBody" in operation:
request_body = operation["requestBody"]
# Extract parameters from requestBody and convert to query parameters
if isinstance(request_body, dict) and "content" in request_body:
content = request_body.get("content", {})
json_content = content.get("application/json", {})
schema = json_content.get("schema", {})
if "parameters" not in operation:
operation["parameters"] = []
elif not isinstance(operation["parameters"], list):
operation["parameters"] = []
# If the schema has properties, convert each to a query parameter
if isinstance(schema, dict) and "properties" in schema:
for param_name, param_schema in schema["properties"].items():
# Check if this parameter is already in the parameters list
existing_param = None
for existing in operation["parameters"]:
if isinstance(existing, dict) and existing.get("name") == param_name:
existing_param = existing
break
if not existing_param:
# Create a new query parameter from the requestBody property
required = param_name in schema.get("required", [])
query_param = {
"name": param_name,
"in": "query",
"required": required,
"schema": param_schema,
}
# Add description if present
if "description" in param_schema:
query_param["description"] = param_schema["description"]
operation["parameters"].append(query_param)
elif isinstance(schema, dict):
# Handle direct schema (not a model with properties)
# Try to infer parameter name from schema title
param_name = schema.get("title", "").lower().replace(" ", "_")
if param_name:
# Check if this parameter is already in the parameters list
existing_param = None
for existing in operation["parameters"]:
if isinstance(existing, dict) and existing.get("name") == param_name:
existing_param = existing
break
if not existing_param:
# Create a new query parameter from the requestBody schema
query_param = {
"name": param_name,
"in": "query",
"required": False, # Default to optional for GET requests
"schema": schema,
}
# Add description if present
if "description" in schema:
query_param["description"] = schema["description"]
operation["parameters"].append(query_param)
# Remove request body from GET endpoint
del operation["requestBody"]
return openapi_schema
def _convert_multiline_strings_to_literal(obj: Any) -> Any: def _convert_multiline_strings_to_literal(obj: Any) -> Any:
"""Recursively convert multi-line strings to LiteralScalarString for YAML block scalar formatting.""" """Recursively convert multi-line strings to LiteralScalarString for YAML block scalar formatting."""
try: try:
@ -1619,6 +1711,11 @@ def generate_openapi_spec(output_dir: str) -> dict[str, Any]:
# Add x-llama-stack-extra-body-params extension for ExtraBodyField parameters # Add x-llama-stack-extra-body-params extension for ExtraBodyField parameters
openapi_schema = _add_extra_body_params_extension(openapi_schema) openapi_schema = _add_extra_body_params_extension(openapi_schema)
# Remove request bodies from GET endpoints (GET requests should never have request bodies)
# This must run AFTER _add_extra_body_params_extension to ensure any request bodies
# that FastAPI incorrectly added to GET endpoints are removed
openapi_schema = _remove_request_bodies_from_get_endpoints(openapi_schema)
# Split into stable (v1 only), experimental (v1alpha + v1beta), deprecated, and combined (stainless) specs # Split into stable (v1 only), experimental (v1alpha + v1beta), deprecated, and combined (stainless) specs
# Each spec needs its own deep copy of the full schema to avoid cross-contamination # Each spec needs its own deep copy of the full schema to avoid cross-contamination
import copy import copy