Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 46 additions & 5 deletions tests/parser/engine/test_inkling.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,13 @@ def _delegating(mock_tokenizer, tools=None):

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)``."""
tokens per delta; return ``(content, reasoning, ordered tool names,
ordered tool arguments)``. Arguments arrive in fragments, so they are
concatenated per tool index."""
tokens = _tokenize(text)
content, reasoning, tools = "", "", {}
content, reasoning = "", ""
tools: dict[int, str] = {}
args: dict[int, str] = {}
for start in range(0, len(tokens), chunk_size):
batch = tokens[start : start + chunk_size]
delta = parser.parse_delta(
Expand All @@ -186,7 +190,14 @@ def _stream_delegating(parser, request, text, chunk_size, prompt_token_ids):
for tc in delta.tool_calls:
if tc.function and tc.function.name:
tools[tc.index] = tc.function.name
return content, reasoning, [tools[k] for k in sorted(tools)]
if tc.function and tc.function.arguments:
args[tc.index] = args.get(tc.index, "") + tc.function.arguments
return (
content,
reasoning,
[tools[k] for k in sorted(tools)],
[args.get(k, "") for k in sorted(tools)],
)


class TestArgConverter:
Expand Down Expand Up @@ -615,7 +626,7 @@ def test_plain_text_non_streaming(self, mock_tokenizer, mock_request):
def test_plain_text_streaming(self, mock_tokenizer, mock_request, chunk_size):
tools = [_function_tool()]
mock_request.tools = tools
content, _, calls = _stream_delegating(
content, _, calls, _ = _stream_delegating(
_delegating(mock_tokenizer, tools),
mock_request,
f"{TEXT_START}The answer is 42.{END_MESSAGE}",
Expand Down Expand Up @@ -734,7 +745,7 @@ def test_reset_reuse_after_incomplete_span(self, mock_tokenizer, mock_request):
def test_reasoning_then_tool_streaming(
self, mock_tokenizer, mock_request, chunk_size
):
content, _, tools = _stream_delegating(
content, _, tools, _ = _stream_delegating(
_delegating(mock_tokenizer),
mock_request,
f"{THINK_START}plan{END_MESSAGE}{MSG_MODEL}"
Expand All @@ -745,3 +756,33 @@ def test_reasoning_then_tool_streaming(
assert tools == ["get_weather"]
assert TOOL_JSON not in content
assert END_MESSAGE not in content

@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(
self, mock_tokenizer, mock_request, chunk_size, opener
):
"""Visible content before a tool call, with no thinking block.

The reasoning pass leaves its reasoning phase only on an explicit
reasoning-end event. A response that opens with visible content never
emitted one, so the tool pass never ran: the entire tool block came
back as assistant content and no tool call was parsed. Every opener
that starts a visible block has to confirm the boundary, which is why
``TOOL_TEXT`` and ``TOOL_ERROR`` are covered alongside ``TEXT_START``.
"""
tools = [_function_tool()]
mock_request.tools = tools
content, _, names, args = _stream_delegating(
_delegating(mock_tokenizer, tools),
mock_request,
f"{opener}let me check{END_MESSAGE}{MSG_MODEL}"
+ _tool_block("get_weather", '{"city":"SF"}'),
chunk_size,
self.GEN_PROMPT,
)
assert content == "let me check"
assert names == ["get_weather"]
assert json.loads(args[0]) == {"city": "SF"}
assert TOOL_JSON not in content
assert END_MESSAGE not in content
17 changes: 11 additions & 6 deletions vllm/parser/inkling.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,14 @@ def inkling_config() -> ParserEngineConfig:
ParserState.MESSAGE_HEADER,
(),
),
# Opening a block that renders as visible content proves no reasoning
# is still open. Confirming that here is what lets DelegatingParser
# hand the following blocks to the tool pass: the reasoning pass only
# leaves its reasoning phase on an explicit reasoning-end event, and a
# response with no thinking block never emits one otherwise.
(ParserState.CONTENT, "TEXT_START"): Transition(
ParserState.CONTENT,
(),
(EventType.REASONING_END,),
Comment on lines 197 to +199

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge End reasoning for raw and error tool blocks

In streaming DelegatingParser configurations with tools declared, TOOL_TEXT and TOOL_ERROR also open visible-content blocks, but only TEXT_START now emits REASONING_END. Those transitions therefore leave the outer parser in its reasoning phase; when the normal block-end terminal arrives, skip_tool_parsing re-emits it as text and the downstream tool pass never runs, leaking the literal <|end_message|> or end-sampling marker. Emit the same reasoning-end event for both visible raw/error block transitions, including their MESSAGE_HEADER variants.

Useful? React with 👍 / 👎.

),
(ParserState.CONTENT, "THINK_START"): Transition(
ParserState.REASONING,
Expand All @@ -204,11 +209,11 @@ def inkling_config() -> ParserEngineConfig:
# Raw / error tool blocks render as visible text.
(ParserState.CONTENT, "TOOL_TEXT"): Transition(
ParserState.CONTENT,
(),
(EventType.REASONING_END,),
),
(ParserState.CONTENT, "TOOL_ERROR"): Transition(
ParserState.CONTENT,
(),
(EventType.REASONING_END,),
),
# The optional function name between the model-role and content-kind
# markers is metadata, not visible assistant content.
Expand All @@ -218,7 +223,7 @@ def inkling_config() -> ParserEngineConfig:
),
(ParserState.MESSAGE_HEADER, "TEXT_START"): Transition(
ParserState.CONTENT,
(),
(EventType.REASONING_END,),
),
(ParserState.MESSAGE_HEADER, "THINK_START"): Transition(
ParserState.REASONING,
Expand All @@ -230,11 +235,11 @@ def inkling_config() -> ParserEngineConfig:
),
(ParserState.MESSAGE_HEADER, "TOOL_TEXT"): Transition(
ParserState.CONTENT,
(),
(EventType.REASONING_END,),
),
(ParserState.MESSAGE_HEADER, "TOOL_ERROR"): Transition(
ParserState.CONTENT,
(),
(EventType.REASONING_END,),
),
# ── Inside a thinking block ───────────────────────────────────
(ParserState.REASONING, "THINK_START"): Transition(
Expand Down
Loading