diff --git a/tests/parser/engine/test_inkling.py b/tests/parser/engine/test_inkling.py index 9026d548df..1ce653f8b1 100644 --- a/tests/parser/engine/test_inkling.py +++ b/tests/parser/engine/test_inkling.py @@ -23,8 +23,13 @@ from vllm.entrypoints.openai.chat_completion.protocol import ( ChatCompletionToolsParam, FunctionDefinition, ) +from vllm.parser.abstract_parser import DelegatingParser from vllm.parser.engine.events import EventType from vllm.parser.engine.parser_engine_config import ParserState +from vllm.parser.engine.registered_adapters import ( + InklingParserReasoningAdapter, + InklingParserToolAdapter, +) from vllm.parser.engine.streaming_parser_engine import StreamingParserEngine from vllm.parser.inkling import InklingParser, _inkling_arg_converter, inkling_config from vllm.parser.parser_manager import ParserManager @@ -166,6 +171,11 @@ def _delegating(mock_tokenizer, tools=None): return parser_cls(mock_tokenizer, tools or []) +class _TwoPassInklingParser(DelegatingParser): + reasoning_parser_cls = InklingParserReasoningAdapter + tool_parser_cls = InklingParserToolAdapter + + def _stream_delegating(parser, request, text, chunk_size, prompt_token_ids): """Stream ``text`` through ``DelegatingParser.parse_delta``, ``chunk_size`` tokens per delta; return ``(content, reasoning, ordered tool names, @@ -759,6 +769,30 @@ class TestDelegatingTwoPass: assert TOOL_JSON not in content assert END_MESSAGE not in content + def test_function_name_not_in_content_after_reasoning( + self, mock_tokenizer, mock_request + ): + name = "search_documents_by_keyword" + tools = [_function_tool(name)] + mock_request.tools = tools + text = ( + f"{THINK_START} context.{END_MESSAGE}{MSG_MODEL}" + f"{name}{_tool_block(name, '{}')}" + ) + + content, reasoning, names, args = _stream_delegating( + _TwoPassInklingParser(mock_tokenizer, tools), + mock_request, + text, + chunk_size=12, + prompt_token_ids=self.GEN_PROMPT, + ) + + assert reasoning == " context." + assert content == "" + assert names == [name] + assert json.loads(args[0]) == {} + @pytest.mark.parametrize("opener", [TEXT_START, TOOL_TEXT, TOOL_ERROR]) @pytest.mark.parametrize("chunk_size", [1, 3, 64]) def test_visible_text_then_tool_streaming( diff --git a/vllm/parser/inkling.py b/vllm/parser/inkling.py index cc786c66dd..46be6f02b6 100644 --- a/vllm/parser/inkling.py +++ b/vllm/parser/inkling.py @@ -167,11 +167,12 @@ def inkling_config() -> ParserEngineConfig: (EventType.REASONING_END, EventType.TOOL_CALL_START), ), } - # Block-end terminals behave identically regardless of label. A - # closed tool block returns to CONTENT (Inkling has no section wrapper; + # A closed tool block returns to CONTENT (Inkling has no section wrapper; # blocks of any kind may follow), which also keeps the block-kind # and role tokens out of the engine's tool-terminal set so the # skip_tool_parsing reasoning pass still classifies reasoning. + # In a reasoning block, THINK_END defers REASONING_END to the next block + # opener so the function-name header is not emitted as content. for end in ("THINK_END", "END_SAMPLING"): transitions[(ParserState.CONTENT, end)] = Transition( ParserState.CONTENT, @@ -179,7 +180,7 @@ def inkling_config() -> ParserEngineConfig: ) transitions[(ParserState.REASONING, end)] = Transition( ParserState.CONTENT, - (EventType.REASONING_END,), + () if end == "THINK_END" else (EventType.REASONING_END,), ) transitions[(ParserState.TOOL_ARGS, end)] = Transition( ParserState.CONTENT,