[Bugfix][Frontend] Keep length finish_reason for max_tokens-truncated streaming tool calls (#46303)

Signed-off-by: Ting Sun <suntcrick@gmail.com>

Ting SUN committed Sep 23, 2026 at 14:31 UTC b33c8a657d487a1f00cfd6fc9d2bdd70424ade7c
2 files changed +15 -5
tests/entrypoints/openai/chat_completion/test_serving_chat.py
+10 -4
@@ -2239,7 +2239,13 @@ async def test_tool_choice_validation_without_parser():
2239
2240
2241 @pytest.mark.asyncio
2242 -async def test_streaming_n_gt1_independent_tool_parsers():
2242 +@pytest.mark.parametrize(
2243 + ("engine_finish_reason", "expected_finish_reason"),
2244 + [("stop", "tool_calls"), ("length", "length")],
2245 +)
2246 +async def test_streaming_n_gt1_independent_tool_parsers(
2247 + engine_finish_reason: str, expected_finish_reason: str
2248 +):
2249 """n>1 streaming must use independent parser instances
2250 and token-id histories per choice.
2251 """
@@ -2344,7 +2350,7 @@ async def test_streaming_n_gt1_independent_tool_parsers():
2350 token_ids=[],
2351 cumulative_logprob=0.0,
2352 logprobs=None,
2347 - finish_reason="stop",
2353 + finish_reason=engine_finish_reason,
2354 )
2355 for choice_idx in range(num_choices)
2356 ],
@@ -2408,8 +2414,8 @@ async def test_streaming_n_gt1_independent_tool_parsers():
2414 assert len(reasons) == 1, (
2415 f"Choice {choice_idx}: expected exactly 1 finish_reason, got {reasons}"
2416 )
2411 - assert reasons[0] == "tool_calls", (
2412 - f"Choice {choice_idx}: expected finish_reason='tool_calls', "
2417 + assert reasons[0] == expected_finish_reason, (
2418 + f"Choice {choice_idx}: expected finish_reason={expected_finish_reason!r}, "
2419 f"got '{reasons[0]}'"
2420 )
2421
vllm/entrypoints/openai/chat_completion/serving.py
+5 -1
@@ -757,7 +757,11 @@ class OpenAIServingChat(GenerateBaseServing):
757 # finish_reason is:
758 # "tool_calls" for "auto" or "required" tool calls,
759 # and "stop" for named tool calls.
760 - if tools_streamed[i] and not tool_choice_function_name:
760 + if (
761 + tools_streamed[i]
762 + and not tool_choice_function_name
763 + and output.finish_reason == "stop"
764 + ):
765 finish_reason_ = "tool_calls"
766 else:
767 finish_reason_ = (