diff --git a/python/sglang/test/kits/reasoning_tokens_kit.py b/python/sglang/test/kits/reasoning_kit.py similarity index 57% rename from python/sglang/test/kits/reasoning_tokens_kit.py rename to python/sglang/test/kits/reasoning_kit.py index c3b02ad81..55274b759 100644 --- a/python/sglang/test/kits/reasoning_tokens_kit.py +++ b/python/sglang/test/kits/reasoning_kit.py @@ -1,5 +1,6 @@ import json +import openai import requests from sglang.srt.parser.reasoning_parser import ReasoningParser @@ -112,3 +113,85 @@ class ReasoningTokenUsageMixin: reported = data["meta_info"]["reasoning_tokens"] actual = data["output_ids"].index(self.think_end_token_id) + 1 self.assertEqual(reported, actual) + + +class SeparateReasoningMixin: + """Mixin for separate_reasoning tests. + + Required attributes on the test class: + model: str + base_url: str (without /v1) + api_key: str + """ + + def _openai_client(self): + return openai.Client(api_key=self.api_key, base_url=f"{self.base_url}/v1") + + def _chat(self, stream=False, extra_body=None): + return self._openai_client().chat.completions.create( + model=self.model, + messages=[{"role": "user", "content": "What is 1+3?"}], + max_tokens=1024, + stream=stream, + extra_body=extra_body, + ) + + def _collect_stream(self, response): + reasoning_content = "" + content = "" + for chunk in response: + if chunk.choices[0].delta.content: + content += chunk.choices[0].delta.content + elif chunk.choices[0].delta.reasoning_content: + reasoning_content += chunk.choices[0].delta.reasoning_content + return reasoning_content, content + + def test_streaming_separate_reasoning_false(self): + response = self._chat(stream=True, extra_body={"separate_reasoning": False}) + reasoning_content, content = self._collect_stream(response) + self.assertEqual(len(reasoning_content), 0) + self.assertGreater(len(content), 0) + + def test_streaming_separate_reasoning_true(self): + response = self._chat(stream=True, extra_body={"separate_reasoning": True}) + reasoning_content, content = self._collect_stream(response) + self.assertGreater(len(reasoning_content), 0) + self.assertGreater(len(content), 0) + + def test_streaming_separate_reasoning_true_stream_reasoning_false(self): + response = self._chat( + stream=True, + extra_body={"separate_reasoning": True, "stream_reasoning": False}, + ) + reasoning_content = "" + content = "" + first_chunk = False + for chunk in response: + if chunk.choices[0].delta.reasoning_content: + reasoning_content = chunk.choices[0].delta.reasoning_content + first_chunk = True + if chunk.choices[0].delta.content: + content += chunk.choices[0].delta.content + if not first_chunk: + reasoning_content = chunk.choices[0].delta.reasoning_content + first_chunk = True + if not first_chunk: + assert ( + not chunk.choices[0].delta.reasoning_content + or len(chunk.choices[0].delta.reasoning_content) == 0 + ) + self.assertGreater(len(reasoning_content), 0) + self.assertGreater(len(content), 0) + + def test_nonstreaming_separate_reasoning_false(self): + response = self._chat(extra_body={"separate_reasoning": False}) + assert ( + not response.choices[0].message.reasoning_content + or len(response.choices[0].message.reasoning_content) == 0 + ) + self.assertGreater(len(response.choices[0].message.content), 0) + + def test_nonstreaming_separate_reasoning_true(self): + response = self._chat(extra_body={"separate_reasoning": True}) + self.assertGreater(len(response.choices[0].message.reasoning_content), 0) + self.assertGreater(len(response.choices[0].message.content), 0) diff --git a/test/registered/4-gpu-models/test_qwen35_models.py b/test/registered/4-gpu-models/test_qwen35_models.py index 233518f6d..828857b55 100644 --- a/test/registered/4-gpu-models/test_qwen35_models.py +++ b/test/registered/4-gpu-models/test_qwen35_models.py @@ -7,7 +7,7 @@ from sglang.srt.environ import envs from sglang.srt.utils import kill_process_tree from sglang.test.accuracy_test_runner import AccuracyTestParams from sglang.test.ci.ci_register import register_cuda_ci -from sglang.test.kits.reasoning_tokens_kit import ReasoningTokenUsageMixin +from sglang.test.kits.reasoning_kit import ReasoningTokenUsageMixin # This eval harness applies the chat_template, which is critical for qwen3.5 # to get good accuracy on gsm8k diff --git a/test/registered/openai_server/features/test_reasoning_content.py b/test/registered/openai_server/features/test_reasoning_content.py deleted file mode 100644 index d53c41f44..000000000 --- a/test/registered/openai_server/features/test_reasoning_content.py +++ /dev/null @@ -1,345 +0,0 @@ -""" -Usage: -python3 -m unittest openai_server.features.test_reasoning_content.TestReasoningContentAPI.test_streaming_separate_reasoning_false -python3 -m unittest openai_server.features.test_reasoning_content.TestReasoningContentAPI.test_streaming_separate_reasoning_true -python3 -m unittest openai_server.features.test_reasoning_content.TestReasoningContentAPI.test_streaming_separate_reasoning_true_stream_reasoning_false -python3 -m unittest openai_server.features.test_reasoning_content.TestReasoningContentAPI.test_nonstreaming_separate_reasoning_false -python3 -m unittest openai_server.features.test_reasoning_content.TestReasoningContentAPI.test_nonstreaming_separate_reasoning_true -python3 -m unittest openai_server.features.test_reasoning_content.TestReasoningContentStartup.test_nonstreaming -python3 -m unittest openai_server.features.test_reasoning_content.TestReasoningContentStartup.test_streaming -""" - -import unittest - -import openai - -from sglang.srt.utils import kill_process_tree -from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci -from sglang.test.test_utils import ( - DEFAULT_REASONING_MODEL_NAME_FOR_TEST, - DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH, - DEFAULT_URL_FOR_TEST, - CustomTestCase, - popen_launch_server, -) - -register_cuda_ci(est_time=89, suite="stage-b-test-1-gpu-small") -register_amd_ci(est_time=89, suite="stage-b-test-1-gpu-small-amd") - - -class TestReasoningContentAPI(CustomTestCase): - @classmethod - def setUpClass(cls): - cls.model = DEFAULT_REASONING_MODEL_NAME_FOR_TEST - cls.base_url = DEFAULT_URL_FOR_TEST - cls.api_key = "sk-1234" - cls.process = popen_launch_server( - cls.model, - cls.base_url, - timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH, - api_key=cls.api_key, - other_args=[ - "--reasoning-parser", - "deepseek-r1", - ], - ) - cls.base_url += "/v1" - - @classmethod - def tearDownClass(cls): - kill_process_tree(cls.process.pid) - - def test_streaming_separate_reasoning_false(self): - # Test streaming with separate_reasoning=False, reasoning_content should be empty - client = openai.Client(api_key=self.api_key, base_url=self.base_url) - payload = { - "model": self.model, - "messages": [ - { - "role": "user", - "content": "What is 1+3?", - } - ], - "max_tokens": 100, - "stream": True, - "extra_body": {"separate_reasoning": False}, - } - response = client.chat.completions.create(**payload) - - reasoning_content = "" - content = "" - for chunk in response: - if chunk.choices[0].delta.content: - content += chunk.choices[0].delta.content - elif chunk.choices[0].delta.reasoning_content: - reasoning_content += chunk.choices[0].delta.reasoning_content - - assert len(reasoning_content) == 0 - assert len(content) > 0 - - def test_streaming_separate_reasoning_true(self): - # Test streaming with separate_reasoning=True, reasoning_content should not be empty - client = openai.Client(api_key=self.api_key, base_url=self.base_url) - payload = { - "model": self.model, - "messages": [ - { - "role": "user", - "content": "What is 1+3?", - } - ], - "max_tokens": 100, - "stream": True, - "extra_body": {"separate_reasoning": True}, - } - response = client.chat.completions.create(**payload) - - reasoning_content = "" - content = "" - for chunk in response: - if chunk.choices[0].delta.content: - content += chunk.choices[0].delta.content - elif chunk.choices[0].delta.reasoning_content: - reasoning_content += chunk.choices[0].delta.reasoning_content - - assert len(reasoning_content) > 0 - assert len(content) > 0 - - def test_streaming_separate_reasoning_true_stream_reasoning_false(self): - # Test streaming with separate_reasoning=True, reasoning_content should not be empty - client = openai.Client(api_key=self.api_key, base_url=self.base_url) - payload = { - "model": self.model, - "messages": [ - { - "role": "user", - "content": "What is 1+3?", - } - ], - "max_tokens": 100, - "stream": True, - "extra_body": {"separate_reasoning": True, "stream_reasoning": False}, - } - response = client.chat.completions.create(**payload) - - reasoning_content = "" - content = "" - first_chunk = False - for chunk in response: - if chunk.choices[0].delta.reasoning_content: - reasoning_content = chunk.choices[0].delta.reasoning_content - first_chunk = True - if chunk.choices[0].delta.content: - content += chunk.choices[0].delta.content - if not first_chunk: - reasoning_content = chunk.choices[0].delta.reasoning_content - first_chunk = True - if not first_chunk: - assert ( - not chunk.choices[0].delta.reasoning_content - or len(chunk.choices[0].delta.reasoning_content) == 0 - ) - assert len(reasoning_content) > 0 - assert len(content) > 0 - - def test_nonstreaming_separate_reasoning_false(self): - # Test non-streaming with separate_reasoning=False, reasoning_content should be empty - client = openai.Client(api_key=self.api_key, base_url=self.base_url) - payload = { - "model": self.model, - "messages": [ - { - "role": "user", - "content": "What is 1+3?", - } - ], - "max_tokens": 100, - "extra_body": {"separate_reasoning": False}, - } - response = client.chat.completions.create(**payload) - - assert ( - not response.choices[0].message.reasoning_content - or len(response.choices[0].message.reasoning_content) == 0 - ) - assert len(response.choices[0].message.content) > 0 - - def test_nonstreaming_separate_reasoning_true(self): - # Test non-streaming with separate_reasoning=True, reasoning_content should not be empty - client = openai.Client(api_key=self.api_key, base_url=self.base_url) - payload = { - "model": self.model, - "messages": [ - { - "role": "user", - "content": "What is 1+3?", - } - ], - "max_tokens": 100, - "extra_body": {"separate_reasoning": True}, - } - response = client.chat.completions.create(**payload) - - assert len(response.choices[0].message.reasoning_content) > 0 - assert len(response.choices[0].message.content) > 0 - - -class TestReasoningContentWithoutParser(CustomTestCase): - @classmethod - def setUpClass(cls): - cls.model = DEFAULT_REASONING_MODEL_NAME_FOR_TEST - cls.base_url = DEFAULT_URL_FOR_TEST - cls.api_key = "sk-1234" - cls.process = popen_launch_server( - cls.model, - cls.base_url, - timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH, - api_key=cls.api_key, - other_args=[], # No reasoning parser - ) - cls.base_url += "/v1" - - @classmethod - def tearDownClass(cls): - kill_process_tree(cls.process.pid) - - def test_streaming_separate_reasoning_false(self): - # Test streaming with separate_reasoning=False, reasoning_content should be empty - client = openai.Client(api_key=self.api_key, base_url=self.base_url) - payload = { - "model": self.model, - "messages": [ - { - "role": "user", - "content": "What is 1+3?", - } - ], - "max_tokens": 100, - "stream": True, - "extra_body": {"separate_reasoning": False}, - } - response = client.chat.completions.create(**payload) - - reasoning_content = "" - content = "" - for chunk in response: - if chunk.choices[0].delta.content: - content += chunk.choices[0].delta.content - elif chunk.choices[0].delta.reasoning_content: - reasoning_content += chunk.choices[0].delta.reasoning_content - - assert len(reasoning_content) == 0 - assert len(content) > 0 - - def test_streaming_separate_reasoning_true(self): - # Test streaming with separate_reasoning=True, reasoning_content should not be empty - client = openai.Client(api_key=self.api_key, base_url=self.base_url) - payload = { - "model": self.model, - "messages": [ - { - "role": "user", - "content": "What is 1+3?", - } - ], - "max_tokens": 100, - "stream": True, - "extra_body": {"separate_reasoning": True}, - } - response = client.chat.completions.create(**payload) - - reasoning_content = "" - content = "" - for chunk in response: - if chunk.choices[0].delta.content: - content += chunk.choices[0].delta.content - elif chunk.choices[0].delta.reasoning_content: - reasoning_content += chunk.choices[0].delta.reasoning_content - - assert len(reasoning_content) == 0 - assert len(content) > 0 - - def test_streaming_separate_reasoning_true_stream_reasoning_false(self): - # Test streaming with separate_reasoning=True, reasoning_content should not be empty - client = openai.Client(api_key=self.api_key, base_url=self.base_url) - payload = { - "model": self.model, - "messages": [ - { - "role": "user", - "content": "What is 1+3?", - } - ], - "max_tokens": 100, - "stream": True, - "extra_body": {"separate_reasoning": True, "stream_reasoning": False}, - } - response = client.chat.completions.create(**payload) - - reasoning_content = "" - content = "" - first_chunk = False - for chunk in response: - if chunk.choices[0].delta.reasoning_content: - reasoning_content = chunk.choices[0].delta.reasoning_content - first_chunk = True - if chunk.choices[0].delta.content: - content += chunk.choices[0].delta.content - if not first_chunk: - reasoning_content = chunk.choices[0].delta.reasoning_content - first_chunk = True - if not first_chunk: - assert ( - not chunk.choices[0].delta.reasoning_content - or len(chunk.choices[0].delta.reasoning_content) == 0 - ) - assert not reasoning_content or len(reasoning_content) == 0 - assert len(content) > 0 - - def test_nonstreaming_separate_reasoning_false(self): - # Test non-streaming with separate_reasoning=False, reasoning_content should be empty - client = openai.Client(api_key=self.api_key, base_url=self.base_url) - payload = { - "model": self.model, - "messages": [ - { - "role": "user", - "content": "What is 1+3?", - } - ], - "max_tokens": 100, - "extra_body": {"separate_reasoning": False}, - } - response = client.chat.completions.create(**payload) - - assert ( - not response.choices[0].message.reasoning_content - or len(response.choices[0].message.reasoning_content) == 0 - ) - assert len(response.choices[0].message.content) > 0 - - def test_nonstreaming_separate_reasoning_true(self): - # Test non-streaming with separate_reasoning=True, reasoning_content should not be empty - client = openai.Client(api_key=self.api_key, base_url=self.base_url) - payload = { - "model": self.model, - "messages": [ - { - "role": "user", - "content": "What is 1+3?", - } - ], - "max_tokens": 100, - "extra_body": {"separate_reasoning": True}, - } - response = client.chat.completions.create(**payload) - - assert ( - not response.choices[0].message.reasoning_content - or len(response.choices[0].message.reasoning_content) == 0 - ) - assert len(response.choices[0].message.content) > 0 - - -if __name__ == "__main__": - unittest.main() diff --git a/test/registered/openai_server/features/test_enable_thinking.py b/test/registered/reasoning/test_reasoning.py similarity index 71% rename from test/registered/openai_server/features/test_enable_thinking.py rename to test/registered/reasoning/test_reasoning.py index 51ae25351..0517e2c8f 100644 --- a/test/registered/openai_server/features/test_enable_thinking.py +++ b/test/registered/reasoning/test_reasoning.py @@ -1,11 +1,3 @@ -""" -Usage: -python3 -m unittest openai_server.features.test_enable_thinking.TestEnableThinking.test_chat_completion_with_reasoning -python3 -m unittest openai_server.features.test_enable_thinking.TestEnableThinking.test_chat_completion_without_reasoning -python3 -m unittest openai_server.features.test_enable_thinking.TestEnableThinking.test_stream_chat_completion_with_reasoning -python3 -m unittest openai_server.features.test_enable_thinking.TestEnableThinking.test_stream_chat_completion_without_reasoning -""" - import json import unittest @@ -13,7 +5,10 @@ import requests from sglang.srt.utils import kill_process_tree from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci -from sglang.test.kits.reasoning_tokens_kit import ReasoningTokenUsageMixin +from sglang.test.kits.reasoning_kit import ( + ReasoningTokenUsageMixin, + SeparateReasoningMixin, +) from sglang.test.test_utils import ( DEFAULT_ENABLE_THINKING_MODEL_NAME_FOR_TEST, DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH, @@ -26,7 +21,9 @@ register_cuda_ci(est_time=109, suite="stage-b-test-1-gpu-large") register_amd_ci(est_time=200, suite="stage-b-test-1-gpu-small-amd") -class TestEnableThinking(ReasoningTokenUsageMixin, CustomTestCase): +class TestEnableThinking( + ReasoningTokenUsageMixin, SeparateReasoningMixin, CustomTestCase +): reasoning_parser_name = "qwen3" @classmethod @@ -122,7 +119,6 @@ class TestEnableThinking(ReasoningTokenUsageMixin, CustomTestCase): has_reasoning = False has_content = False - print("\n=== Stream With Reasoning ===") for line in response.iter_lines(): if line: line = line.decode("utf-8") @@ -146,7 +142,7 @@ class TestEnableThinking(ReasoningTokenUsageMixin, CustomTestCase): ) def test_stream_chat_completion_without_reasoning(self): - # Test streaming with "enable_thinking": False, reasoning_content should be empty + # Test streaming with "enable_thinking": False, reasoning_content should be empty response = requests.post( f"{self.base_url}/v1/chat/completions", headers={"Authorization": f"Bearer {self.api_key}"}, @@ -167,7 +163,6 @@ class TestEnableThinking(ReasoningTokenUsageMixin, CustomTestCase): has_reasoning = False has_content = False - print("\n=== Stream Without Reasoning ===") for line in response.iter_lines(): if line: line = line.decode("utf-8") @@ -191,55 +186,5 @@ class TestEnableThinking(ReasoningTokenUsageMixin, CustomTestCase): ) -# Skip for ci test -# class TestGLM45EnableThinking(TestEnableThinking): -# @classmethod -# def setUpClass(cls): -# # Replace with the model name needed for testing; if not required, reuse DEFAULT_SMALL_MODEL_NAME_FOR_TEST -# cls.model = "THUDM/GLM-4.5" -# cls.base_url = DEFAULT_URL_FOR_TEST -# cls.api_key = "sk-1234" -# cls.process = popen_launch_server( -# cls.model, -# cls.base_url, -# timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH, -# api_key=cls.api_key, -# other_args=[ -# "--tool-call-parser", -# "glm45", -# "--reasoning-parser", -# "glm45", -# "--tp-size", -# "8" -# ], -# ) - -# # Validate whether enable-thinking conflict with tool_calls -# cls.additional_chat_kwargs = { -# "tools": [ -# { -# "type": "function", -# "function": { -# "name": "add", -# "description": "Compute the sum of two numbers", -# "parameters": { -# "type": "object", -# "properties": { -# "a": { -# "type": "int", -# "description": "A number", -# }, -# "b": { -# "type": "int", -# "description": "A number", -# }, -# }, -# "required": ["a", "b"], -# }, -# }, -# } -# ] -# } - if __name__ == "__main__": unittest.main() diff --git a/test/registered/unit/parser/test_reasoning_content_without_parser.py b/test/registered/unit/parser/test_reasoning_content_without_parser.py new file mode 100644 index 000000000..446dea6b3 --- /dev/null +++ b/test/registered/unit/parser/test_reasoning_content_without_parser.py @@ -0,0 +1,80 @@ +import unittest + +from sglang.srt.parser.reasoning_parser import ReasoningParser +from sglang.test.ci.ci_register import register_cpu_ci +from sglang.test.test_utils import CustomTestCase + +register_cpu_ci(est_time=5, suite="stage-a-test-cpu") + +# Simulated model output that contains think tags (e.g. from DeepSeek-R1) +THINK_OUTPUT = ( + "\nLet me think about this.\n1 + 3 = 4\n\nThe answer is 4." +) +THINK_OUTPUT_QWEN3 = ( + "\nLet me think about this.\n1 + 3 = 4\n\n\nThe answer is 4." +) + + +class TestReasoningContentWithoutParser(CustomTestCase): + """Test the code path: when no reasoning parser is configured, reasoning + content should never be separated, even if the model output contains + think tags. This mirrors the guard in serving_chat.py: + + if self.reasoning_parser and request.separate_reasoning: + ... + + When reasoning_parser is None the block is skipped entirely. + """ + + def test_no_parser_text_passthrough(self): + """Without a parser, raw text with tags passes through as-is.""" + reasoning_parser = None + + # Simulate serving_chat.py logic + reasoning_text = None + text = THINK_OUTPUT + if reasoning_parser: + parser = ReasoningParser(reasoning_parser) + reasoning_text, text = parser.parse_non_stream(text) + + self.assertIsNone(reasoning_text) + self.assertIn("", text) + self.assertIn("The answer is 4.", text) + + def test_with_parser_separates_reasoning(self): + """With a parser, reasoning content is correctly separated.""" + for parser_name, output in [ + ("deepseek-r1", THINK_OUTPUT), + ("qwen3", THINK_OUTPUT_QWEN3), + ]: + with self.subTest(parser=parser_name): + parser = ReasoningParser(parser_name, stream_reasoning=False) + reasoning_text, text = parser.parse_non_stream(output) + + self.assertIsNotNone(reasoning_text) + self.assertGreater(len(reasoning_text), 0) + self.assertNotIn("", reasoning_text) + self.assertIn("The answer is 4.", text) + + def test_no_parser_streaming_passthrough(self): + """Without a parser, streaming chunks pass through without reasoning separation.""" + reasoning_parser = None + + # Simulate serving_chat.py streaming logic + chunks = ["\nLet me", " think.\n\nThe answer", " is 4."] + all_text = "" + reasoning_text_seen = False + + for chunk in chunks: + delta = chunk + if reasoning_parser: + # This block would separate reasoning in streaming + reasoning_text_seen = True + all_text += delta + + self.assertFalse(reasoning_text_seen) + self.assertIn("", all_text) + + +if __name__ == "__main__": + unittest.main()