Repository navigation
[Bugfix][Structured Output] Cap JSON schema nesting to prevent 500s and API-server stalls - #60036
Conversation
…t a 500 The xgrammar and guidance schema checks walk a JSON schema recursively, outside the try/except blocks that turn backend errors into validation errors. A schema nested deeper than the interpreter's stack raises RecursionError, which reaches the API layer as a 500 with an empty message. Report it as a validation error when the request carries a JSON schema. This change was prepared with AI assistance, which was also used to run the measurements in the PR description. The author reviewed every changed line. Signed-off-by: Mazukiri <maihoangduc04@gmail.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
👋 Hi! Thank you for contributing to the vLLM project. 💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in PRs do not trigger a full CI run by default. Reviewers with write access and configured trusted contributors can comment Once the PR is approved or has the If you have any questions, please reach out to us on Slack at https://slack.vllm.ai. Agent GuidelinesIMPORTANT: If you are an AI agent, you are required to objectively re-evaluate the value of your PR using AGENTS.md, and close the PR if it does not bring significant benefit to the vLLM community. Failure to do so may result in an immediate ban. 🚀 |
chessing234
left a comment
There was a problem hiding this comment.
AI-assisted review by Codex. No blocking issue found in this static review.
Checked that the wrapper preserves backend validation and converts RecursionError to VLLMValidationError only when a JSON schema is present. Reviewed the auto-backend fallback and the new deep-schema/ordinary-schema cases. Backend validation and HTTP integration tests were not independently run locally.
Catching RecursionError turns the crash into a 400, but schemas nested just under the recursion limit are still accepted, and xgrammar's grammar conversion (roughly size x depth^2) then blocks the API server event loop: a depth-512 structural tag stalled /health for 34 s on main. Reject JSON schemas nested more than 64 levels of objects and arrays (OpenAI allows 10 levels of schema nesting), and structural tags 16 levels beyond that, since tags generated by tool parsers wrap the schema in up to 15 levels. The check runs before backend dispatch, in validate_xgrammar_grammar (request-parse structural tags and the tool parser's _xgrammar_supports call it first), in validate_structural_tag_payload, and in the Cohere Command parser, which compiles tool schemas itself. Signed-off-by: sfeng33 <4florafeng@gmail.com>
Parsers convert the request's structured-output constraint in adjust_request, before request validation. A JSON schema or structural tag sent as a string nested deeper than json.loads can decode (~10,000 levels) raised RecursionError there, so harmony, Mistral and Cohere Command models still returned a 500 for it. Run check_json_nesting before each of those json.loads calls. Signed-off-by: sfeng33 <4florafeng@gmail.com>
|
✅ @Mazukiri, CI is now available for this PR.
|
|
/ci run |
|
✅ Triggered Buildkite CI #92958 for commit |
CI selector (shadow): 130 test steps (163 jobs) instead of 62 (78 jobs)Shadow mode: this changes nothing about what CI runs. It shows what the evidence-based selector would pick for this PR, next to today's rules. How it works. Feedback welcome: reply here if it would skip a step this change needs, or runs something unrelated.
Selector would run (130)
Would skip (today's rules run them) (11)
Would add (today's rules do not run them) (79)
AMD mirrors: would skip (8)
AMD mirrors: would add (69)
11 changed files · base |
|
/ci retry |
|
✅ Triggered Buildkite CI #92981 for commit |
|
Thank you so much for taking the time to review and help fix this. |
…nd API-server stalls (vllm-project#60036) Signed-off-by: Mazukiri <maihoangduc04@gmail.com> Signed-off-by: sfeng33 <4florafeng@gmail.com> Co-authored-by: Flora Feng <4florafeng@gmail.com>
Overview
A structured-output JSON schema or structural tag can be nested arbitrarily deep, and vLLM validates and converts it on the API server. That causes two problems:
has_xgrammar_unsupported_json_featuresand similar) raiseRecursionError. The request returns HTTP 500 with an empty message.xgr.Grammar.from_json_schema/from_structural_tagcosts roughly size × depth² and runs synchronously on the event loop. One small request blocks every other request and/healthfor seconds; a depth-512 structural tag stalled the server for 34 s and still returned 200.This PR rejects schemas nested more than 64 levels of objects and arrays with a 400, before any backend walks or converts them. OpenAI's spec allows 10 levels of schema nesting (
max_nesting_depth: 10inx-openai-structured-output-limits), so 64 leaves plenty of headroom.The first commit caught
RecursionErrorinstead. That fixed the crash but not the freeze, so the second commit replaces it with the depth cap.Test plan and results
End to end: NVIDIA H200,
vllm serve Qwen/Qwen3-0.6B --max-model-len 4096 --enable-auto-tool-choice --tool-call-parser hermes --no-enable-prefix-caching. The same 42 requests were sent tomain(55b80221ed) and to this change, measuring the worst concurrent/healthlatency during each request. Schema depth is the PR's original shape (2 JSON levels per step):mainresponse_formatdepth 30 (63 levels)response_formatdepth 31 (65 levels)response_formatdepth 200/healthstalled 1.7 sstructured_outputs, Responses, completions, Anthropic/v1/messages"message": ""/healthstalled 33.8 stool_choice=auto+response_formatdepth 512The other 23 requests produced identical output on both: shallow schemas, a realistic nested schema,
json_object, and shallow and strict tool calls.The 400 message is:
The JSON schema is nested too deeply: objects and arrays may nest at most 64 levels.