| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | |
| 4 | import json |
| 5 | |
| 6 | import pytest |
| 7 | |
| 8 | from tests.parser.engine.replay_harness import MockTokenizer, _test_request |
| 9 | from vllm.reasoning import ReasoningParserManager |
| 10 | from vllm.tool_parsers import ToolParserManager |
| 11 | |
| 12 | |
| 13 | def _tokenizer() -> MockTokenizer: |
| 14 | vocab = { |
| 15 | "<think>": 1, |
| 16 | "</think>": 2, |
| 17 | "<tool_call>": 3, |
| 18 | "</tool_call>": 4, |
| 19 | "<arg_key>": 5, |
| 20 | "</arg_key>": 6, |
| 21 | "<arg_value>": 7, |
| 22 | "</arg_value>": 8, |
| 23 | } |
| 24 | return MockTokenizer(vocab=vocab, tokens=[]) |
| 25 | |
| 26 | |
| 27 | def test_ling3_registered(): |
| 28 | assert ReasoningParserManager.get_reasoning_parser("ling3") is not None |
| 29 | assert ToolParserManager.get_tool_parser("ling3") is not None |
| 30 | |
| 31 | |
| 32 | def test_ling3_defaults_thinking_on(): |
| 33 | parser_cls = ReasoningParserManager.get_reasoning_parser("ling3") |
| 34 | parser = parser_cls(_tokenizer()) |
| 35 | |
| 36 | assert parser.reasoning_start_str == "<think>" |
| 37 | assert parser.reasoning_end_str == "</think>" |
| 38 | |
| 39 | reasoning, content = parser.extract_reasoning( |
| 40 | "<think>reason</think>answer", _test_request() |
| 41 | ) |
| 42 | |
| 43 | assert reasoning == "reason" |
| 44 | assert content == "answer" |
| 45 | |
| 46 | |
| 47 | def test_ling3_disable_thinking_keeps_reasoning_as_content(): |
| 48 | parser_cls = ReasoningParserManager.get_reasoning_parser("ling3") |
| 49 | parser = parser_cls( |
| 50 | _tokenizer(), |
| 51 | chat_template_kwargs={"enable_thinking": False}, |
| 52 | ) |
| 53 | |
| 54 | reasoning, content = parser.extract_reasoning( |
| 55 | "<think>reason</think>answer", _test_request() |
| 56 | ) |
| 57 | |
| 58 | assert reasoning is None |
| 59 | assert content == "<think>reason</think>answer" |
| 60 | |
| 61 | |
| 62 | @pytest.mark.parametrize("model_output", ["only reasoning", "<think>only reasoning"]) |
| 63 | def test_ling3_truncated_reasoning_stays_reasoning(model_output): |
| 64 | parser_cls = ReasoningParserManager.get_reasoning_parser("ling3") |
| 65 | parser = parser_cls( |
| 66 | _tokenizer(), |
| 67 | chat_template_kwargs={"enable_thinking": True}, |
| 68 | ) |
| 69 | |
| 70 | reasoning, content = parser.extract_reasoning(model_output, _test_request()) |
| 71 | |
| 72 | assert reasoning == "only reasoning" |
| 73 | assert content is None |
| 74 | |
| 75 | |
| 76 | def test_ling3_tool_call_without_newline(): |
| 77 | parser_cls = ToolParserManager.get_tool_parser("ling3") |
| 78 | parser = parser_cls(_tokenizer()) |
| 79 | |
| 80 | result = parser.extract_tool_calls( |
| 81 | "<tool_call>get_weather<arg_key>city</arg_key>" |
| 82 | "<arg_value>Beijing</arg_value></tool_call>", |
| 83 | request=_test_request(), |
| 84 | ) |
| 85 | |
| 86 | assert result.tools_called |
| 87 | assert result.content is None |
| 88 | assert result.tool_calls[0].function.name == "get_weather" |
| 89 | assert json.loads(result.tool_calls[0].function.arguments) == { |
| 90 | "city": "Beijing", |
| 91 | } |
| 92 | |
| 93 | |
| 94 | def test_ling3_tool_call_without_args(): |
| 95 | parser_cls = ToolParserManager.get_tool_parser("ling3") |
| 96 | parser = parser_cls(_tokenizer()) |
| 97 | |
| 98 | result = parser.extract_tool_calls( |
| 99 | "<tool_call>ping</tool_call>", |
| 100 | request=_test_request(), |
| 101 | ) |
| 102 | |
| 103 | assert result.tools_called |
| 104 | assert result.content is None |
| 105 | assert result.tool_calls[0].function.name == "ping" |
| 106 | assert json.loads(result.tool_calls[0].function.arguments) == {} |