[Bugfix] Support repsonse_format + tool_choice=auto (#56086)
Signed-off-by: Artem Perevedentsev <aperevedents@nvidia.com> Co-authored-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Co-authored-by: pablopupo <145598901+pablopupo@users.noreply.github.com> Co-authored-by: hubunt <150658615+hubunt@users.noreply.github.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Artem Perevedentsev committed
Sep 27, 2026 at 20:17 UTC
2b9b55c7f1bd344d07ca5545cd31735e580b2d1a
6 files changed
+472
-62
tests/parser/test_abstract_parser.py
new
+305
@@ -0,0 +1,305 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+import json
5
+from typing import Any
6
+from unittest.mock import MagicMock, patch
7
+
8
+import pytest
9
+from xgrammar import Grammar
10
+from xgrammar.testing import _is_grammar_accept_string
11
+
12
+from vllm.entrypoints.openai.chat_completion.protocol import (
13
+ ChatCompletionRequest,
14
+ ChatCompletionToolsParam,
15
+)
16
+from vllm.parser.abstract_parser import DelegatingParser
17
+from vllm.sampling_params import StructuredOutputsParams
18
+from vllm.tool_parsers.abstract_tool_parser import ToolParser
19
+from vllm.tool_parsers.qwen3_engine_tool_parser import Qwen3EngineToolParser
20
+from vllm.tool_parsers.structural_tag_registry import ToolChoice
21
+
22
+
23
+class TestToolChoice_Plus_ResponseFormat:
24
+ """Note(arpera):
25
+ Test cases for tool_choice={auto,required} + response_format
26
+ To keep it short:
27
+ DelegatingParser.adjust_request behavior in some corner cases is checked there
28
+
29
+ Initial bug report:
30
+ https://github.com/vllm-project/vllm/issues/39929
31
+ And PR that fixed this:
32
+ https://github.com/vllm-project/vllm/pull/56086
33
+ """
34
+
35
+ # ================================
36
+ # Helper methods
37
+ # ================================
38
+
39
+ @staticmethod
40
+ def _tools(strict: bool) -> list[ChatCompletionToolsParam]:
41
+ """Single get_weather tool, optionally marked as strict"""
42
+ function: dict[str, Any] = {
43
+ "name": "get_weather",
44
+ "parameters": {
45
+ "type": "object",
46
+ "properties": {"city": {"type": "string"}},
47
+ "required": ["city"],
48
+ },
49
+ }
50
+ if strict:
51
+ function["strict"] = True
52
+ return [ChatCompletionToolsParam(type="function", function=function)]
53
+
54
+ @staticmethod
55
+ def _qwen_tool_call() -> str:
56
+ """Tool call for Qwen model that do support structural tag"""
57
+ return (
58
+ "<tool_call>\n<function=get_weather>\n<parameter=city>\nParis\n"
59
+ "</parameter>\n</function>\n</tool_call>"
60
+ )
61
+
62
+ @staticmethod
63
+ def _json_schema_response_format() -> dict:
64
+ return {
65
+ "type": "json_schema",
66
+ "json_schema": {
67
+ "name": "answer",
68
+ "schema": {
69
+ "type": "object",
70
+ "properties": {"text": {"type": "string"}},
71
+ "required": ["text"],
72
+ },
73
+ },
74
+ }
75
+
76
+ @staticmethod
77
+ def _setup_request(
78
+ tools: list[ChatCompletionToolsParam],
79
+ tool_choice: ToolChoice,
80
+ response_format: dict,
81
+ ) -> ChatCompletionRequest:
82
+ request = ChatCompletionRequest(
83
+ messages=[], # for our test cases it's always empty
84
+ model="m", # Just a placeholder, don't pay much attention
85
+ tools=tools,
86
+ tool_choice=tool_choice,
87
+ response_format=response_format,
88
+ )
89
+ return request
90
+
91
+ @staticmethod
92
+ def _setup_abstract_parser(
93
+ tools: list[ChatCompletionToolsParam],
94
+ ) -> DelegatingParser:
95
+ """Construct parser that does NOT support structural tag"""
96
+
97
+ class TestParser(DelegatingParser):
98
+ tool_parser_cls = ToolParser
99
+
100
+ return TestParser(MagicMock(), tools=tools)
101
+
102
+ @staticmethod
103
+ def _setup_qwen_parser(
104
+ tools: list[ChatCompletionToolsParam],
105
+ ) -> DelegatingParser:
106
+ """Construct parser that supports structural tag"""
107
+
108
+ class TestParser(DelegatingParser):
109
+ tool_parser_cls = Qwen3EngineToolParser
110
+
111
+ return TestParser(MagicMock(), tools=tools)
112
+
113
+ # ================================
114
+ # Test cases
115
+ # tool_choice=auto + response_format
116
+ # ================================
117
+
118
+ @pytest.mark.parametrize(
119
+ # In this test we check that for response_format
120
+ # resulting grammar accepts @compliant_output and rejects @non_compliant_output
121
+ # You can add more examples here if you see some corner cases not covered
122
+ ("response_format", "compliant_output", "non_compliant_output"),
123
+ [
124
+ (_json_schema_response_format(), '{"text": "hi"}', '{"foo": 1}'),
125
+ ({"type": "json_object"}, '{"any": 1}', "[1, 2]"),
126
+ ],
127
+ # We test here two different response_format types:
128
+ ids=["json_schema", "json_object"],
129
+ )
130
+ def test_auto_with_strict_tools(
131
+ self,
132
+ response_format: dict,
133
+ compliant_output: str,
134
+ non_compliant_output: str,
135
+ ):
136
+ tools = self._tools(strict=True)
137
+ request = self._setup_request(
138
+ tools=tools,
139
+ tool_choice="auto",
140
+ response_format=response_format,
141
+ )
142
+ parser = self._setup_qwen_parser(tools)
143
+ out = parser.adjust_request(request)
144
+
145
+ # Now check that request does not have response_format anymore
146
+ # but instead has structured_outputs set as structural tag
147
+ # And this structural tag is OR operation
148
+ assert out.tool_choice == "auto"
149
+ assert out.response_format is None
150
+ assert out.structured_outputs is not None
151
+ tag = json.loads(out.structured_outputs.structural_tag)
152
+ assert tag["format"]["type"] == "or"
153
+ grammar = Grammar.from_structural_tag(out.structured_outputs.structural_tag)
154
+
155
+ assert _is_grammar_accept_string(grammar, compliant_output)
156
+ assert not _is_grammar_accept_string(grammar, non_compliant_output)
157
+
158
+ # Also tool call must be accepted by grammar
159
+ assert _is_grammar_accept_string(grammar, self._qwen_tool_call())
160
+
161
+ # IMPORTANT(arpera): Regression test
162
+ # If we in adjust_request implementation by mistake
163
+ # construct structural tag using tool_choice=auto
164
+ # then such a structural tag would allow plain text as well
165
+ # We need to be sure that plain text is NOT accepted in our case
166
+ assert not _is_grammar_accept_string(grammar, "Hello")
167
+
168
+ def test_auto_without_strict_tools(self):
169
+ tools = self._tools(strict=False)
170
+ request = self._setup_request(
171
+ tools=tools,
172
+ tool_choice="auto",
173
+ response_format=self._json_schema_response_format(),
174
+ )
175
+ parser = self._setup_qwen_parser(tools)
176
+
177
+ # There must be a warning that tool calls are disabled
178
+ # Consume that warning
179
+ with patch("vllm.parser.abstract_parser.logger.warning_once") as mock_warn:
180
+ out = parser.adjust_request(request)
181
+
182
+ assert out.response_format is not None
183
+ assert out.structured_outputs is None
184
+ mock_warn.assert_called_once()
185
+
186
+ def test_when_model_does_not_have_structural_tag(self):
187
+ """Note(arpera):
188
+ When model does NOT have structural tag support
189
+ we apply constraint only for response_format
190
+ """
191
+ tools = self._tools(strict=True)
192
+ request = self._setup_request(
193
+ tools=tools,
194
+ tool_choice="auto",
195
+ response_format=self._json_schema_response_format(),
196
+ )
197
+ # SIC! use parser whose model does NOT support structural tag
198
+ parser = self._setup_abstract_parser(tools)
199
+
200
+ with patch("vllm.parser.abstract_parser.logger.warning_once") as mock_warn:
201
+ out = parser.adjust_request(request)
202
+
203
+ assert out.response_format is not None
204
+ assert out.structured_outputs is None
205
+ mock_warn.assert_called_once()
206
+
207
+ def test_auto_with_xgrammar_unsupported_schema(self):
208
+ """Conrer case based on Vadim's feedback in PR #56086
209
+ https://github.com/vllm-project/vllm/pull/56086#pullrequestreview-5329203459
210
+ """
211
+ tools = self._tools(strict=True)
212
+ request = self._setup_request(
213
+ tools=tools,
214
+ tool_choice="auto",
215
+ response_format={
216
+ "type": "json_schema",
217
+ "json_schema": {
218
+ "name": "answer",
219
+ "schema": {"type": "integer", "multipleOf": 2},
220
+ },
221
+ },
222
+ )
223
+ parser = self._setup_qwen_parser(tools)
224
+
225
+ with patch("vllm.parser.abstract_parser.logger.warning_once") as mock_warn:
226
+ out = parser.adjust_request(request)
227
+
228
+ assert out.response_format is not None
229
+ assert out.structured_outputs is None
230
+ mock_warn.assert_called_once()
231
+
232
+ def test_auto_with_xgrammar_unsupported_nested_schema(self):
233
+ """Conrer case based on Vadim's feedback in PR #56086
234
+ https://github.com/vllm-project/vllm/pull/56086#pullrequestreview-5329203459
235
+ """
236
+ tools = self._tools(strict=True)
237
+ request = self._setup_request(
238
+ tools=tools,
239
+ tool_choice="auto",
240
+ response_format={
241
+ "type": "json_schema",
242
+ "json_schema": {
243
+ "name": "answer",
244
+ "schema": {
245
+ "type": "object",
246
+ "properties": {"value": {"type": "number", "multipleOf": 0.5}},
247
+ "required": ["value"],
248
+ },
249
+ },
250
+ },
251
+ )
252
+ parser = self._setup_qwen_parser(tools)
253
+
254
+ with patch("vllm.parser.abstract_parser.logger.warning_once") as mock_warn:
255
+ out = parser.adjust_request(request)
256
+
257
+ assert out.response_format is not None
258
+ assert out.structured_outputs is None
259
+ mock_warn.assert_called_once()
260
+
261
+ def test_auto_with_lark_grammar(self):
262
+ """Conrer case based on Vadim's feedback in PR #56086
263
+ https://github.com/vllm-project/vllm/pull/56086#pullrequestreview-5329203459
264
+ Lark grammars are converted to EBNF before being merged into the tag.
265
+ """
266
+ tools = self._tools(strict=True)
267
+ request = ChatCompletionRequest(
268
+ messages=[],
269
+ model="m",
270
+ tools=tools,
271
+ tool_choice="auto",
272
+ structured_outputs=StructuredOutputsParams(grammar='start: "ok"'),
273
+ )
274
+ parser = self._setup_qwen_parser(tools)
275
+ out = parser.adjust_request(request)
276
+
277
+ assert out.structured_outputs is not None
278
+ grammar = Grammar.from_structural_tag(out.structured_outputs.structural_tag)
279
+ assert _is_grammar_accept_string(grammar, "ok")
280
+ assert not _is_grammar_accept_string(grammar, "bad")
281
+ assert _is_grammar_accept_string(grammar, self._qwen_tool_call())
282
+
283
+ # ================================
284
+ # Test cases
285
+ # tool_choice=required + response_format
286
+ # ================================
287
+
288
+ def test_required(self):
289
+ tools = self._tools(strict=True)
290
+ request = self._setup_request(
291
+ tools=tools,
292
+ tool_choice="required",
293
+ response_format=self._json_schema_response_format(),
294
+ )
295
+ parser = self._setup_qwen_parser(tools)
296
+
297
+ with patch("vllm.parser.abstract_parser.logger.warning_once") as mock_warn:
298
+ out = parser.adjust_request(request)
299
+
300
+ assert out.response_format is None
301
+ assert out.structured_outputs is not None
302
+ grammar = Grammar.from_structural_tag(out.structured_outputs.structural_tag)
303
+ assert _is_grammar_accept_string(grammar, self._qwen_tool_call())
304
+ assert not _is_grammar_accept_string(grammar, '{"text": "hi"}')
305
+ mock_warn.assert_called_once()
tests/parser/test_harmony.py
+16
@@ -1196,6 +1196,21 @@ class TestAdjustRequest:
1196
"TOOL_CALL_2_FUNCTION_FIRST",
1197
],
1198
),
1199
+ (
1200
+ {
1201
+ "tool_choice": "auto",
1202
+ "strict_tools": True,
1203
+ "response_format_type": "json_schema",
1204
+ },
1205
+ [
1206
+ "COMMENTARY",
1207
+ "TOOL_CALL_1_CHANNEL_FIRST",
1208
+ "TOOL_CALL_1_FUNCTION_FIRST",
1209
+ "TOOL_CALL_2_CHANNEL_FIRST",
1210
+ "TOOL_CALL_2_FUNCTION_FIRST",
1211
+ "FINAL_JSON_SCHEMA",
1212
+ ],
1213
+ ),
1214
(
1215
{"response_format_type": "json_schema"},
1216
["FINAL_JSON_SCHEMA"],
@@ -1281,6 +1296,7 @@ class TestAdjustRequest:
1296
"tool_auto_strict",
1297
"tool_required",
1298
"tool_named",
1299
+ "pr56086_tool_auto_strict_response_format_json_schema",
1300
"response_format_json_schema",
1301
"response_format_json_object",
1302
"structured_outputs_json",
vllm/parser/abstract_parser.py
+138
-16
@@ -5,10 +5,21 @@ import contextlib
5
import json
6
from abc import abstractmethod
7
from collections.abc import Sequence
8
-from dataclasses import dataclass, field
8
+from dataclasses import dataclass, field, replace
9
10
from openai.types.responses import ToolChoiceFunction
11
from pydantic import TypeAdapter, ValidationError
12
+from xgrammar import Grammar, StructuralTag
13
+from xgrammar.structural_tag import (
14
+ ConstStringFormat,
15
+ Format,
16
+ GrammarFormat,
17
+ JSONSchemaFormat,
18
+ OrFormat,
19
+ RegexFormat,
20
+ TagFormat,
21
+ TriggeredTagsFormat,
22
+)
23
24
from vllm.entrypoints.chat_utils import (
25
get_tool_call_id_type,
@@ -25,17 +36,19 @@ from vllm.entrypoints.openai.chat_completion.protocol import (
36
ChatCompletionRequest,
37
)
38
from vllm.entrypoints.openai.responses.protocol import ResponsesRequest
39
+from vllm.exceptions import VLLMValidationError
40
from vllm.logger import init_logger
41
from vllm.parser.metrics import record_tool_parser_invocation
42
from vllm.parser.utils import count_history_tool_calls
43
from vllm.reasoning.abs_reasoning_parsers import ReasoningParser
32
-from vllm.sampling_params import StructuredOutputsParams
44
+from vllm.sampling_params import SamplingParams, StructuredOutputsParams
45
from vllm.tokenizers import TokenizerLike
46
from vllm.tool_parsers.abstract_tool_parser import Tool, ToolParser
47
from vllm.tool_parsers.streaming import (
48
extract_named_tool_call_streaming,
49
extract_required_tool_call_streaming,
50
)
51
+from vllm.tool_parsers.structural_tag_registry import resolve_tool_strictness
52
from vllm.tool_parsers.tool_strict_level import ToolStrictLevel
53
54
logger = init_logger(__name__)
@@ -236,6 +249,67 @@ class Parser:
249
return 0
250
251
252
+def structured_outputs_to_format(params: StructuredOutputsParams) -> Format | None:
253
+ """Map StructuredOutputsParams in a XGrammar Format."""
254
+ if params.json_object:
255
+ return JSONSchemaFormat(json_schema={"type": "object"})
256
+ if params.json is not None:
257
+ schema = params.json
258
+ if isinstance(schema, str):
259
+ schema = json.loads(schema)
260
+ return JSONSchemaFormat(json_schema=schema)
261
+ if params.regex is not None:
262
+ return RegexFormat(pattern=params.regex)
263
+ if params.choice is not None:
264
+ return OrFormat(
265
+ elements=[ConstStringFormat(value=choice) for choice in params.choice]
266
+ )
267
+ if params.grammar is not None:
268
+ from vllm.v1.structured_output.utils import grammar_is_likely_lark
269
+
270
+ grammar = params.grammar
271
+ # IMPORTANT(arpera):
272
+ # GrammarFormat only accepts EBNF.
273
+ if grammar_is_likely_lark(grammar):
274
+ try:
275
+ grammar = str(Grammar.from_lark(grammar))
276
+ except Exception as e:
277
+ raise VLLMValidationError("Invalid grammar specification.") from e
278
+ return GrammarFormat(grammar=grammar)
279
+ if params.structural_tag is not None:
280
+ s_tag = json.loads(params.structural_tag)
281
+ if "structures" in s_tag:
282
+ # LegacyStructuralTagResponseFormat
283
+ return TriggeredTagsFormat(
284
+ triggers=s_tag["triggers"],
285
+ tags=[
286
+ TagFormat(
287
+ begin=structure["begin"],
288
+ content=JSONSchemaFormat(json_schema=structure["schema"]),
289
+ end=structure["end"],
290
+ )
291
+ for structure in s_tag["structures"]
292
+ ],
293
+ )
294
+ # StructuralTagResponseFormat
295
+ return StructuralTag.model_validate(s_tag).format
296
+ return None
297
+
298
+
299
+def _xgrammar_supports(structured_outputs: StructuredOutputsParams) -> bool:
300
+ """Whether the constraint passes xgrammar validation on its own."""
301
+ from vllm.v1.structured_output.backend_xgrammar import validate_xgrammar_grammar
302
+
303
+ try:
304
+ # validate_xgrammar_grammar rewrites `choice` into `grammar` in place.
305
+ validate_xgrammar_grammar(
306
+ SamplingParams(structured_outputs=replace(structured_outputs))
307
+ )
308
+ except (TypeError, ValueError, VLLMValidationError):
309
+ return False
310
+ return True
311
+
312
+
313
class DelegatingParser(Parser):
314
"""A Parser implementation that delegates to separate ReasoningParser and
315
ToolParser instances.
@@ -390,18 +464,14 @@ class DelegatingParser(Parser):
464
request = self._reasoning_parser.adjust_request(request)
465
if self._tool_parser is not None:
466
request = self._apply_structural_tag(request)
393
- if self._tool_parser is not None:
467
request = self._tool_parser.adjust_request(request)
468
return request
469
470
def _apply_structural_tag(
471
self, request: ChatCompletionRequest | ResponsesRequest
472
) -> ChatCompletionRequest | ResponsesRequest:
400
- if (
401
- self._tool_parser is None
402
- or self._tool_parser.structural_tag_model is None
403
- or not request.tools
404
- ):
473
+ tool_parser = self._tool_parser
474
+ if tool_parser is None or not request.tools:
475
return request
476
477
need_tool_calling = (
@@ -415,17 +485,69 @@ class DelegatingParser(Parser):
485
if not need_tool_calling:
486
return request
487
418
- structure_tag = self._tool_parser.get_structural_tag(
419
- request,
420
- reasoning=False,
421
- strict_level=self.tool_strict_level,
422
- )
423
- if structure_tag is None:
488
+ structured_outputs = request.extract_structured_outputs()
489
+ is_auto = request.tool_choice == "auto"
490
+
491
+ resolved_tools = None
492
+ if tool_parser.structural_tag_model is not None:
493
+ resolved_tools = resolve_tool_strictness(
494
+ request.tools,
495
+ request.tool_choice,
496
+ self.tool_strict_level,
497
+ )
498
+
499
+ output_format = None
500
+ if resolved_tools and is_auto and structured_outputs:
501
+ if not _xgrammar_supports(structured_outputs):
502
+ logger.warning_once(
503
+ "Tool calls are not constrained for tool_choice=auto with "
504
+ "structured outputs because xgrammar does not support the "
505
+ "structured output constraint; the structured output "
506
+ "constraint applies.",
507
+ scope="local",
508
+ )
509
+ return request
510
+ output_format = structured_outputs_to_format(structured_outputs)
511
+
512
+ if resolved_tools is not None:
513
+ tag_request = request
514
+ if output_format is not None:
515
+ # IMPORTANT(arpera):
516
+ # The "auto" tag allows plain text in response.
517
+ # Use tag "required" here to ensure structured-output branch
518
+ # remains meaningful.
519
+ tag_request = request.model_copy(update={"tool_choice": "required"})
520
+ tools_structural_tag = tool_parser.get_structural_tag(
521
+ tag_request,
522
+ reasoning=False,
523
+ strict_level=self.tool_strict_level,
524
+ )
525
+ else:
526
+ tools_structural_tag = None
527
+
528
+ if tools_structural_tag is None:
529
+ if is_auto and structured_outputs is not None:
530
+ logger.warning_once(
531
+ "Tool calls are not constrained for tool_choice=auto with "
532
+ "structured outputs because structural tags are unavailable; "
533
+ "the structured output constraint applies.",
534
+ scope="local",
535
+ )
536
return request
537
426
- structural_tag = json.dumps(structure_tag.model_dump())
538
+ structural_tag = tools_structural_tag
539
+ if output_format is not None:
540
+ structural_tag = StructuralTag(
541
+ format=OrFormat(elements=[tools_structural_tag.format, output_format])
542
+ )
543
+ elif structured_outputs is not None and not is_auto:
544
+ logger.warning_once(
545
+ "structured outputs are ignored because tool_choice forces tool call.",
546
+ scope="local",
547
+ )
548
+
549
request.structured_outputs = StructuredOutputsParams(
428
- structural_tag=structural_tag,
550
+ structural_tag=json.dumps(structural_tag.model_dump()),
551
)
552
if isinstance(request, ResponsesRequest):
553
request.text = None
vllm/parser/cohere_command.py
+9
@@ -563,6 +563,15 @@ class CohereCommandParser(DelegatingParser):
563
request.response_format = None
564
return request
565
566
+ def _apply_structural_tag(
567
+ self, request: ChatCompletionRequest | ResponsesRequest
568
+ ) -> ChatCompletionRequest | ResponsesRequest:
569
+ # Note(arpera):
570
+ # No need to go through _apply_structural_tag in abstract_parser.py
571
+ # since we have already go through _apply_structural_tags here
572
+ # So, override this method to return request as is
573
+ return request
574
+
575
def count_reasoning_tokens(self, token_ids: Sequence[int]) -> int:
576
count = 0
577
depth = 0
vllm/parser/harmony.py
+2
-44
@@ -16,14 +16,11 @@ from xgrammar.structural_tag import (
16
AnyTextFormat,
17
ConstStringFormat,
18
Format,
19
- GrammarFormat,
19
JSONSchemaFormat,
20
OptionalFormat,
21
OrFormat,
23
- RegexFormat,
22
SequenceFormat,
23
TagFormat,
26
- TriggeredTagsFormat,
24
)
25
26
from vllm.entrypoints.chat_utils import make_tool_call_id
@@ -44,9 +41,8 @@ from vllm.entrypoints.openai.parser.harmony_utils import (
41
)
42
from vllm.entrypoints.openai.responses.protocol import ResponsesRequest
43
from vllm.logger import init_logger
47
-from vllm.parser.abstract_parser import DelegatingParser
44
+from vllm.parser.abstract_parser import DelegatingParser, structured_outputs_to_format
45
from vllm.reasoning.gptoss_reasoning_parser import GptOssReasoningParser
49
-from vllm.sampling_params import StructuredOutputsParams
46
from vllm.tool_parsers.gptoss_tool_parser import GptOssToolParser
47
from vllm.tool_parsers.structural_tag_registry import (
48
SimplifiedToolChoice,
@@ -428,7 +424,6 @@ _FUNCTION_CALL_BEGINS = [
424
" to=functions.{name}{channel}{constrain}<|message|>",
425
"{channel} to=functions.{name}{constrain}<|message|>",
426
]
431
-_JSON_CONTENT = JSONSchemaFormat(json_schema={"type": "object"})
427
_ANY_CONTENT = AnyTextFormat()
428
429
@@ -531,43 +526,6 @@ def get_harmony_structural_tag(
526
)
527
528
534
-def _params_to_final_content(params: StructuredOutputsParams) -> Format | None:
535
- """Map StructuredOutputsParams in a XGrammar Format."""
536
- if params.json_object:
537
- return _JSON_CONTENT
538
- if params.json is not None:
539
- schema = params.json
540
- if isinstance(schema, str):
541
- schema = json.loads(schema)
542
- return JSONSchemaFormat(json_schema=schema)
543
- if params.regex is not None:
544
- return RegexFormat(pattern=params.regex)
545
- if params.choice is not None:
546
- return OrFormat(
547
- elements=[ConstStringFormat(value=choice) for choice in params.choice]
548
- )
549
- if params.grammar is not None:
550
- return GrammarFormat(grammar=params.grammar)
551
- if params.structural_tag is not None:
552
- s_tag = json.loads(params.structural_tag)
553
- if "structures" in s_tag:
554
- # LegacyStructuralTagResponseFormat
555
- return TriggeredTagsFormat(
556
- triggers=s_tag["triggers"],
557
- tags=[
558
- TagFormat(
559
- begin=structure["begin"],
560
- content=JSONSchemaFormat(json_schema=structure["schema"]),
561
- end=structure["end"],
562
- )
563
- for structure in s_tag["structures"]
564
- ],
565
- )
566
- # StructuralTagResponseFormat
567
- return StructuralTag.model_validate(s_tag).format
568
- return None
569
-
570
-
529
def _adjust_output_format(
530
request: ChatCompletionRequest | ResponsesRequest,
531
) -> ChatCompletionRequest | ResponsesRequest:
@@ -576,7 +534,7 @@ def _adjust_output_format(
534
if params is None:
535
return request
536
579
- final_content = _params_to_final_content(params)
537
+ final_content = structured_outputs_to_format(params)
538
if final_content is None:
539
return request
540
vllm/tool_parsers/structural_tag_registry.py
+2
-2
@@ -121,7 +121,7 @@ def _with_tool_strict(
121
return tool
122
123
124
-def _resolve_tool_strictness(
124
+def resolve_tool_strictness(
125
tools: Sequence[ChatCompletionToolsParam | ResponsesTool],
126
tool_choice: ToolChoice,
127
strict_level: ToolStrictLevel,
@@ -158,7 +158,7 @@ def get_model_structural_tag(
158
if not tools or tool_choice == "none":
159
return None
160
161
- tools = _resolve_tool_strictness(tools, tool_choice, strict_level)
161
+ tools = resolve_tool_strictness(tools, tool_choice, strict_level)
162
if tools is None:
163
return None
164