[Bugfix][Structured Output] Preserve reasoning adapters for shared parser engines (#52830)
Signed-off-by: sfeng33 <4florafeng@gmail.com>
Flora Feng committed
Aug 26, 2026 at 00:37 UTC
46638857fdbb30e0c232c9e8f9cb1ff6d6f545c3
2 files changed
+27
-49
tests/parser/engine/test_parser_engine.py
+25
-19
@@ -893,7 +893,7 @@ class _CombinedDelegating(DelegatingParser):
893
tool_parser_cls = _CombinedToolAdapter
894
895
896
-def test_parser_manager_uses_shared_engine_directly(monkeypatch):
896
+def test_parser_manager_preserves_shared_engine_adapters(monkeypatch):
897
monkeypatch.setattr(
898
ParserManager,
899
"get_reasoning_parser",
@@ -912,17 +912,38 @@ def test_parser_manager_uses_shared_engine_directly(monkeypatch):
912
)
913
914
assert parser_cls is not None
915
- assert parser_cls is _CombinedTestEngine
915
+ assert issubclass(parser_cls, DelegatingParser)
916
parser = parser_cls(make_mock_tokenizer(_VOCAB))
917
+ assert parser.reasoning_parser is not None
918
+ assert parser.tool_parser is not None
919
+ assert parser.reasoning_parser._parser_engine_cls is _CombinedTestEngine
920
+ assert parser.tool_parser._parser_engine_cls is _CombinedTestEngine
921
request = _make_delegating_request()
922
+ token_ids = [ord("a"), ord("b"), 201, ord("c")]
923
reasoning, content, _ = parser.parse(
924
"ab</think>c",
925
request,
921
- model_output_token_ids=[ord("a"), ord("b"), 201, ord("c")],
926
+ model_output_token_ids=token_ids,
927
)
928
assert reasoning == "ab"
929
assert content == "c"
925
- assert parser.count_reasoning_tokens([]) == 2
930
+ assert parser.count_reasoning_tokens(token_ids) == 2
931
+
932
+
933
+def test_parser_manager_preserves_shared_engine_reasoning_wiring():
934
+ parser_cls = ParserManager.get_parser(
935
+ tool_parser_name="qwen3_xml",
936
+ reasoning_parser_name="qwen3",
937
+ enable_auto_tools=True,
938
+ )
939
+
940
+ assert parser_cls is not None
941
+ parser = parser_cls(
942
+ make_mock_tokenizer(_VOCAB),
943
+ chat_template_kwargs={"enable_thinking": False},
944
+ )
945
+ assert parser.reasoning_parser is not None
946
+ assert parser.reasoning_parser._parser_engine.thinking_enabled is False
947
948
949
def test_parser_manager_preserves_reasoning_only_adapter(monkeypatch):
@@ -983,21 +1004,6 @@ def test_parser_manager_preserves_tool_only_adapter(monkeypatch):
1004
assert tool_calls == []
1005
1006
986
-def test_parser_manager_rejects_non_engine_adapter_metadata():
987
- class TraditionalParser:
988
- pass
989
-
990
- class InvalidEngineAdapter:
991
- _parser_engine_cls = TraditionalParser
992
-
993
- assert ParserManager._get_parser_engine_cls(TraditionalParser) is None
994
- assert ParserManager._get_parser_engine_cls(InvalidEngineAdapter) is None
995
- assert (
996
- ParserManager._get_parser_engine_cls(_CombinedReasoningAdapter)
997
- is _CombinedTestEngine
998
- )
999
-
1000
-
1007
def test_reasoning_adapter_counts_after_final_non_streaming_parse():
1008
parser = _CombinedReasoningAdapter(make_mock_tokenizer(_VOCAB))
1009
request = _make_delegating_request()
vllm/parser/parser_manager.py
+2
-30
@@ -9,7 +9,6 @@ from vllm.logger import init_logger
9
10
if TYPE_CHECKING:
11
from vllm.parser.abstract_parser import Parser
12
- from vllm.parser.engine.parser_engine import ParserEngine
12
from vllm.reasoning import ReasoningParser
13
from vllm.tool_parsers import ToolParser
14
@@ -18,30 +17,9 @@ logger = init_logger(__name__)
17
18
class ParserManager:
19
"""
21
- Provides a unified Parser from the reasoning and tool parser registries.
22
-
23
- Parser engine adapters backed by the same engine are collapsed back into
24
- that engine. Other parser pairs are composed through ``DelegatingParser``.
20
+ Provides a unified Parser by composing reasoning and tool parser adapters.
21
"""
22
27
- @staticmethod
28
- def _get_parser_engine_cls(
29
- parser_cls: type[object] | None,
30
- ) -> type[ParserEngine] | None:
31
- if parser_cls is None:
32
- return None
33
- parser_engine_cls = getattr(parser_cls, "_parser_engine_cls", None)
34
- if parser_engine_cls is None:
35
- return None
36
-
37
- from vllm.parser.engine.parser_engine import ParserEngine
38
-
39
- if not isinstance(parser_engine_cls, type) or not issubclass(
40
- parser_engine_cls, ParserEngine
41
- ):
42
- return None
43
- return parser_engine_cls
44
-
23
@classmethod
24
def get_tool_parser(
25
cls,
@@ -105,8 +83,7 @@ class ParserManager:
83
"""
84
Get a Parser that handles both reasoning and tool parsing.
85
108
- Reuses a shared parser engine when possible, otherwise composes the
109
- individual parsers into a ``DelegatingParser`` subclass.
86
+ Composes the individual parsers into a ``DelegatingParser`` subclass.
87
88
Args:
89
tool_parser_name: The name of the tool parser.
@@ -137,11 +114,6 @@ class ParserManager:
114
HarmonyParser.tool_parser_cls = tool_parser_cls
115
return HarmonyParser
116
140
- reasoning_engine_cls = cls._get_parser_engine_cls(reasoning_parser_cls)
141
- tool_engine_cls = cls._get_parser_engine_cls(tool_parser_cls)
142
- if reasoning_engine_cls is not None and reasoning_engine_cls is tool_engine_cls:
143
- return reasoning_engine_cls
144
-
117
if reasoning_parser_name == "kimi_k3" or tool_parser_name == "kimi_k3":
118
from vllm.parser.kimi_k3 import KimiK3Parser
119