@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / fd267bc7b7
Fork 0

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)