| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | |
| 4 | """parse_delta-level regression tests for the MuseGlimmer parsers. |
| 5 | |
| 6 | These drive the UNIFIED parser-engine streaming API the serving layer actually |
| 7 | uses (``DelegatingParser.parse_delta``), NOT the ``extract_*_streaming`` methods |
| 8 | directly. This is the coverage that was missing when a live no-tools streaming |
| 9 | request leaked raw harmony framing into ``content``: the phase machine marked |
| 10 | ``reasoning_ended=True`` at the prompt boundary (because ``is_reasoning_end`` on |
| 11 | a prompt with no ``to=self`` wrongly returned True), skipping the reasoning |
| 12 | phase for the whole generation. |
| 13 | |
| 14 | Requires a real MuseGlimmer tokenizer; skipped if the checkpoint is unavailable. |
| 15 | """ |
| 16 | |
| 17 | import json |
| 18 | import os |
| 19 | |
| 20 | import pytest |
| 21 | |
| 22 | CKPT = os.environ.get("MUSE_GLIMMER_CKPT", "") |
| 23 | |
| 24 | pytestmark = pytest.mark.skipif( |
| 25 | not os.path.isdir(CKPT), reason=f"MuseGlimmer checkpoint not found at {CKPT}" |
| 26 | ) |
| 27 | |
| 28 | _FRAMING = [ |
| 29 | "<|start|>", |
| 30 | "<|message|>", |
| 31 | "<|eom|>", |
| 32 | "<|eot|>", |
| 33 | "to=self", |
| 34 | "to=user", |
| 35 | "<atem:", |
| 36 | ] |
| 37 | |
| 38 | PROMPT = "<|start|>user<|message|>hi<|eom|><|start|>assistant" |
| 39 | |
| 40 | |
| 41 | class _Req: |
| 42 | tools = None |
| 43 | tool_choice = None |
| 44 | include_reasoning = True |
| 45 | |
| 46 | |
| 47 | @pytest.fixture(scope="module") |
| 48 | def tok(): |
| 49 | from transformers import AutoTokenizer |
| 50 | |
| 51 | return AutoTokenizer.from_pretrained(CKPT, trust_remote_code=True) |
| 52 | |
| 53 | |
| 54 | @pytest.fixture(scope="module") |
| 55 | def parser_cls(): |
| 56 | from vllm.parser import ParserManager |
| 57 | |
| 58 | return ParserManager.get_parser( |
| 59 | tool_parser_name="muse_glimmer", |
| 60 | reasoning_parser_name="muse_glimmer", |
| 61 | enable_auto_tools=True, |
| 62 | ) |
| 63 | |
| 64 | |
| 65 | def _drive(parser_cls, tok, gen_text, req=None): |
| 66 | """Feed gen_text token-by-token through parse_delta (as serving does).""" |
| 67 | from vllm.parser.abstract_parser import StreamState |
| 68 | |
| 69 | if req is None: |
| 70 | req = _Req() |
| 71 | parser = parser_cls(tok) |
| 72 | parser._stream_state = StreamState() |
| 73 | prompt_ids = tok.encode(PROMPT, add_special_tokens=False) |
| 74 | gen_ids = tok.encode(gen_text, add_special_tokens=False) |
| 75 | reasoning, content, tools = [], [], [] |
| 76 | for i, tid in enumerate(gen_ids): |
| 77 | dm = parser.parse_delta( |
| 78 | tok.decode([tid]), |
| 79 | [tid], |
| 80 | req, |
| 81 | prompt_token_ids=prompt_ids if i == 0 else None, |
| 82 | finished=(i == len(gen_ids) - 1), |
| 83 | ) |
| 84 | if dm is None: |
| 85 | continue |
| 86 | if getattr(dm, "reasoning", None): |
| 87 | reasoning.append(dm.reasoning) |
| 88 | if getattr(dm, "content", None): |
| 89 | content.append(dm.content) |
| 90 | for tc in getattr(dm, "tool_calls", None) or []: |
| 91 | fn = tc.function |
| 92 | name = fn.get("name") if isinstance(fn, dict) else getattr(fn, "name", None) |
| 93 | args = ( |
| 94 | fn.get("arguments") |
| 95 | if isinstance(fn, dict) |
| 96 | else getattr(fn, "arguments", None) |
| 97 | ) |
| 98 | tools.append((tc.index, name, args)) |
| 99 | return "".join(reasoning), "".join(content), tools |
| 100 | |
| 101 | |
| 102 | def _assert_no_framing(text): |
| 103 | for f in _FRAMING: |
| 104 | assert f not in text, f"framing {f!r} leaked: {text!r}" |
| 105 | |
| 106 | |
| 107 | def test_parse_delta_no_tools_reasoning_then_answer(parser_cls, tok): |
| 108 | # The core regression: reasoning phase must stay active for a no-tools turn. |
| 109 | reasoning, content, tools = _drive( |
| 110 | parser_cls, |
| 111 | tok, |
| 112 | " to=self<|message|>Let me think step by step about the sum.<|eom|>" |
| 113 | "<|start|>assistant to=user<|message|>The answer is 42.<|eot|>", |
| 114 | ) |
| 115 | _assert_no_framing(content) |
| 116 | assert reasoning == "Let me think step by step about the sum.", repr(reasoning) |
| 117 | assert content == "The answer is 42.", repr(content) |
| 118 | assert tools == [] |
| 119 | |
| 120 | |
| 121 | def test_parse_delta_content_only(parser_cls, tok): |
| 122 | reasoning, content, tools = _drive( |
| 123 | parser_cls, |
| 124 | tok, |
| 125 | " to=user<|message|>Just a direct answer.<|eot|>", |
| 126 | ) |
| 127 | _assert_no_framing(content) |
| 128 | assert content == "Just a direct answer.", repr(content) |
| 129 | assert tools == [] |
| 130 | |
| 131 | |
| 132 | def test_parse_delta_tool_call(parser_cls, tok): |
| 133 | reasoning, content, tools = _drive( |
| 134 | parser_cls, |
| 135 | tok, |
| 136 | " to=self<|message|>I should read the hostname.<|eom|>" |
| 137 | "<|start|>assistant to=read.read<|message|>" |
| 138 | '<atem:function_calls>\n<atem:invoke name="read.read">\n' |
| 139 | '<atem:parameter name="path">/etc/hostname</atem:parameter>\n' |
| 140 | "</atem:invoke>\n</atem:function_calls>", |
| 141 | ) |
| 142 | _assert_no_framing(content) |
| 143 | assert reasoning == "I should read the hostname.", repr(reasoning) |
| 144 | assert len(tools) == 1, tools |
| 145 | idx, name, args = tools[0] |
| 146 | assert idx == 0 and name == "read.read" |
| 147 | assert json.loads(args) == {"path": "/etc/hostname"} |
| 148 | |
| 149 | |
| 150 | def test_parse_delta_truncated_cot_no_toolcall(parser_cls, tok): |
| 151 | # Contemplated invoke inside an unterminated to=self block: no tool call, |
| 152 | # no framing leak into content, partial reasoning recovered. |
| 153 | reasoning, content, tools = _drive( |
| 154 | parser_cls, |
| 155 | tok, |
| 156 | " to=self<|message|>Maybe I should call " |
| 157 | '<atem:function_calls>\n<atem:invoke name="read.read">\n' |
| 158 | '<atem:parameter name="path">/etc/hostname</atem:parameter>\n' |
| 159 | "</atem:invoke>\n</atem:function_calls> but wait", |
| 160 | ) |
| 161 | _assert_no_framing(content) |
| 162 | assert tools == [], f"contemplated invoke leaked as tool call: {tools}" |
| 163 | assert "Maybe I should call" in reasoning, repr(reasoning) |
| 164 | |
| 165 | |
| 166 | def test_parse_delta_reasoning_suppressed_when_not_requested(parser_cls, tok): |
| 167 | class _NoReasonReq: |
| 168 | tools = None |
| 169 | tool_choice = None |
| 170 | include_reasoning = False |
| 171 | |
| 172 | reasoning, content, tools = _drive( |
| 173 | parser_cls, |
| 174 | tok, |
| 175 | " to=self<|message|>secret thoughts<|eom|>" |
| 176 | "<|start|>assistant to=user<|message|>Public answer.<|eot|>", |
| 177 | req=_NoReasonReq(), |
| 178 | ) |
| 179 | assert reasoning == "", repr(reasoning) # suppressed |
| 180 | assert content == "Public answer.", repr(content) |
| 181 | _assert_no_framing(content) |
| 182 | |
| 183 | |
| 184 | if __name__ == "__main__": |
| 185 | import sys |
| 186 | |
| 187 | sys.exit(pytest.main([__file__, "-v"])) |