[Bugfix][Parser] Confirm reasoning end when an Inkling content block opens (#49876)

Signed-off-by: Vegetog <110553275+Vegetog@users.noreply.github.com>
This commit is contained in:
Vegetog
2026-08-08 18:07:39 -04:00
committed by GitHub
parent 75231eff2f
commit 1c1077c6cc
2 changed files with 57 additions and 11 deletions
+46 -5
View File
@@ -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(
@@ -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:
@@ -615,7 +626,7 @@ class TestDelegatingTwoPass:
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}",
@@ -734,7 +745,7 @@ class TestDelegatingTwoPass:
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}"
@@ -745,3 +756,33 @@ class TestDelegatingTwoPass:
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
+11 -6
View File
@@ -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,),
),
(ParserState.CONTENT, "THINK_START"): Transition(
ParserState.REASONING,
@@ -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.
@@ -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,
@@ -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(