[New Model] Gemma 4 (#21952)
Signed-off-by: Xinyuan Tong <xinyuantong.cs@gmail.com> Co-authored-by: Pengyu Chen <pychen96@gmail.com> Co-authored-by: kpham-sgl <khoa.pham@radixark.ai> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Andy Luo <andy.luo@amd.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: adarshxs <adarsh.shirawalmath@gmail.com>
This commit is contained in:
co-authored by
Pengyu Chen
kpham-sgl
Claude Opus 4.6
Andy Luo
gemini-code-assist[bot]
adarshxs
parent
be0277f9a0
commit
2813cb6d9a
@@ -6,6 +6,12 @@ from sglang.srt.function_call.base_format_detector import BaseFormatDetector
|
||||
from sglang.srt.function_call.core_types import StreamingParseResult
|
||||
from sglang.srt.function_call.deepseekv3_detector import DeepSeekV3Detector
|
||||
from sglang.srt.function_call.deepseekv32_detector import DeepSeekV32Detector
|
||||
from sglang.srt.function_call.gemma4_detector import (
|
||||
Gemma4Detector,
|
||||
_parse_gemma4_args,
|
||||
_parse_gemma4_array,
|
||||
_parse_gemma4_value,
|
||||
)
|
||||
from sglang.srt.function_call.gigachat3_detector import GigaChat3Detector
|
||||
from sglang.srt.function_call.glm4_moe_detector import Glm4MoeDetector
|
||||
from sglang.srt.function_call.glm47_moe_detector import Glm47MoeDetector
|
||||
@@ -4008,5 +4014,340 @@ class TestQwen25Detector(unittest.TestCase):
|
||||
self.assertEqual(cities, ["NYC", "LA"])
|
||||
|
||||
|
||||
class TestGemma4Detector(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tools = [
|
||||
Tool(
|
||||
type="function",
|
||||
function=Function(
|
||||
name="get_weather",
|
||||
description="Get weather information",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {"type": "string"},
|
||||
"unit": {
|
||||
"type": "string",
|
||||
"enum": ["celsius", "fahrenheit"],
|
||||
},
|
||||
},
|
||||
"required": ["location"],
|
||||
},
|
||||
),
|
||||
)
|
||||
]
|
||||
self.detector = Gemma4Detector()
|
||||
|
||||
def test_detect_and_parse(self):
|
||||
text = 'Some text before <|tool_call>call:get_weather{location:<|"|>Tokyo<|"|>}<tool_call|>'
|
||||
result = self.detector.detect_and_parse(text, self.tools)
|
||||
|
||||
self.assertEqual(result.normal_text, "Some text before ")
|
||||
self.assertEqual(len(result.calls), 1)
|
||||
self.assertEqual(result.calls[0].name, "get_weather")
|
||||
|
||||
params = json.loads(result.calls[0].parameters)
|
||||
self.assertEqual(params["location"], "Tokyo")
|
||||
|
||||
def test_parse_streaming_increment(self):
|
||||
chunks = [
|
||||
"Some text ",
|
||||
"before <|tool",
|
||||
"_call>call:get_we",
|
||||
"ather{location:<|", # codespell:ignore
|
||||
'"|>Tokyo<|"|>}<tool_',
|
||||
"call|> after",
|
||||
]
|
||||
|
||||
all_results = []
|
||||
for chunk in chunks:
|
||||
res = self.detector.parse_streaming_increment(chunk, self.tools)
|
||||
all_results.append(res)
|
||||
|
||||
combined_normal_text = "".join(r.normal_text for r in all_results)
|
||||
self.assertEqual(combined_normal_text, "Some text before after")
|
||||
|
||||
found_name = False
|
||||
found_params = False
|
||||
for res in all_results:
|
||||
for call in res.calls:
|
||||
if call.name == "get_weather":
|
||||
found_name = True
|
||||
if call.parameters:
|
||||
params = json.loads(call.parameters)
|
||||
if params == {"location": "Tokyo"}:
|
||||
found_params = True
|
||||
|
||||
self.assertTrue(found_name)
|
||||
self.assertTrue(found_params)
|
||||
|
||||
def test_nested_array_streaming(self):
|
||||
# Additional coverage for complex structure
|
||||
chunks = [
|
||||
'<|tool_call>call:get_weather{location:<|"',
|
||||
'|>New York<|"|>,nested:[1, 2, {inner:<|"|>',
|
||||
'val<|"|>}]}<tool_call|>',
|
||||
]
|
||||
|
||||
all_results = []
|
||||
for chunk in chunks:
|
||||
res = self.detector.parse_streaming_increment(chunk, self.tools)
|
||||
all_results.append(res)
|
||||
|
||||
found_params = False
|
||||
for res in all_results:
|
||||
for call in res.calls:
|
||||
if call.parameters:
|
||||
params = json.loads(call.parameters)
|
||||
if "location" in params and params["location"] == "New York":
|
||||
if "nested" in params and params["nested"] == [
|
||||
1,
|
||||
2,
|
||||
{"inner": "val"},
|
||||
]:
|
||||
found_params = True
|
||||
|
||||
self.assertTrue(found_params)
|
||||
|
||||
def test_has_tool_call(self):
|
||||
self.assertTrue(
|
||||
self.detector.has_tool_call(
|
||||
'<|tool_call>call:get_weather{location:<|"|>Tokyo<|"|>}<tool_call|>'
|
||||
)
|
||||
)
|
||||
self.assertFalse(self.detector.has_tool_call("no tool call here"))
|
||||
|
||||
def test_detect_and_parse_no_tool_call(self):
|
||||
text = "This is plain text without any tool calls."
|
||||
result = self.detector.detect_and_parse(text, self.tools)
|
||||
self.assertEqual(result.normal_text, text)
|
||||
self.assertEqual(len(result.calls), 0)
|
||||
|
||||
def test_detect_and_parse_tool_index(self):
|
||||
text = '<|tool_call>call:get_weather{location:<|"|>Tokyo<|"|>}<tool_call|>'
|
||||
result = self.detector.detect_and_parse(text, self.tools)
|
||||
self.assertEqual(len(result.calls), 1)
|
||||
self.assertEqual(result.calls[0].tool_index, 0)
|
||||
self.assertEqual(result.calls[0].name, "get_weather")
|
||||
|
||||
def test_detect_and_parse_unknown_tool_index(self):
|
||||
text = '<|tool_call>call:unknown_func{arg:<|"|>val<|"|>}<tool_call|>'
|
||||
result = self.detector.detect_and_parse(text, self.tools)
|
||||
self.assertEqual(len(result.calls), 1)
|
||||
self.assertEqual(result.calls[0].tool_index, -1)
|
||||
|
||||
def test_detect_and_parse_nested_object(self):
|
||||
text = '<|tool_call>call:get_weather{location:<|"|>Tokyo<|"|>,details:{temp:25,unit:<|"|>celsius<|"|>}}<tool_call|>'
|
||||
result = self.detector.detect_and_parse(text, self.tools)
|
||||
self.assertEqual(len(result.calls), 1)
|
||||
params = json.loads(result.calls[0].parameters)
|
||||
self.assertEqual(params["location"], "Tokyo")
|
||||
self.assertIsInstance(params["details"], dict)
|
||||
self.assertEqual(params["details"]["temp"], 25)
|
||||
self.assertEqual(params["details"]["unit"], "celsius")
|
||||
|
||||
def test_detect_and_parse_multiple_calls(self):
|
||||
extra_tools = self.tools + [
|
||||
Tool(
|
||||
type="function",
|
||||
function=Function(
|
||||
name="get_time",
|
||||
description="Get current time",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {"timezone": {"type": "string"}},
|
||||
},
|
||||
),
|
||||
)
|
||||
]
|
||||
text = (
|
||||
'Some text <|tool_call>call:get_weather{location:<|"|>Tokyo<|"|>}<tool_call|>'
|
||||
' more text <|tool_call>call:get_time{timezone:<|"|>UTC<|"|>}<tool_call|>'
|
||||
)
|
||||
result = self.detector.detect_and_parse(text, extra_tools)
|
||||
self.assertEqual(len(result.calls), 2)
|
||||
self.assertEqual(result.calls[0].name, "get_weather")
|
||||
self.assertEqual(result.calls[1].name, "get_time")
|
||||
self.assertEqual(result.normal_text, "Some text ")
|
||||
|
||||
def test_parse_gemma4_args_empty(self):
|
||||
self.assertEqual(_parse_gemma4_args(""), {})
|
||||
self.assertEqual(_parse_gemma4_args(" "), {})
|
||||
|
||||
def test_parse_gemma4_args_booleans(self):
|
||||
result = _parse_gemma4_args("flag:true,other:false")
|
||||
self.assertIs(result["flag"], True)
|
||||
self.assertIs(result["other"], False)
|
||||
|
||||
def test_parse_gemma4_args_numbers(self):
|
||||
result = _parse_gemma4_args("count:42,ratio:3.14")
|
||||
self.assertEqual(result["count"], 42)
|
||||
self.assertAlmostEqual(result["ratio"], 3.14)
|
||||
|
||||
def test_parse_gemma4_args_string_with_colon(self):
|
||||
result = _parse_gemma4_args('url:<|"|>http://example.com<|"|>')
|
||||
self.assertEqual(result["url"], "http://example.com")
|
||||
|
||||
def test_parse_gemma4_args_nested_object(self):
|
||||
result = _parse_gemma4_args('outer:{inner:<|"|>val<|"|>,num:5}')
|
||||
self.assertIsInstance(result["outer"], dict)
|
||||
self.assertEqual(result["outer"]["inner"], "val")
|
||||
self.assertEqual(result["outer"]["num"], 5)
|
||||
|
||||
def test_parse_gemma4_array_mixed_types(self):
|
||||
result = _parse_gemma4_array('<|"|>hello<|"|>, 42, true, {key:<|"|>val<|"|>}')
|
||||
self.assertEqual(result[0], "hello")
|
||||
self.assertEqual(result[1], 42)
|
||||
self.assertIs(result[2], True)
|
||||
self.assertIsInstance(result[3], dict)
|
||||
self.assertEqual(result[3]["key"], "val")
|
||||
|
||||
def test_parse_gemma4_value_types(self):
|
||||
self.assertIs(_parse_gemma4_value("true"), True)
|
||||
self.assertIs(_parse_gemma4_value("false"), False)
|
||||
self.assertEqual(_parse_gemma4_value("42"), 42)
|
||||
self.assertAlmostEqual(_parse_gemma4_value("3.14"), 3.14)
|
||||
self.assertEqual(_parse_gemma4_value("hello"), "hello")
|
||||
self.assertEqual(_parse_gemma4_value(""), "")
|
||||
|
||||
def _collect_streaming(self, chunks):
|
||||
"""Helper: feed chunks and collect normal text + tool calls by index."""
|
||||
normal_text = ""
|
||||
tool_calls_by_index = {}
|
||||
for chunk in chunks:
|
||||
result = self.detector.parse_streaming_increment(chunk, self.tools)
|
||||
normal_text += result.normal_text
|
||||
for call in result.calls:
|
||||
if call.tool_index is not None:
|
||||
if call.tool_index not in tool_calls_by_index:
|
||||
tool_calls_by_index[call.tool_index] = {
|
||||
"name": "",
|
||||
"parameters": "",
|
||||
}
|
||||
if call.name:
|
||||
tool_calls_by_index[call.tool_index]["name"] = call.name
|
||||
if call.parameters:
|
||||
tool_calls_by_index[call.tool_index][
|
||||
"parameters"
|
||||
] += call.parameters
|
||||
return normal_text, tool_calls_by_index
|
||||
|
||||
def test_streaming_multiple_tool_calls(self):
|
||||
"""Test streaming with two consecutive tool calls."""
|
||||
extra_tools = self.tools + [
|
||||
Tool(
|
||||
type="function",
|
||||
function=Function(
|
||||
name="get_time",
|
||||
description="Get current time",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {"timezone": {"type": "string"}},
|
||||
},
|
||||
),
|
||||
)
|
||||
]
|
||||
chunks = [
|
||||
'<|tool_call>call:get_weather{location:<|"|>',
|
||||
'Tokyo<|"|>}<tool_call|>',
|
||||
' <|tool_call>call:get_time{timezone:<|"|>',
|
||||
'UTC<|"|>}<tool_call|>',
|
||||
]
|
||||
normal_text = ""
|
||||
tool_calls_by_index = {}
|
||||
for chunk in chunks:
|
||||
result = self.detector.parse_streaming_increment(chunk, extra_tools)
|
||||
normal_text += result.normal_text
|
||||
for call in result.calls:
|
||||
if call.tool_index is not None:
|
||||
if call.tool_index not in tool_calls_by_index:
|
||||
tool_calls_by_index[call.tool_index] = {
|
||||
"name": "",
|
||||
"parameters": "",
|
||||
}
|
||||
if call.name:
|
||||
tool_calls_by_index[call.tool_index]["name"] = call.name
|
||||
if call.parameters:
|
||||
tool_calls_by_index[call.tool_index][
|
||||
"parameters"
|
||||
] += call.parameters
|
||||
|
||||
self.assertEqual(len(tool_calls_by_index), 2)
|
||||
self.assertEqual(tool_calls_by_index[0]["name"], "get_weather")
|
||||
self.assertEqual(tool_calls_by_index[1]["name"], "get_time")
|
||||
params0 = json.loads(tool_calls_by_index[0]["parameters"])
|
||||
params1 = json.loads(tool_calls_by_index[1]["parameters"])
|
||||
self.assertEqual(params0["location"], "Tokyo")
|
||||
self.assertEqual(params1["timezone"], "UTC")
|
||||
|
||||
def test_streaming_very_small_chunks(self):
|
||||
"""Test streaming with character-by-character chunks."""
|
||||
full_text = '<|tool_call>call:get_weather{location:<|"|>Rome<|"|>}<tool_call|>'
|
||||
chunks = list(full_text)
|
||||
|
||||
normal_text, tool_calls = self._collect_streaming(chunks)
|
||||
|
||||
self.assertEqual(len(tool_calls), 1)
|
||||
self.assertEqual(tool_calls[0]["name"], "get_weather")
|
||||
params = json.loads(tool_calls[0]["parameters"])
|
||||
self.assertEqual(params["location"], "Rome")
|
||||
|
||||
def test_streaming_empty_args(self):
|
||||
"""Test streaming a tool call with no arguments."""
|
||||
chunks = ["<|tool_call>call:get_weather{}", "<tool_call|>"]
|
||||
normal_text, tool_calls = self._collect_streaming(chunks)
|
||||
self.assertEqual(len(tool_calls), 1)
|
||||
self.assertEqual(tool_calls[0]["name"], "get_weather")
|
||||
|
||||
def test_streaming_text_between_tool_calls(self):
|
||||
"""Test streaming with normal text interleaved between two different tool calls."""
|
||||
extra_tools = self.tools + [
|
||||
Tool(
|
||||
type="function",
|
||||
function=Function(
|
||||
name="get_time",
|
||||
description="Get current time",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {"timezone": {"type": "string"}},
|
||||
},
|
||||
),
|
||||
)
|
||||
]
|
||||
chunks = [
|
||||
"Hello! ",
|
||||
'<|tool_call>call:get_weather{location:<|"|>Paris<|"|>}<tool_call|>',
|
||||
" Let me also check ",
|
||||
'<|tool_call>call:get_time{timezone:<|"|>UTC<|"|>}<tool_call|>',
|
||||
]
|
||||
normal_text = ""
|
||||
tool_calls_by_index = {}
|
||||
for chunk in chunks:
|
||||
result = self.detector.parse_streaming_increment(chunk, extra_tools)
|
||||
normal_text += result.normal_text
|
||||
for call in result.calls:
|
||||
if call.tool_index is not None:
|
||||
if call.tool_index not in tool_calls_by_index:
|
||||
tool_calls_by_index[call.tool_index] = {
|
||||
"name": "",
|
||||
"parameters": "",
|
||||
}
|
||||
if call.name:
|
||||
tool_calls_by_index[call.tool_index]["name"] = call.name
|
||||
if call.parameters:
|
||||
tool_calls_by_index[call.tool_index][
|
||||
"parameters"
|
||||
] += call.parameters
|
||||
self.assertIn("Hello!", normal_text)
|
||||
self.assertIn("Let me also check", normal_text)
|
||||
self.assertEqual(len(tool_calls_by_index), 2)
|
||||
self.assertEqual(tool_calls_by_index[0]["name"], "get_weather")
|
||||
self.assertEqual(tool_calls_by_index[1]["name"], "get_time")
|
||||
params0 = json.loads(tool_calls_by_index[0]["parameters"])
|
||||
params1 = json.loads(tool_calls_by_index[1]["parameters"])
|
||||
self.assertEqual(params0["location"], "Paris")
|
||||
self.assertEqual(params1["timezone"], "UTC")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -5,6 +5,7 @@ import unittest
|
||||
from sglang.srt.parser.reasoning_parser import (
|
||||
BaseReasoningFormatDetector,
|
||||
DeepSeekR1Detector,
|
||||
Gemma4Detector,
|
||||
Glm45Detector,
|
||||
KimiDetector,
|
||||
KimiK2Detector,
|
||||
@@ -586,6 +587,141 @@ class TestNemotron3Detector(CustomTestCase):
|
||||
self.assertEqual(result.reasoning_text, "")
|
||||
|
||||
|
||||
class TestGemma4Detector(CustomTestCase):
|
||||
def setUp(self):
|
||||
self.detector = Gemma4Detector()
|
||||
|
||||
def test_init(self):
|
||||
"""Test Gemma4Detector initialization."""
|
||||
self.assertEqual(self.detector.think_start_token, "<|channel>")
|
||||
self.assertEqual(self.detector.think_end_token, "<channel|>")
|
||||
self.assertEqual(self.detector.think_start_self_label, "thought\n")
|
||||
self.assertFalse(self.detector._in_reasoning)
|
||||
self.assertTrue(self.detector.stream_reasoning)
|
||||
|
||||
def test_detect_and_parse_complete_reasoning(self):
|
||||
"""Test parsing complete Gemma4 reasoning block (think_start_self_label is stripped)."""
|
||||
text = "<|channel>thought\nLet me think about this<channel|>The answer is 42."
|
||||
result = self.detector.detect_and_parse(text)
|
||||
self.assertEqual(result.reasoning_text, "Let me think about this")
|
||||
self.assertEqual(result.normal_text, "The answer is 42.")
|
||||
|
||||
def test_detect_and_parse_without_thinking(self):
|
||||
"""Test parsing without thinking (enable_thinking=False case)."""
|
||||
text = "Direct answer without thinking."
|
||||
result = self.detector.detect_and_parse(text)
|
||||
self.assertEqual(result.normal_text, text)
|
||||
self.assertEqual(result.reasoning_text, "")
|
||||
|
||||
def test_detect_and_parse_reasoning_only(self):
|
||||
"""Test parsing when output is all reasoning (no end token yet)."""
|
||||
text = "<|channel>thought\nStill thinking..."
|
||||
result = self.detector.detect_and_parse(text)
|
||||
self.assertEqual(result.reasoning_text, "Still thinking...")
|
||||
self.assertEqual(result.normal_text, "")
|
||||
|
||||
def test_streaming_complete_flow(self):
|
||||
"""Test streaming parse of Gemma4 reasoning flow."""
|
||||
chunks = [
|
||||
"<|channel>",
|
||||
"thought\nreasoning content",
|
||||
"<channel|>",
|
||||
"final answer",
|
||||
]
|
||||
all_reasoning = ""
|
||||
all_normal = ""
|
||||
for chunk in chunks:
|
||||
result = self.detector.parse_streaming_increment(chunk)
|
||||
all_reasoning += result.reasoning_text
|
||||
all_normal += result.normal_text
|
||||
self.assertIn("reasoning content", all_reasoning)
|
||||
self.assertIn("final answer", all_normal)
|
||||
|
||||
def test_streaming_full_start_sequence(self):
|
||||
"""Test streaming with the full start sequence (token + self_label)."""
|
||||
# Gemma4 start sequence is "<|channel>thought\n", not just "<|channel>"
|
||||
result = self.detector.parse_streaming_increment("<|channel>thought\n")
|
||||
self.assertEqual(result.normal_text, "")
|
||||
self.assertEqual(result.reasoning_text, "")
|
||||
self.assertTrue(self.detector._in_reasoning)
|
||||
|
||||
result = self.detector.parse_streaming_increment("reasoning content")
|
||||
self.assertEqual(result.reasoning_text, "reasoning content")
|
||||
self.assertEqual(result.normal_text, "")
|
||||
|
||||
def test_streaming_partial_start_buffered(self):
|
||||
"""Test that partial start sequence is buffered."""
|
||||
# "<|channel>" alone is a prefix of "<|channel>thought\n", so it's buffered
|
||||
result = self.detector.parse_streaming_increment("<|channel>")
|
||||
self.assertEqual(result.normal_text, "")
|
||||
self.assertEqual(result.reasoning_text, "")
|
||||
|
||||
def test_streaming_end_token_mid_chunk(self):
|
||||
"""Test end token arriving in the same chunk as reasoning content."""
|
||||
self.detector.parse_streaming_increment("<|channel>thought\n")
|
||||
result = self.detector.parse_streaming_increment(
|
||||
"some reasoning<channel|>the answer"
|
||||
)
|
||||
self.assertEqual(result.reasoning_text, "some reasoning")
|
||||
self.assertEqual(result.normal_text, "the answer")
|
||||
self.assertFalse(self.detector._in_reasoning)
|
||||
|
||||
def test_streaming_split_end_token(self):
|
||||
"""Test end token split across two chunks."""
|
||||
self.detector.parse_streaming_increment("<|channel>thought\n")
|
||||
self.detector.parse_streaming_increment("reasoning content")
|
||||
|
||||
result1 = self.detector.parse_streaming_increment("<chan")
|
||||
self.assertEqual(result1.normal_text, "")
|
||||
|
||||
result2 = self.detector.parse_streaming_increment("nel|>final answer")
|
||||
self.assertFalse(self.detector._in_reasoning)
|
||||
self.assertIn("final answer", result2.normal_text)
|
||||
|
||||
def test_streaming_self_label_split_across_chunks(self):
|
||||
"""Test self_label ('thought\\n') arriving separately from start token."""
|
||||
result1 = self.detector.parse_streaming_increment("<|channel>")
|
||||
self.assertEqual(result1.reasoning_text, "")
|
||||
self.assertEqual(result1.normal_text, "")
|
||||
|
||||
result2 = self.detector.parse_streaming_increment("thought\n")
|
||||
self.assertTrue(self.detector._in_reasoning)
|
||||
|
||||
result3 = self.detector.parse_streaming_increment("reasoning here")
|
||||
self.assertEqual(result3.reasoning_text, "reasoning here")
|
||||
|
||||
def test_streaming_force_reasoning(self):
|
||||
"""Test streaming with force_reasoning=True (no start token needed)."""
|
||||
detector = Gemma4Detector(force_reasoning=True)
|
||||
|
||||
result1 = detector.parse_streaming_increment("reasoning content")
|
||||
self.assertEqual(result1.reasoning_text, "reasoning content")
|
||||
self.assertEqual(result1.normal_text, "")
|
||||
|
||||
result2 = detector.parse_streaming_increment("<channel|>the answer")
|
||||
self.assertFalse(detector._in_reasoning)
|
||||
self.assertIn("the answer", result2.normal_text)
|
||||
|
||||
def test_streaming_multiple_reasoning_chunks(self):
|
||||
"""Test reasoning content arriving in many small chunks."""
|
||||
self.detector.parse_streaming_increment("<|channel>thought\n")
|
||||
|
||||
all_reasoning = ""
|
||||
for chunk in ["Think", "ing ", "step ", "by ", "step."]:
|
||||
result = self.detector.parse_streaming_increment(chunk)
|
||||
all_reasoning += result.reasoning_text
|
||||
self.assertEqual(result.normal_text, "")
|
||||
self.assertEqual(all_reasoning, "Thinking step by step.")
|
||||
|
||||
def test_force_reasoning(self):
|
||||
"""Test Gemma4Detector with force_reasoning=True."""
|
||||
detector = Gemma4Detector(force_reasoning=True)
|
||||
text = "This should be reasoning<channel|>The answer."
|
||||
result = detector.detect_and_parse(text)
|
||||
self.assertEqual(result.reasoning_text, "This should be reasoning")
|
||||
self.assertEqual(result.normal_text, "The answer.")
|
||||
|
||||
|
||||
class TestReasoningParser(CustomTestCase):
|
||||
def test_init_valid_model(self):
|
||||
"""Test initialization with valid model types."""
|
||||
@@ -604,6 +740,9 @@ class TestReasoningParser(CustomTestCase):
|
||||
parser = ReasoningParser("glm45")
|
||||
self.assertIsInstance(parser.detector, Glm45Detector)
|
||||
|
||||
parser = ReasoningParser("gemma4")
|
||||
self.assertIsInstance(parser.detector, Gemma4Detector)
|
||||
|
||||
def test_init_invalid_model(self):
|
||||
"""Test initialization with invalid model type."""
|
||||
with self.assertRaises(ValueError) as context:
|
||||
@@ -782,6 +921,35 @@ class TestIntegrationScenarios(CustomTestCase):
|
||||
self.assertIn("multiple factors", all_reasoning)
|
||||
self.assertIn("42", all_normal)
|
||||
|
||||
def test_gemma4_complete_response(self):
|
||||
"""Test complete Gemma4 response parsing (think_start_self_label stripped)."""
|
||||
parser = ReasoningParser("gemma4")
|
||||
text = "<|channel>thought\nI need to solve x + 2 = 5. Subtracting 2: x = 3.<channel|>The answer is x = 3."
|
||||
reasoning, normal = parser.parse_non_stream(text)
|
||||
self.assertIn("x = 3", reasoning)
|
||||
self.assertNotIn("thought\n", reasoning)
|
||||
self.assertEqual(normal, "The answer is x = 3.")
|
||||
|
||||
def test_gemma4_streaming_scenario(self):
|
||||
"""Test Gemma4 streaming scenario."""
|
||||
parser = ReasoningParser("gemma4")
|
||||
chunks = [
|
||||
"<|channel>",
|
||||
"thought\nLet me analyze.",
|
||||
" Multiple factors.",
|
||||
"<channel|>",
|
||||
"The solution is 42.",
|
||||
]
|
||||
all_reasoning = ""
|
||||
all_normal = ""
|
||||
for chunk in chunks:
|
||||
reasoning, normal = parser.parse_stream_chunk(chunk)
|
||||
all_reasoning += reasoning
|
||||
all_normal += normal
|
||||
self.assertIn("analyze", all_reasoning)
|
||||
self.assertIn("Multiple factors", all_reasoning)
|
||||
self.assertIn("42", all_normal)
|
||||
|
||||
def test_empty_reasoning_blocks(self):
|
||||
"""Test handling of empty reasoning blocks."""
|
||||
parser = ReasoningParser("qwen3")
|
||||
|
||||
Reference in New Issue
Block a user