Checklist
Describe the bug
I encountered this bug when tool call names were outputted by ~10% of our Inkling sessions which seemed very strange for a frontier lab model. Asked Devin to research and it produced this issue report. I confirmed everything myself, but everything beyond here is written by Devin:
Describe the bug
When an Inkling turn's first block is a tool invocation and the generated text does
not start with a <|message_model|> opener, InklingDetector emits the tool name as
ordinary visible content. The tool call itself is still parsed correctly, so the
caller receives both a valid tool_calls entry and a stray assistant message
containing just the tool's name:
{
"content": "get_weather",
"tool_calls": [{"function": {"name": "get_weather", "arguments": "{}"}}]
}
A turn that opens with a thinking block is unaffected, because the model emits its
own <|message_model|> for the following block.
The cause is in python/sglang/srt/parser/reasoning_parser.py. _parse_blocks only
enters header mode when it actually sees the opener token:
# reasoning_parser.py:865-869
if token == MESSAGE_MODEL:
if self._kind in (None, "header"):
flush_reasoning()
self._pending_header = ""
self._kind = "header"
With no opener, self._kind is still None when the tool name is emitted, so
emit() falls through to the trailing branch and routes it to visible content:
# reasoning_parser.py:846-852
elif self._kind == "header":
self._pending_header += text
elif text:
# No open block — e.g. a continue_final_message stream resuming
# mid text block. Route to visible content, matching the
# ...
content.append(text)
By the time <|content_invoke_tool_json|> arrives, the name has already been
appended to content. That branch does reconstruct the framing when a header was
buffered, but not when _kind is None:
# reasoning_parser.py:877-885
flush_reasoning()
if self._kind == "header":
content.extend((MESSAGE_MODEL, self._pending_header, token))
self._pending_header = ""
else:
content.append(token)
self._kind = "tool"
I realize the elif text: fallback is deliberate for resumed continue_final_message
streams. The distinguishing signal arrives one token later though: when
<|content_invoke_tool_json|> / <|content_invoke_tool_text|> is reached with
_kind is None, the immediately preceding text run can only have been a tool header,
so it could be reclassified into _pending_header instead of staying in content.
Happy to send a PR along those lines if that's the direction you'd want.
Affects both streaming and non-streaming, and reproduces on a hosted deployment at
roughly 10% of all tool-calling turns — every turn that begins with a tool call
instead of a thinking block.
Reproduction
Parser-level, no model or GPU needed. Run the attached script from an sglang checkout:
python sglang-inkling-issue-repro.py
case | mode | status | reasoning | visible | tool calls
------------------------------------------------------------------------------
A missing opener | one-shot | LEAK | '' | 'get_weather' | get_weather({})
A missing opener | streamed | LEAK | '' | 'get_weather' | get_weather({})
B opener present | one-shot | OK | '' | '' | get_weather({})
B opener present | streamed | OK | '' | '' | get_weather({})
C thinking first | one-shot | OK | 'Checking the forecast.' | '' | get_weather({})
C thinking first | streamed | OK | 'Checking the forecast.' | '' | get_weather({})
The three inputs, all of which should produce empty visible content:
A get_weather<|content_invoke_tool_json|>{"name":"get_weather","args":{}}<|end_message|>
B <|message_model|>get_weather<|content_invoke_tool_json|>{"name":"get_weather","args":{}}<|end_message|>
C <|content_thinking|>Checking the forecast.<|end_message|><|message_model|>get_weather<|content_invoke_tool_json|>{"name":"get_weather","args":{}}<|end_message|>
The script drives the parsers in the same order as the OpenAI serving path —
InklingDetector first, then FunctionCallParser(tool_call_parser="inkling") on the
resulting normal_text — one-shot via detect_and_parse and streamed in 7-character
increments.
Inkling Issue Repro.py
Environment
Reproduced against main @ bae8eb8d6caa26a8820ce70c35cdfe57fa5182b3, Python 3.12.8,
Linux x86_64.
python3 -m sglang.check_env can't run here: the repro only needs the parser modules,
so the venv is CPU-only (torch 2.3.0+cpu, no CUDA), and check_env.py:585-595 selects
an env class from is_cuda_v2() / is_hip() / is_npu() / is_musa() / is_mps()
with no CPU fallback, so env.check_env() raises NameError: name 'env' is not defined. Let me know if you want the output from a GPU host instead.
Checklist
Describe the bug
I encountered this bug when tool call names were outputted by ~10% of our Inkling sessions which seemed very strange for a frontier lab model. Asked Devin to research and it produced this issue report. I confirmed everything myself, but everything beyond here is written by Devin:
Describe the bug
When an Inkling turn's first block is a tool invocation and the generated text does
not start with a
<|message_model|>opener,InklingDetectoremits the tool name asordinary visible content. The tool call itself is still parsed correctly, so the
caller receives both a valid
tool_callsentry and a stray assistant messagecontaining just the tool's name:
{ "content": "get_weather", "tool_calls": [{"function": {"name": "get_weather", "arguments": "{}"}}] }A turn that opens with a thinking block is unaffected, because the model emits its
own
<|message_model|>for the following block.The cause is in
python/sglang/srt/parser/reasoning_parser.py._parse_blocksonlyenters header mode when it actually sees the opener token:
With no opener,
self._kindis stillNonewhen the tool name is emitted, soemit()falls through to the trailing branch and routes it to visible content:By the time
<|content_invoke_tool_json|>arrives, the name has already beenappended to
content. That branch does reconstruct the framing when a header wasbuffered, but not when
_kindisNone:I realize the
elif text:fallback is deliberate for resumedcontinue_final_messagestreams. The distinguishing signal arrives one token later though: when
<|content_invoke_tool_json|>/<|content_invoke_tool_text|>is reached with_kind is None, the immediately preceding text run can only have been a tool header,so it could be reclassified into
_pending_headerinstead of staying incontent.Happy to send a PR along those lines if that's the direction you'd want.
Affects both streaming and non-streaming, and reproduces on a hosted deployment at
roughly 10% of all tool-calling turns — every turn that begins with a tool call
instead of a thinking block.
Reproduction
Parser-level, no model or GPU needed. Run the attached script from an sglang checkout:
The three inputs, all of which should produce empty visible content:
The script drives the parsers in the same order as the OpenAI serving path —
InklingDetectorfirst, thenFunctionCallParser(tool_call_parser="inkling")on theresulting
normal_text— one-shot viadetect_and_parseand streamed in 7-characterincrements.
Inkling Issue Repro.py
Environment
Reproduced against
main@bae8eb8d6caa26a8820ce70c35cdfe57fa5182b3, Python 3.12.8,Linux x86_64.
python3 -m sglang.check_envcan't run here: the repro only needs the parser modules,so the venv is CPU-only (torch 2.3.0+cpu, no CUDA), and
check_env.py:585-595selectsan env class from
is_cuda_v2()/is_hip()/is_npu()/is_musa()/is_mps()with no CPU fallback, so
env.check_env()raisesNameError: name 'env' is not defined. Let me know if you want the output from a GPU host instead.