feat(vllm): add grammar and structured output support - #8806
Conversation
✅ Deploy Preview for localai ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
dabd63c to
bb08454
Compare
|
GM @mudler |
|
Hi @localai-bot would you review this PR? |
| _structured_output_field = "guided_decoding" | ||
| except ImportError: | ||
| _structured_output_cls = None | ||
| _structured_output_field = None |
There was a problem hiding this comment.
Do we need a fallback? We usually pin the upstream version.
There was a problem hiding this comment.
Good point. I checked and vLLM is actually not pinned to a specific version — requirements-after.txt just lists vllm with no version constraint, and different platform builds (CPU/CUDA/ROCm) may end up with different vLLM versions.
That said, if the project plans to pin vLLM to a specific version, I'm happy to drop the fallback and target whichever API is current. Let me know which you'd prefer.
There was a problem hiding this comment.
OK, when you say newer versions, how new? If it's a very recent change then maybe we need this, otherwise we probably don't
There was a problem hiding this comment.
The rename happened in vLLM v0.8.x → latest. GuidedDecodingParams was renamed to StructuredOutputsParams and the corresponding SamplingParams field changed from guided_decoding to structured_outputs.
Since vLLM isn't pinned (requirements-after.txt just says vllm), builds can land on either version depending on when/how the image is built. If we pin to a specific version, I can drop the fallback and target that API directly — let me know which version to target.
Also in the latest push: I've refactored to use the Metadata map instead of new proto fields, as discussed.
There was a problem hiding this comment.
Just remove the fallback and if it passes CI we don't need to worry
|
Also can you create e2e tests for this? |
Would you review again? |
|
this sounds like an important/cool feature |
278e7e2 to
c703a03
Compare
| # The corresponding SamplingParams field also changed from guided_decoding to structured_outputs. | ||
| try: | ||
| from vllm.sampling_params import StructuredOutputsParams | ||
| _structured_output_cls = StructuredOutputsParams |
| try: | ||
| from vllm.sampling_params import StructuredOutputsParams | ||
| _structured_output_cls = StructuredOutputsParams | ||
| _structured_output_field = "structured_outputs" |
There was a problem hiding this comment.
ditto here - I'm not sure what are we doing here, just importing , adding to a var.. and not used?
|
Heads-up from testing on vLLM 0.23.0 (arm64 / CUDA 13): I hit the same I opened #10339 with a minimal fix that adapts the existing path to the renamed class/field (with a fallback for older vLLM). It looked complementary to the native-json_schema-via-metadata direction here — I noticed this PR adds |
|
I'm not sure if we need this one after #10343 ? |
|
Hi Richie — these two PRs address different layers of the same area, but #10343 is functionally sufficient on its own: #10343 is a compatibility hotfix. The import failure on vLLM ≥0.23 meant the existing #8806 adds a native JSON-Schema pass-through (new proto fields So: if the goal is "structured outputs work" → #10343 already does that. If the goal is "vLLM's native — Poseidon |
Add JSONSchema field to ModelConfig to carry the raw JSON schema string alongside the GBNF Grammar. Pass both JSONSchema and ResponseFormat through gRPCPredictOpts to backends via the new proto fields. This allows backends like vLLM to receive the original JSON schema for native structured output support. Ref: mudler#6857 Signed-off-by: eureka928 <meobius123@gmail.com>
In chat and completion endpoints, when response_format is json_schema, extract the raw JSON schema and store it on config.JSONSchema alongside the GBNF grammar. Also set config.ResponseFormat to the format type. This allows backends that support native structured output (like vLLM) to use the JSON schema directly instead of the GBNF grammar. Ref: mudler#6857 Signed-off-by: eureka928 <meobius123@gmail.com>
Update the vLLM backend to support structured output: - Import GuidedDecodingParams from vllm.sampling_params - Handle JSONSchema: parse and pass as GuidedDecodingParams(json_schema=...) - Handle json_object response format: GuidedDecodingParams(json_object=True) - Fall back to Grammar (GBNF) via GuidedDecodingParams(grammar=...) - Remove phantom GuidedDecoding mapping (field doesn't exist in proto) - Fix missing 'import time' and 'import json' for load_video and schema parsing Priority: JSONSchema > json_object > Grammar (GBNF fallback) Ref: mudler#6857 Signed-off-by: eureka928 <meobius123@gmail.com>
- Make GuidedDecodingParams import conditional (try/except) for backwards compatibility with older vLLM versions - Remove GBNF grammar fallback — vLLM expects EBNF, not GBNF, so passing LocalAI's GBNF grammar would produce confusing errors - Pass JSONSchema as string directly instead of parsing to dict (safer across vLLM versions) - Add GBNF grammar generation for json_schema in completion endpoint so non-vLLM backends (llama.cpp) also get grammar enforcement Ref: mudler#6857 Signed-off-by: eureka928 <meobius123@gmail.com>
- Handle both StructuredOutputsParams (vLLM latest) and GuidedDecodingParams (vLLM <=0.8.x) with graceful fallback - Use the correct SamplingParams field name for each version (structured_outputs vs guided_decoding) - Use 'json' parameter (not 'json_schema') matching both APIs - Re-add grammar (GBNF/BNF) passthrough — both vLLM APIs accept a 'grammar' parameter handled by xgrammar which supports GBNF - Priority: JSONSchema > json_object > Grammar Ref: mudler#6857 Signed-off-by: eureka928 <meobius123@gmail.com>
Update the compatibility notice to include vLLM alongside llama.cpp. Add a vLLM-specific section with examples for all three supported methods: json_schema, json_object, and grammar (via xgrammar). Ref: mudler#6857 Signed-off-by: eureka928 <meobius123@gmail.com>
…ctured output Address review feedback: - Remove JSONSchema and ResponseFormat proto fields; pass them via the existing Metadata map instead, avoiding proto changes - vLLM backend reads json_schema and response_format from request.Metadata - Add structured output support (json_schema, json_object) to Open Responses API via text_format parameter - Update docs with Open Responses structured output examples Ref: mudler#6857 Signed-off-by: eureka928 <meobius123@gmail.com>
c703a03 to
d074a44
Compare
richiejp
left a comment
There was a problem hiding this comment.
There is a regression risk here, but it's the same whenever we expose more backend functionality changing the semantics of requests.
localai-bot
left a comment
There was a problem hiding this comment.
Thanks for this PR, and sorry it sat for so long. There is real value in here (two genuine endpoint gaps you found and fixed), but the PR has drifted from its description after the rebase, and a large part of it is now dead code because master evolved in the meantime. Detailed review below.
Description vs. diff
The description talks about backend.proto fields 52/53, backend.py changes, and a missing import time fix. None of that is in the current diff, which touches 5 files: options.go, chat.go, completion.go, responses.go, and docs. Since this PR was opened, #10343 landed StructuredOutputsParams/GuidedDecodingParams handling in the vLLM backend on master, which covered the original core intent: vLLM already consumes request.Grammar (JSON schema or GBNF) today. Please rewrite the description (and probably the title, since the diff no longer touches any vLLM code) to match what the PR does now.
Issues
1. The new metadata keys are write-only (dead code). config.RequestMetadata["json_schema"] (chat.go, completion.go, responses.go) and metadata["response_format"] (options.go) are consumed by no backend. The vLLM backend only reads request.Grammar (backend.py:558-564), and llama.cpp's grpc-server only reads chat_template_kwargs and the reasoning keys from metadata (its data.contains("json_schema") at grpc-server.cpp:1881 reads the predict-options JSON, not metadata). So with this diff, vLLM still receives the GBNF-converted grammar exactly as on master, not a native JSON schema.
2. The keys leak into jinja templates. gRPCPredictOpts folds every metadata key (including RequestMetadata) into the chat_template_kwargs blob via ResolveChatTemplateKwargs (core/config/model_config.go:828). With this PR, every json_schema request to llama.cpp injects the entire raw schema string plus response_format as chat template kwargs. Related nit: config.RequestMetadata aliases the client's input.Metadata map (core/http/middleware/request.go:346, no copy), so the endpoints end up mutating the parsed client request in place.
3. The "raw JSON schema" is lossy. json.Marshal(d.JsonSchema.Schema) round-trips the schema through functions.Item, which only has Type and Properties (pkg/functions/function_structure.go:9-12). required, enum, additionalProperties, $defs, and everything else is silently dropped; your own doc example uses "required": ["name", "age"], which would be lost. If this plumbing stays, the schema must be marshaled from the raw config.ResponseFormatMap instead.
4. The Open Responses docs example is wrong. convertTextFormatToResponseFormat (responses.go:801) expects the flat Responses-API form ({"type": "json_schema", "name": ..., "schema": ...}) and wraps it under a json_schema key. The new docs show the nested chat-style form; feed that in and the JsonSchemaRequest unmarshal lands on the wrong level, Schema comes out empty, and a grammar is silently generated from an empty schema. Either fix the example or make the conversion accept both shapes.
What I'd like to keep
- completion.go:
/v1/completionsonly handledjson_objecton master; addingjson_schemagrammar generation there is a real gap fix. - responses.go: on master,
text_formatis converted intoopenAIReq.ResponseFormatand then nothing constrains generation (that internal request never passes back through the middleware). Generatingcfg.Grammarthere is a real fix, and the ordering is right: the function-calling grammar block runs later and correctly wins when tools are in play. - The docs coverage for structured output on the Responses API, once the example is corrected and the vLLM wording is adjusted ("via GBNF/xgrammar" rather than "native JSON schema enforcement").
Smaller points
- The ~20-line marshal/unmarshal/grammar block is now copy-pasted in three endpoints; please extract a shared helper.
- The nested
if err == nilchains silently swallow failures; a schema that fails to parse should at least log, since the user asked for constrained output and won't get it. - completion.go re-marshals
config.ResponseFormatMapalthoughdatfrom the enclosing scope already holds exactly that. - Overloading
config.ResponseFormat(today an image-endpoint string,b64_json) to carry chat modes works because the config is a per-request copy, but it muddies the field's meaning. If the metadata plumbing goes, this should go too. - No tests cover the two genuine fixes; a small Ginkgo test exercising
json_schemaon/v1/completionsandtext_formaton/v1/responseswould lock them in.
Suggested path forward
Trim the PR down to the completion.go and responses.go grammar fixes plus corrected docs, and drop the RequestMetadata["json_schema"] / metadata["response_format"] / config.ResponseFormat plumbing (or, if you want native schema pass-through for vLLM, add the consumer in backend.py in the same PR and pass the truly raw schema). CI is green and DCO is signed, so once trimmed this can move quickly.
Description
This PR fixes #6857
Adds grammar and structured output support to the vLLM backend, enabling users to enforce structured outputs via JSON schema, JSON object, and BNF/GBNF grammar constraints.
Problem
The vLLM backend ignored all structured output parameters:
Grammarfield from the proto was never readGuidedDecodingmapping referenced a non-existent proto fieldresponse_formatwithjson_schemaorjson_objecthad no effect on vLLMimport timecaused a runtime crash on video inputSolution
Proto (
backend.proto):JSONSchema(field 52) andResponseFormat(field 53) toPredictOptions, allowing backends to receive the raw JSON schema and format type nativelyGo endpoints (
chat.go,completion.go):response_format: {type: "json_schema", ...}and store it onconfig.JSONSchemaconfig.ResponseFormatto the format type (json_object/json_schema)json_schemagrammar support to the completion endpoint (was missing)Go backend (
options.go,model_config.go):JSONSchemaandResponseFormatthroughgRPCPredictOptsto backendsvLLM backend (
backend.py):StructuredOutputsParams(vLLM latest) andGuidedDecodingParams(vLLM <=0.8.x) with graceful import fallbackJSONSchema>json_object>Grammarimport timeandimport jsonGuidedDecodingmappingDocs (
constrained_grammars.md):How It Works
Verification
GuidedDecodingParams.jsonandStructuredOutputsParams.jsonboth accept JSON stringsgrammar_is_likely_lark()in vLLM correctly identifies GBNF as non-Lark (via::=detection)config.ResponseFormatusage (image endpointb64_jsongoes through a different code path)Notes for Reviewers
backend.protois committedGuidedDecodingParams/guided_decoding) and latest (StructuredOutputsParams/structured_outputs)Signed commits
@mudler