feat(reasoning): auto-detect reasoning/tool-call parser from chat template (#23952)
This commit is contained in:
@@ -28,6 +28,7 @@ from sglang.srt.entrypoints.openai.serving_chat import (
|
||||
normalize_tool_content,
|
||||
)
|
||||
from sglang.srt.managers.io_struct import GenerateReqInput
|
||||
from sglang.srt.managers.template_detection import ReasoningToggleConfig
|
||||
from sglang.srt.utils import get_or_create_event_loop
|
||||
from sglang.test.ci.ci_register import register_cpu_ci
|
||||
|
||||
@@ -86,6 +87,8 @@ class _MockTemplateManager:
|
||||
self.chat_template_name: Optional[str] = "llama-3"
|
||||
self.jinja_template_content_format: Optional[str] = None
|
||||
self.completion_template_name: Optional[str] = None
|
||||
self.reasoning_config = None
|
||||
self.force_reasoning = False
|
||||
|
||||
|
||||
class ServingChatTestCase(unittest.TestCase):
|
||||
@@ -1039,6 +1042,175 @@ class ServingChatTestCase(unittest.TestCase):
|
||||
req.reasoning_effort = effort
|
||||
self.assertEqual(chat._get_reasoning_from_request(req), expected)
|
||||
|
||||
# ------------- reasoning config tests -------------
|
||||
def test_get_reasoning_from_request_default_true_toggle(self):
|
||||
self.tm.server_args.reasoning_parser = "qwen3"
|
||||
self.chat.reasoning_parser = "qwen3"
|
||||
self.template_manager.reasoning_config = ReasoningToggleConfig(
|
||||
toggle_param="enable_thinking", default_enabled=True
|
||||
)
|
||||
|
||||
enabled_by_default = ChatCompletionRequest(
|
||||
model="x", messages=[{"role": "user", "content": "Hi?"}]
|
||||
)
|
||||
disabled_explicitly = ChatCompletionRequest(
|
||||
model="x",
|
||||
messages=[{"role": "user", "content": "Hi?"}],
|
||||
chat_template_kwargs={"enable_thinking": False},
|
||||
)
|
||||
|
||||
self.assertTrue(self.chat._get_reasoning_from_request(enabled_by_default))
|
||||
self.assertFalse(self.chat._get_reasoning_from_request(disabled_explicitly))
|
||||
|
||||
def test_get_reasoning_from_request_default_false_toggle(self):
|
||||
self.tm.server_args.reasoning_parser = "deepseek-v3"
|
||||
self.chat.reasoning_parser = "deepseek-v3"
|
||||
self.template_manager.reasoning_config = ReasoningToggleConfig(
|
||||
toggle_param="thinking", default_enabled=False
|
||||
)
|
||||
|
||||
disabled_by_default = ChatCompletionRequest(
|
||||
model="x", messages=[{"role": "user", "content": "Hi?"}]
|
||||
)
|
||||
enabled_explicitly = ChatCompletionRequest(
|
||||
model="x",
|
||||
messages=[{"role": "user", "content": "Hi?"}],
|
||||
chat_template_kwargs={"thinking": True},
|
||||
)
|
||||
|
||||
self.assertFalse(self.chat._get_reasoning_from_request(disabled_by_default))
|
||||
self.assertTrue(self.chat._get_reasoning_from_request(enabled_explicitly))
|
||||
|
||||
def test_get_reasoning_from_request_special_cases(self):
|
||||
self.tm.server_args.reasoning_parser = "mistral"
|
||||
self.chat.reasoning_parser = "mistral"
|
||||
req = ChatCompletionRequest(
|
||||
model="x", messages=[{"role": "user", "content": "Hi?"}]
|
||||
)
|
||||
|
||||
self.template_manager.reasoning_config = ReasoningToggleConfig(
|
||||
special_case="always"
|
||||
)
|
||||
self.assertTrue(self.chat._get_reasoning_from_request(req))
|
||||
|
||||
self.template_manager.reasoning_config = ReasoningToggleConfig(
|
||||
special_case="mistral"
|
||||
)
|
||||
self.assertFalse(self.chat._get_reasoning_from_request(req))
|
||||
req.reasoning_effort = "medium"
|
||||
self.assertTrue(self.chat._get_reasoning_from_request(req))
|
||||
|
||||
# --- fallback path tests (config=None, uses reasoning_default) ---
|
||||
|
||||
def _setup_fallback(self, parser_name):
|
||||
"""Set up reasoning with config=None to exercise the fallback path."""
|
||||
self.tm.server_args.reasoning_parser = parser_name
|
||||
self.chat = OpenAIServingChat(self.tm, self.template_manager)
|
||||
self.chat.reasoning_parser = parser_name
|
||||
self.template_manager.reasoning_config = None
|
||||
|
||||
def test_fallback_always_mode(self):
|
||||
self._setup_fallback("deepseek-r1")
|
||||
req = ChatCompletionRequest(
|
||||
model="x", messages=[{"role": "user", "content": "Hi?"}]
|
||||
)
|
||||
self.assertTrue(self.chat._get_reasoning_from_request(req))
|
||||
|
||||
def test_fallback_mistral_mode(self):
|
||||
self._setup_fallback("mistral")
|
||||
req_no_effort = ChatCompletionRequest(
|
||||
model="x", messages=[{"role": "user", "content": "Hi?"}]
|
||||
)
|
||||
self.assertFalse(self.chat._get_reasoning_from_request(req_no_effort))
|
||||
|
||||
req_with_effort = ChatCompletionRequest(
|
||||
model="x",
|
||||
messages=[{"role": "user", "content": "Hi?"}],
|
||||
reasoning_effort="high",
|
||||
)
|
||||
self.assertTrue(self.chat._get_reasoning_from_request(req_with_effort))
|
||||
|
||||
def test_fallback_enable_thinking_mode_default_on(self):
|
||||
self._setup_fallback("qwen3")
|
||||
req_default = ChatCompletionRequest(
|
||||
model="x", messages=[{"role": "user", "content": "Hi?"}]
|
||||
)
|
||||
self.assertTrue(self.chat._get_reasoning_from_request(req_default))
|
||||
|
||||
req_disabled = ChatCompletionRequest(
|
||||
model="x",
|
||||
messages=[{"role": "user", "content": "Hi?"}],
|
||||
chat_template_kwargs={"enable_thinking": False},
|
||||
)
|
||||
self.assertFalse(self.chat._get_reasoning_from_request(req_disabled))
|
||||
|
||||
def test_fallback_explicit_thinking_mode_default_off(self):
|
||||
self._setup_fallback("deepseek-v3")
|
||||
req_default = ChatCompletionRequest(
|
||||
model="x", messages=[{"role": "user", "content": "Hi?"}]
|
||||
)
|
||||
self.assertFalse(self.chat._get_reasoning_from_request(req_default))
|
||||
|
||||
req_enabled = ChatCompletionRequest(
|
||||
model="x",
|
||||
messages=[{"role": "user", "content": "Hi?"}],
|
||||
chat_template_kwargs={"thinking": True},
|
||||
)
|
||||
self.assertTrue(self.chat._get_reasoning_from_request(req_enabled))
|
||||
|
||||
def test_fallback_explicit_enable_thinking_mode_default_off(self):
|
||||
self._setup_fallback("mimo")
|
||||
req_default = ChatCompletionRequest(
|
||||
model="x", messages=[{"role": "user", "content": "Hi?"}]
|
||||
)
|
||||
self.assertFalse(self.chat._get_reasoning_from_request(req_default))
|
||||
|
||||
req_enabled = ChatCompletionRequest(
|
||||
model="x",
|
||||
messages=[{"role": "user", "content": "Hi?"}],
|
||||
chat_template_kwargs={"enable_thinking": True},
|
||||
)
|
||||
self.assertTrue(self.chat._get_reasoning_from_request(req_enabled))
|
||||
|
||||
def test_fallback_no_detector_returns_false(self):
|
||||
self.chat.reasoning_parser = "qwen3"
|
||||
self.chat._reasoning_detector = None
|
||||
self.template_manager.reasoning_config = None
|
||||
req = ChatCompletionRequest(
|
||||
model="x", messages=[{"role": "user", "content": "Hi?"}]
|
||||
)
|
||||
self.assertFalse(self.chat._get_reasoning_from_request(req))
|
||||
|
||||
def test_build_chat_response_qwen3_thinking_forces_reasoning(self):
|
||||
self.tm.server_args.reasoning_parser = "qwen3-thinking"
|
||||
self.chat.reasoning_parser = "qwen3-thinking"
|
||||
self.template_manager.reasoning_config = ReasoningToggleConfig(
|
||||
toggle_param="enable_thinking", default_enabled=True
|
||||
)
|
||||
|
||||
req = ChatCompletionRequest(
|
||||
model="Qwen/Qwen3-0.6B",
|
||||
messages=[{"role": "user", "content": "Hi?"}],
|
||||
separate_reasoning=True,
|
||||
chat_template_kwargs={"enable_thinking": False},
|
||||
)
|
||||
ret_item = {
|
||||
"text": "42",
|
||||
"meta_info": {
|
||||
"id": f"chatcmpl-{uuid.uuid4()}",
|
||||
"prompt_tokens": 10,
|
||||
"completion_tokens": 1,
|
||||
"weight_version": "default",
|
||||
"finish_reason": {"type": "stop", "matched": None},
|
||||
},
|
||||
"index": 0,
|
||||
}
|
||||
|
||||
response = self.chat._build_chat_response(req, [ret_item], created=0)
|
||||
msg = response.choices[0].message
|
||||
self.assertIsNone(msg.content)
|
||||
self.assertEqual(msg.reasoning_content, "42")
|
||||
|
||||
|
||||
class TestProcessToolCallsWithRequiredToolChoice(unittest.TestCase):
|
||||
"""Test _process_tool_calls with tool_choice='required' uses model-specific parser."""
|
||||
|
||||
Reference in New Issue
Block a user