A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 8, 07:30 UTC
[Bugfix]: Fix structured output in multi-turn gpt-oss (#34454)
Signed-off-by: Ben Browning <bbrownin@redhat.com> Co-authored-by: Cyrus Leung <cyrus.tl.leung@gmail.com> Co-authored-by: Cyrus Leung <tlleungac@connect.ust.hk>
Ben Browning committed
Feb 13, 2026 at 14:12 UTC
fd267bc7b7cd3d001ac5a893eacb9e56ff256822
4 files changed
+29
-1
tests/entrypoints/openai/test_gptoss_structural_tags_integration.py
+1
@@ -23,6 +23,7 @@ class TestGptOssStructuralTagsIntegration:
23
"""Create a mock tokenizer."""
24
tokenizer = Mock()
25
tokenizer.encode = Mock(return_value=[1, 2, 3, 4, 5])
26
+ tokenizer.vocab = {"<|end|>": 6}
27
return tokenizer
28
29
@pytest.fixture
tests/reasoning/test_gptoss_reasoning_parser.py
+18
-1
@@ -17,7 +17,9 @@ def gpt_oss_tokenizer():
17
18
USER_MESSAGE_START = "<|start|>user<|message|>"
19
REASONING_SECTION_START = "<|end|><|start|>assistant<|channel|>analysis<|message|>"
20
-ASSISTANT_CONTENT_START_PREFIX = "<|end|><|start|>assistant<|channel|>final"
20
+END = "<|end|>"
21
+ASSISTANT_START = "<|start|>assistant"
22
+ASSISTANT_CONTENT_START_PREFIX = END + ASSISTANT_START + "<|channel|>final"
23
ASSISTANT_CONTENT_START_SUFFIX = "<|message|>"
24
ASSISTANT_CONTENT_START = (
25
ASSISTANT_CONTENT_START_PREFIX + ASSISTANT_CONTENT_START_SUFFIX
@@ -97,6 +99,20 @@ COMPLEX_CONTENT_2 = {
99
"is_reasoning_end": True,
100
}
101
102
+MULTI_TURN_CONTENT = {
103
+ "output": USER_MESSAGE_START
104
+ + "1st turn user message"
105
+ + REASONING_SECTION_START
106
+ + "1st turn reasoning"
107
+ + ASSISTANT_CONTENT_START
108
+ + "1st turn response"
109
+ + END
110
+ + USER_MESSAGE_START
111
+ + "2nd turn user message"
112
+ + END
113
+ + ASSISTANT_START,
114
+ "is_reasoning_end": False,
115
+}
116
TEST_CASES = [
117
BASIC_CONTENT,
118
BASIC_REASONING_ONLY,
@@ -106,6 +122,7 @@ TEST_CASES = [
122
COMPLEX_CONTENT_1,
123
COMPLEX_CONTENT_1_WITH_CONTENT,
124
COMPLEX_CONTENT_2,
125
+ MULTI_TURN_CONTENT,
126
]
127
128
tests/v1/structured_output/test_gptoss_structural_tags.py
+1
@@ -25,6 +25,7 @@ class TestGptOssReasoningParser:
25
"""Create a mock tokenizer for testing."""
26
tokenizer = Mock()
27
tokenizer.encode = Mock(return_value=[1, 2, 3, 4, 5])
28
+ tokenizer.vocab = {"<|end|>": 6}
29
return tokenizer
30
31
@pytest.fixture
vllm/reasoning/gptoss_reasoning_parser.py
+9
@@ -76,6 +76,9 @@ class GptOssReasoningParser(ReasoningParser):
76
"<|channel|>final"
77
)
78
self.reasoning_end_token_ids_suffix = self.model_tokenizer.encode("<|message|>")
79
+ # We also need to check for the <|end|> token to avoid false positives from
80
+ # previous messages in multi-turn conversations.
81
+ self.eom_token_id = self.model_tokenizer.vocab["<|end|>"]
82
self.reasoning_max_num_between_tokens = 20
83
84
def is_reasoning_end(self, input_ids: Sequence[int]) -> bool:
@@ -86,6 +89,12 @@ class GptOssReasoningParser(ReasoningParser):
89
# Check if the end sequence is present in the input_ids.
90
# We search from the end of input_ids to find the last match.
91
for i in range(len(input_ids) - len(end_token_ids_prefix), -1, -1):
92
+ if input_ids[i] == self.eom_token_id:
93
+ # We looped backwards far enough to find the end of a previous message,
94
+ # which means we have searched the entirety of the current message
95
+ # and can exit early without searching further back into prior
96
+ # messages of the conversation.
97
+ return False
98
if input_ids[i : i + len(end_token_ids_prefix)] == end_token_ids_prefix:
99
# We have found the prefix, now we look for the suffix after the prefix.
100
suffix_start = i + len(end_token_ids_prefix)