sgl-project/sglang · error · DS32EncodingError
No tool calls but found tool output
Error message
No tool calls but found tool output
What it means
A tool output was found but the preceding assistant message has no (or too few) tool_calls. The encoder computes tool_call_order = index - prev_assistant_idx and requires the assistant's tool_calls list to be at least that long.
Source
Thrown at python/sglang/srt/entrypoints/openai/encoding_dsv32.py:240
prompt += thinking_end_token
elif role == "tool":
prev_assistant_idx = index - 1
assistant_msg = messages[prev_assistant_idx]
while prev_assistant_idx >= 0 and assistant_msg.get("role") == "tool":
prev_assistant_idx -= 1
assistant_msg = messages[prev_assistant_idx]
if not (
index == 0
or (prev_assistant_idx >= 0 and assistant_msg.get("role") == "assistant")
):
raise DS32EncodingError(f"Invalid messages at {index}:\n{assistant_msg}")
tool_call_order = index - prev_assistant_idx
assistant_tool_calls = assistant_msg.get("tool_calls")
if not (assistant_tool_calls and len(assistant_tool_calls) >= tool_call_order):
raise DS32EncodingError("No tool calls but found tool output")
if tool_call_order == 1:
prompt += "\n\n<function_results>"
prompt += tool_output_template.format(content=content)
if tool_call_order == len(assistant_tool_calls):
prompt += "\n</function_results>"
if index >= last_user_idx and thinking_mode == "thinking":
prompt += "\n\n" + thinking_start_token
else:
prompt += "\n\n" + thinking_end_token
elif role == "assistant":
prev_assistant_idx = index
thinking_part = ""
View on GitHub (pinned to 0132848349)
Solutions
- Attach the matching tool_calls array to the assistant message.
- Remove orphan tool messages that have no corresponding assistant tool_call.
- Ensure the count of tool messages after an assistant turn does not exceed len(assistant['tool_calls']).
Example fix
# before
{"role":"assistant","content":"let me check"}
{"role":"tool","content":"42"}
# after
{"role":"assistant","content":"","tool_calls":[{"id":"1","function":{"name":"calc","arguments":"{}"}}]}
{"role":"tool","tool_call_id":"1","content":"42"} Defensive patterns
Strategy: validation
Validate before calling
for i,m in enumerate(messages):
if m['role']=='tool':
a=next((x for x in reversed(messages[:i]) if x['role']=='assistant'),None)
assert a and a.get('tool_calls') Type guard
null
Prevention
- Never emit a tool result without a matching assistant tool_call.
- Verify len(tool msgs) <= len(assistant tool_calls).
When it happens
Trigger: Assistant message without 'tool_calls', or with fewer tool_calls than the number of subsequent tool messages; orphan tool outputs after the assistant turn.
Common situations: Framework strips empty tool_calls arrays; partial history where only some tool results were kept; assistant replied with content only but a stale tool result follows.
Related errors
- No call message found for {call_id}
- Invalid thinking_mode `{thinking_mode}`
- Invalid messages at {index}: {assistant_msg}
- Assistant tool call function.arguments must be a JSON object
- every tool call must be a JSON object with a 'name'
AI-assisted analysis of sgl-project/sglang@0132848349 (2026-08-28).
Data as JSON: /api/errors/a52d3b605ee0f902.
Report an issue: GitHub.