[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