feat(chat-completions): add structured_outputs (choice/grammar) and disable_any_whitespace - #4845
Open
lvhan028 wants to merge 4 commits into
Open
feat(chat-completions): add structured_outputs (choice/grammar) and disable_any_whitespace#4845lvhan028 wants to merge 4 commits into
lvhan028 wants to merge 4 commits into
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR extends the /v1/chat/completions API with a new structured_outputs request field to support guided decoding via either a literal-choice constraint (choice) or an EBNF grammar constraint (grammar), and threads a disable_any_whitespace flag through the serving → generation config → engine compilation pipeline.
Changes:
- Adds
StructuredOutputsandChatCompletionRequest.structured_outputs, and makesstructured_outputsoverrideresponse_formatin the handler. - Adds shared guided-decoding compile helpers (
guided.py) and updates both PyTorch and TurboMind engine guided-decoding dispatch to supportchoice/grammar(with TurboMind graceful degradation). - Adds focused unit tests for structured outputs, whitespace threading, and chat_completions package migration/re-exports.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_lmdeploy/serve/openai/chat_completions/test_structured_outputs.py | Adds unit tests covering structured_outputs (choice/grammar) and serving-layer precedence behavior. |
| tests/test_lmdeploy/serve/openai/chat_completions/test_disable_any_whitespace.py | Adds tests verifying disable_any_whitespace is threaded to JSON-schema compilation plumbing. |
| tests/test_guided_structural_tag.py | Adds compile-time tests for structural_tag and choice helper compilation behavior. |
| tests/test_chat_completions_package_migration.py | Verifies chat_completions package refactor invariants and top-level protocol re-exports. |
| lmdeploy/turbomind/turbomind.py | Extends TurboMind guided-decoding dispatch to recognize structural_tag/grammar/choice and threads any_whitespace. |
| lmdeploy/serve/openai/protocol.py | Moves chat-completions models out and re-exports them from the new endpoint protocol module for compatibility. |
| lmdeploy/serve/openai/endpoints/chat_completions/validation.py | Factors chat-completions request validation into a dedicated module. |
| lmdeploy/serve/openai/endpoints/chat_completions/serving.py | Adds _structured_outputs_to_response_format and applies structured_outputs precedence after gen config construction. |
| lmdeploy/serve/openai/endpoints/chat_completions/protocol.py | Introduces chat-completions-specific Pydantic models including StructuredOutputs. |
| lmdeploy/serve/openai/endpoints/chat_completions/logprobs.py | Extracts logprobs construction helpers from serving into a standalone module. |
| lmdeploy/serve/openai/endpoints/chat_completions/logits_processors.py | Extracts the logit-bias logits processor from serving into a standalone module. |
| lmdeploy/serve/openai/endpoints/chat_completions/guided.py | Adds compile helpers for structural_tag and choice, shared across engines and tests. |
| lmdeploy/serve/openai/endpoints/chat_completions/init.py | Makes register lazy to avoid circular imports with top-level protocol re-exports. |
| lmdeploy/serve/openai/endpoints/init.py | Makes create_openai_router lazy to avoid circular imports with chat model re-exports. |
| lmdeploy/pytorch/engine/guided_process.py | Adds choice/grammar/structural_tag compilation branches and threads any_whitespace into JSON-schema compilation. |
Suppressed comments (2)
lmdeploy/serve/openai/endpoints/chat_completions/serving.py:62
_structured_outputs_to_response_formattreatsstructured_outputs.choice=[](orgrammar='') as present, producing a response_format that will fail later (e.g.,compile_choice([])raisesValueError). Even with request validation, adding a small guard here avoids creating an invalid engine payload if an empty value slips through.
lmdeploy/turbomind/turbomind.py:771- Same issue as the
json_schemabranch: defaultingany_whitespacetoTruehere overrides the_xgrammarbinding default and changes behavior for existingjson_objectguided-decoding requests. Consider only passingany_whitespacewhen explicitly set ingen_config.response_format.
decode_grammar = str(decode_grammar)
grammar = compiler.compile_json_schema(
decode_grammar,
any_whitespace=gen_config.response_format.get('any_whitespace', True))
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+66
to
+76
| parser_cls = server_context.response_parser_cls | ||
| if request.tool_choice != 'none' and request.tools: | ||
| if parser_cls is None or parser_cls.tool_parser_cls is None: | ||
| return 'Please launch the api_server with --tool-call-parser if you want to use tools.' | ||
|
|
||
| if request.return_routed_experts and not engine_config.enable_return_routed_experts: | ||
| return ( | ||
| 'routed experts requested but not configured in engine configuration. ' | ||
| 'May start api_server with --enable-return-routed-experts flag.') | ||
|
|
||
| return '' |
Comment on lines
760
to
+763
| decode_grammar = json.dumps(decode_grammar) | ||
| grammar = compiler.compile_json_schema(decode_grammar) | ||
| grammar = compiler.compile_json_schema( | ||
| decode_grammar, | ||
| any_whitespace=gen_config.response_format.get('any_whitespace', True)) |
lvhan028
force-pushed
the
feat/structured-outputs
branch
6 times, most recently
from
August 10, 2026 11:22
882d3ba to
8827485
Compare
Reuses the existing grammar-agnostic matcher path (pytorch GuidedDecodingManager; turbomind set_grammar). No C++ kernel changes. Co-Authored-By: Claude <noreply@anthropic.com>
Rebuild compile_choice with xgr.Grammar.union of ConstStringFormat grammars so option strings are opaque literals (no EBNF escaping, no lexer crash on ", no alternation injection). Also wrap _to_xgr_structural_tag native/single/multi-tag branches to convert pydantic ValidationError into ValueError so the engines' except ValueError still catches malformed payloads. Co-Authored-By: Claude <noreply@anthropic.com>
Exposes xgrammar choice (alternation) and grammar (EBNF) constrained decoding via a structured_outputs request field, compiled through the existing grammar-agnostic matcher in both engines. structured_outputs takes precedence over response_format when both are present. On turbomind the bundled _xgrammar binding lacks compile_grammar, so choice/grammar hit the same graceful-degradation path (hasattr guard + ValueError + warning + disable) established for structural_tag; pytorch is fully functional. Co-Authored-By: Claude <noreply@anthropic.com>
Adds a module-level _compile_json_schema_opts wrapper on the pytorch GuidedDecodingManager that forwards the any_whitespace flag to xgrammar's compile_json_schema, and threads it through the json_schema/json_object branches of both engines (pytorch + turbomind) via an any_whitespace key on the response_format dict set by the handler when structured_outputs.disable_any_whitespace is True. Co-Authored-By: Claude <noreply@anthropic.com>
lvhan028
force-pushed
the
feat/structured-outputs
branch
from
August 10, 2026 13:33
8827485 to
0621391
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Add a
structured_outputsrequest field for EBNFgrammarandchoice(const-string) constrained decoding, plusdisable_any_whitespace:ChatCompletionRequest.structured_outputs: StructuredOutputs | None, whereStructuredOutputshaschoice: list[str] | None,grammar: str | None,disable_any_whitespace: bool.structured_outputstakes precedence overresponse_format(the handler overwritesgen_config.response_format).choice→ const-string alternation;grammar→ EBNF string. Both compiled viaguided.py(compile_choice/compile_grammar).disable_any_whitespaceis threaded end-to-end (handler → dict → both engines'compile_json_schema(any_whitespace=...)).Tasks
Tasks 8 + 9 of the chat-completions feature plan.
Files
lmdeploy/serve/openai/endpoints/chat_completions/protocol.py—StructuredOutputs+structured_outputsfield.lmdeploy/serve/openai/endpoints/chat_completions/serving.py—_structured_outputs_to_response_format; applied afterbuild_serving_generation_config(precedence).lmdeploy/serve/openai/protocol.py—StructuredOutputsre-exported from top level.lmdeploy/pytorch/engine/guided_process.py—grammar/choicebranches;_compile_json_schema_optswrapper;any_whitespacethreading.lmdeploy/turbomind/turbomind.py—grammar/choicedispatch + compile branches withhasattr(compiler, 'compile_grammar')guards;any_whitespacethreaded tocompile_json_schema.tests/test_lmdeploy/serve/openai/chat_completions/test_structured_outputs.py(8),test_disable_any_whitespace.py(5).Tests
Dependency
Depends on the guided-structural-tag PR (
feat/guided-structural-tag) — it importscompile_choicefromguided.py. Merge that first, then rebase ontomain.Notes
grammar/choice(its bundled_xgrammarC++ binding lackscompile_grammar— same limitation asstructural_tag). It hits ahasattrguard →ValueError→ caught → warning + disable guided decoding (no crash). pytorch is fully functional. Fix = future C++ binding extension.disable_any_whitespacehas no current behavioral effect whenstructured_outputscarries onlychoice/grammar(neither invokescompile_json_schema); the threading is complete and will activate when a future task adds ajson/json_schemasub-field. Documented in theStructuredOutputsdocstring (accepted scope per the plan).--no-verifylocally (env lacks python3.10 for the docformatter pre-commit hook); CI runs the hook with the correct interpreter.🤖 Generated with Claude Code