Skip to content

Commit 50d9216

Browse files
soldnixuebwang-amd
authored andcommitted
Add Olmo 3 reasoning parser (vllm-project#26054)
Signed-off-by: Luca Soldaini <luca@soldaini.net> Signed-off-by: xuebwang-amd <xuebwang@amd.com>
1 parent 373e325 commit 50d9216

File tree

3 files changed

+453
-0
lines changed

3 files changed

+453
-0
lines changed
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3+
4+
import pytest
5+
from transformers import AutoTokenizer
6+
7+
from tests.reasoning.utils import run_reasoning_extraction
8+
from vllm.reasoning import ReasoningParser, ReasoningParserManager
9+
10+
parser_name = "olmo3"
11+
START_REASONING = "<think>"
12+
END_REASONING = "</think>"
13+
14+
NO_REASONING = {
15+
"output": f"{START_REASONING}{END_REASONING}No thoughts, head empty!",
16+
"reasoning_content": None,
17+
"content": "No thoughts, head empty!",
18+
}
19+
20+
NO_REASONING_WITH_NEWLINE = {
21+
"output":
22+
f"{START_REASONING}\n{END_REASONING}\n\nNo thoughts, head empty!",
23+
"reasoning_content": "\n",
24+
"content": "\n\nNo thoughts, head empty!",
25+
}
26+
27+
SIMPLE_REASONING = {
28+
"output":
29+
f"{START_REASONING}This is a reasoning section{END_REASONING}This is the rest", # noqa: E501
30+
"reasoning_content": "This is a reasoning section",
31+
"content": "This is the rest",
32+
}
33+
34+
SIMPLE_REASONING_WITH_NEWLINE = {
35+
"output":
36+
f"{START_REASONING} Look!\n\nI'm thinking...{END_REASONING}\nThis is the rest", # noqa: E501
37+
"reasoning_content": " Look!\n\nI'm thinking...",
38+
"content": "\nThis is the rest",
39+
}
40+
41+
SIMPLE_REASONING_WITH_MULTIPLE_NEWLINES = {
42+
"output":
43+
f"{START_REASONING}\nLook!\nI'm thinking...\n\n{END_REASONING}\n\n\nThis is the rest", # noqa: E501
44+
"reasoning_content": "\nLook!\nI'm thinking...\n\n",
45+
"content": "\n\n\nThis is the rest",
46+
}
47+
48+
NO_REASONING_ONLY_END_THINK = {
49+
"output": f"{END_REASONING}\n\nNo thoughts, head empty!",
50+
"reasoning_content": None,
51+
"content": "\n\nNo thoughts, head empty!",
52+
}
53+
54+
REASONING_ONLY_END_THINK = {
55+
"output":
56+
f"The user is asking me not to think.{END_REASONING}No thoughts!",
57+
"reasoning_content": "The user is asking me not to think.",
58+
"content": "No thoughts!",
59+
}
60+
61+
TEST_CASES = [
62+
pytest.param(
63+
False, # not streaming
64+
NO_REASONING,
65+
id="no_reasoning",
66+
),
67+
pytest.param(
68+
False, # not streaming
69+
NO_REASONING_WITH_NEWLINE,
70+
id="no_reasoning_with_newline",
71+
),
72+
pytest.param(
73+
False, # not streaming
74+
SIMPLE_REASONING,
75+
id="simple_reasoning",
76+
),
77+
pytest.param(
78+
False, # not streaming
79+
SIMPLE_REASONING_WITH_NEWLINE,
80+
id="simple_reasoning_with_newline",
81+
),
82+
pytest.param(
83+
True, # enable streaming
84+
SIMPLE_REASONING_WITH_MULTIPLE_NEWLINES,
85+
id="simple_reasoning_with_multiple_newlines",
86+
),
87+
pytest.param(
88+
False, # not streaming
89+
NO_REASONING_ONLY_END_THINK,
90+
id="no_reasoning_only_end_think",
91+
),
92+
pytest.param(
93+
False, # not streaming
94+
REASONING_ONLY_END_THINK,
95+
id="yes_reasoning_only_end_think",
96+
),
97+
pytest.param(
98+
True, # enable streaming
99+
NO_REASONING,
100+
id="no_reasoning_streaming",
101+
),
102+
pytest.param(
103+
True, # enable streaming
104+
NO_REASONING_WITH_NEWLINE,
105+
id="no_reasoning_with_newline_streaming",
106+
),
107+
pytest.param(
108+
True, # enable streaming
109+
SIMPLE_REASONING,
110+
id="simple_reasoning_streaming",
111+
),
112+
pytest.param(
113+
True, # enable streaming
114+
SIMPLE_REASONING_WITH_NEWLINE,
115+
id="simple_reasoning_with_newline_streaming",
116+
),
117+
pytest.param(
118+
True, # enable streaming
119+
SIMPLE_REASONING_WITH_MULTIPLE_NEWLINES,
120+
id="simple_reasoning_with_multiple_newlines_streaming",
121+
),
122+
pytest.param(
123+
True, # enable streaming
124+
NO_REASONING_ONLY_END_THINK,
125+
id="no_reasoning_only_end_think_streaming",
126+
),
127+
pytest.param(
128+
True, # enable streaming
129+
REASONING_ONLY_END_THINK,
130+
id="yes_reasoning_only_end_think_streaming",
131+
),
132+
]
133+
134+
# Global tokenizer initialization to avoid repeated loading
135+
tokenizer = AutoTokenizer.from_pretrained("allenai/dolma2-tokenizer")
136+
137+
138+
@pytest.mark.parametrize("streaming, param_dict", TEST_CASES)
139+
def test_reasoning(
140+
streaming: bool,
141+
param_dict: dict[str, str],
142+
):
143+
output = tokenizer.tokenize(param_dict["output"])
144+
145+
# decode everything to tokens
146+
model_output: list[str] = [
147+
tokenizer.convert_tokens_to_string([token]) for token in output
148+
]
149+
parser_cls = ReasoningParserManager.get_reasoning_parser(parser_name)
150+
parser: ReasoningParser = parser_cls(tokenizer)
151+
152+
reasoning, content = run_reasoning_extraction(reasoning_parser=parser,
153+
model_output=model_output,
154+
streaming=streaming)
155+
156+
assert reasoning == param_dict["reasoning_content"]
157+
assert content == param_dict["content"]

vllm/reasoning/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from .granite_reasoning_parser import GraniteReasoningParser
1010
from .hunyuan_a13b_reasoning_parser import HunyuanA13BReasoningParser
1111
from .mistral_reasoning_parser import MistralReasoningParser
12+
from .olmo3_reasoning_parser import Olmo3ReasoningParser
1213
from .qwen3_reasoning_parser import Qwen3ReasoningParser
1314
from .seedoss_reasoning_parser import SeedOSSReasoningParser
1415
from .step3_reasoning_parser import Step3ReasoningParser
@@ -23,6 +24,7 @@
2324
"Qwen3ReasoningParser",
2425
"Glm4MoeModelReasoningParser",
2526
"MistralReasoningParser",
27+
"Olmo3ReasoningParser",
2628
"Step3ReasoningParser",
2729
"GptOssReasoningParser",
2830
"SeedOSSReasoningParser",

0 commit comments

Comments
 (0)