[Bugfix][Frontend] Fix Inkling tool name leaking into content after reasoning (#58792)
Signed-off-by: Baljinder Hothi <baljinder.hothi@cohere.com> Co-authored-by: Flora Feng <4florafeng@gmail.com>
Baljinder Hothi committed
Sep 25, 2026 at 20:38 UTC
fdcce47e9bd9d0068770e4076b8aab5b09150244
2 files changed
+38
-3
tests/parser/engine/test_inkling.py
+34
@@ -23,8 +23,13 @@ from vllm.entrypoints.openai.chat_completion.protocol import (
23
ChatCompletionToolsParam,
24
FunctionDefinition,
25
)
26
+from vllm.parser.abstract_parser import DelegatingParser
27
from vllm.parser.engine.events import EventType
28
from vllm.parser.engine.parser_engine_config import ParserState
29
+from vllm.parser.engine.registered_adapters import (
30
+ InklingParserReasoningAdapter,
31
+ InklingParserToolAdapter,
32
+)
33
from vllm.parser.engine.streaming_parser_engine import StreamingParserEngine
34
from vllm.parser.inkling import InklingParser, _inkling_arg_converter, inkling_config
35
from vllm.parser.parser_manager import ParserManager
@@ -166,6 +171,11 @@ def _delegating(mock_tokenizer, tools=None):
171
return parser_cls(mock_tokenizer, tools or [])
172
173
174
+class _TwoPassInklingParser(DelegatingParser):
175
+ reasoning_parser_cls = InklingParserReasoningAdapter
176
+ tool_parser_cls = InklingParserToolAdapter
177
+
178
+
179
def _stream_delegating(parser, request, text, chunk_size, prompt_token_ids):
180
"""Stream ``text`` through ``DelegatingParser.parse_delta``, ``chunk_size``
181
tokens per delta; return ``(content, reasoning, ordered tool names,
@@ -759,6 +769,30 @@ class TestDelegatingTwoPass:
769
assert TOOL_JSON not in content
770
assert END_MESSAGE not in content
771
772
+ def test_function_name_not_in_content_after_reasoning(
773
+ self, mock_tokenizer, mock_request
774
+ ):
775
+ name = "search_documents_by_keyword"
776
+ tools = [_function_tool(name)]
777
+ mock_request.tools = tools
778
+ text = (
779
+ f"{THINK_START} context.{END_MESSAGE}{MSG_MODEL}"
780
+ f"{name}{_tool_block(name, '{}')}"
781
+ )
782
+
783
+ content, reasoning, names, args = _stream_delegating(
784
+ _TwoPassInklingParser(mock_tokenizer, tools),
785
+ mock_request,
786
+ text,
787
+ chunk_size=12,
788
+ prompt_token_ids=self.GEN_PROMPT,
789
+ )
790
+
791
+ assert reasoning == " context."
792
+ assert content == ""
793
+ assert names == [name]
794
+ assert json.loads(args[0]) == {}
795
+
796
@pytest.mark.parametrize("opener", [TEXT_START, TOOL_TEXT, TOOL_ERROR])
797
@pytest.mark.parametrize("chunk_size", [1, 3, 64])
798
def test_visible_text_then_tool_streaming(
vllm/parser/inkling.py
+4
-3
@@ -167,11 +167,12 @@ def inkling_config() -> ParserEngineConfig:
167
(EventType.REASONING_END, EventType.TOOL_CALL_START),
168
),
169
}
170
- # Block-end terminals behave identically regardless of label. A
171
- # closed tool block returns to CONTENT (Inkling has no section wrapper;
170
+ # A closed tool block returns to CONTENT (Inkling has no section wrapper;
171
# blocks of any kind may follow), which also keeps the block-kind
172
# and role tokens out of the engine's tool-terminal set so the
173
# skip_tool_parsing reasoning pass still classifies reasoning.
174
+ # In a reasoning block, THINK_END defers REASONING_END to the next block
175
+ # opener so the function-name header is not emitted as content.
176
for end in ("THINK_END", "END_SAMPLING"):
177
transitions[(ParserState.CONTENT, end)] = Transition(
178
ParserState.CONTENT,
@@ -179,7 +180,7 @@ def inkling_config() -> ParserEngineConfig:
180
)
181
transitions[(ParserState.REASONING, end)] = Transition(
182
ParserState.CONTENT,
182
- (EventType.REASONING_END,),
183
+ () if end == "THINK_END" else (EventType.REASONING_END,),
184
)
185
transitions[(ParserState.TOOL_ARGS, end)] = Transition(
186
ParserState.CONTENT,