Unverified Commit 431cea3e authored by Wojciech Wais's avatar Wojciech Wais Committed by GitHub
Browse files

[Bugfix] Fix tool_calls Iterable consumed when debug logging is enabled (#34844)


Signed-off-by: default avatarWojciech Wais <wojciech.wais@gmail.com>
Signed-off-by: default avatarmgoin <mgoin64@gmail.com>
Signed-off-by: default avatarXinyu Chen <xinyu1.chen@intel.com>
Signed-off-by: default avatarEkagra Ranjan <3116519+ekagra-ranjan@users.noreply.github.com>
Signed-off-by: default avatarRishi Puri <riship@nvidia.com>
Signed-off-by: default avatarJaebok Lee <jaebok9541@naver.com>
Signed-off-by: default avatarDarkLight1337 <tlleungac@connect.ust.hk>
Signed-off-by: default avataryuwei <yuwei@dev.local>
Signed-off-by: default avatarArtem Perevedentsev <aperevedents@nvidia.com>
Signed-off-by: default avatarIbrahim Arshad <38925737+ibrahim1023@users.noreply.github.com>
Signed-off-by: default avatarLi <chuali@amd.com>
Signed-off-by: default avatarchaunceyjiang <chaunceyjiang@gmail.com>
Signed-off-by: default avatarKunshang Ji <kunshang.ji@intel.com>
Signed-off-by: default avatarKunshang Ji <jikunshang95@gmail.com>
Signed-off-by: default avatarR <Ganesh.R@amd.com>
Signed-off-by: default avatarLucas Wilkinson <lwilkins@redhat.com>
Signed-off-by: default avatarlkm2835 <lkm2835@gmail.com>
Signed-off-by: default avatarRonen Schaffer <ronen.schaffer@ibm.com>
Signed-off-by: default avatarvnadathur <glvikramn@gmail.com>
Signed-off-by: default avatarWorldExplored <srreyansh.sethi@gmail.com>
Signed-off-by: default avatarSrreyansh Sethi <107075589+WorldExplored@users.noreply.github.com>
Signed-off-by: default avatarIsotr0py <mozf@mail2.sysu.edu.cn>
Signed-off-by: default avatarElham Harirpoush <elham.harirpoush@arm.com>
Signed-off-by: default avatarYan Ma <yan.ma@intel.com>
Signed-off-by: default avatarNick Hill <nickhill123@gmail.com>
Signed-off-by: jackwang2120's avatarjackcfwang <jackcfwang@tencent.com>
Signed-off-by: default avatarChendi Xue <chendi.xue@intel.com>
Signed-off-by: default avatarInjae Ryou <injaeryou@gmail.com>
Signed-off-by: default avatarRichard Zou <zou3519@gmail.com>
Signed-off-by: default avatarmilesial <milesial@users.noreply.github.com>
Signed-off-by: default avatarElvir Crncevic <elvircrn@gmail.com>
Signed-off-by: default avatarwhx-sjtu <2952154980@qq.com>
Signed-off-by: default avatarLalithnarayan C <Lalithnarayan.C@amd.com>
Signed-off-by: default avatarPatchouliTaisa <patchychen@tencent.com>
Signed-off-by: default avatarjatseng-ai <jatseng@amd.com>
Signed-off-by: default avatarjatseng-ai <janet.tseng@amd.com>
Signed-off-by: default avatarMatthias Gehre <matthias.gehre@amd.com>
Signed-off-by: default avatarxaguilar-amd <xaguilar@amd.com>
Signed-off-by: default avatarrdondeti <ravitez.dondeti@gmail.com>
Signed-off-by: default avatarRavitez Dondeti <ravitez.dondeti@gmail.com>
Signed-off-by: default avatarNickLucche <nlucches@redhat.com>
Signed-off-by: default avatarPeter Nguyen <petern0408@gmail.com>
Signed-off-by: default avatarwang.yuqi <yuqi.wang@daocloud.io>
Signed-off-by: default avatarzhuhaoran <zhuhaoran.zhr@alibaba-inc.com>
Signed-off-by: default avatarJee Jee Li <pandaleefree@gmail.com>
Signed-off-by: default avatartjtanaa <tunjian.tan@embeddedllm.com>
Signed-off-by: default avatarJesus Federico <jefp@amazon.com>
Signed-off-by: default avatarmanu <fortin.emmanuel@gmail.com>
Signed-off-by: default avatarZhanqiuHu <zhu@redhat.com>
Signed-off-by: default avatarYifan Zong <yzong@redhat.com>
Signed-off-by: default avatarRahul-Tuli <rtuli@redhat.com>
Signed-off-by: default avatarFynn Schmitt-Ulms <fschmitt@redhat.com>
Signed-off-by: default avatarHarry Mellor <19981378+hmellor@users.noreply.github.com>
Signed-off-by: default avatarMichael Goin <mgoin64@gmail.com>
Signed-off-by: default avatarBenjamin Chislett <bchislett@nvidia.com>
Signed-off-by: default avatarTianyu Guo <guoty9@mail2.sysu.edu.cn>
Signed-off-by: default avatarleeyongjun <jqueen.astro@gmail.com>
Signed-off-by: default avatarZiying Tao <tzzying@outlook.com>
Signed-off-by: default avatarjiang1.li <jiang1.li@intel.com>
Signed-off-by: default avatarVibhav Agarwal <vibhavagarwal5@gmail.com>
Signed-off-by: default avatarShubyM <shubymishra20@gmail.com>
Signed-off-by: default avatarwzhao18 <wzhao18.sz@gmail.com>
Signed-off-by: default avatarItay Etelis <itay.etelis@ibm.com>
Signed-off-by: default avatarEdalatiAli <aliedalati@cohere.com>
Signed-off-by: default avatarAndreas Karatzas <akaratza@amd.com>
Signed-off-by: default avatarr266-tech <r266.tech@gmail.com>
Signed-off-by: default avatarRoger Wang <hey@rogerw.io>
Signed-off-by: default avatarMartin Hickey <martin.hickey@ie.ibm.com>
Signed-off-by: default avatarMark McLoughlin <markmc@redhat.com>
Signed-off-by: default avatarAnimesh Jain <anijain@umich.edu>
Signed-off-by: default avatarYongye Zhu <zyy1102000@gmail.com>
Signed-off-by: default avatarzhxchen17 <zhxchen17@fb.com>
Signed-off-by: default avatarEricccYang <yangyang4991@gmail.com>
Signed-off-by: default avatarKaicheng Yang <53411596+EricccYang@users.noreply.github.com>
Signed-off-by: default avatarbaoloongmao <baoloongmao@tencent.com>
Signed-off-by: default avatarsihao.li <sihao.li@intel.com>
Signed-off-by: default avatarsfeng33 <4florafeng@gmail.com>
Signed-off-by: default avatarYufeng He <40085740+he-yufeng@users.noreply.github.com>
Signed-off-by: default avatarZhu, Zufang <zufang.zhu@intel.com>
Signed-off-by: default avatarTihomir Elek <tiho.elek@gmail.com>
Signed-off-by: default avataryiliu30 <yi4.liu@intel.com>
Signed-off-by: default avataryewentao256 <zhyanwentao@126.com>
Signed-off-by: default avatarSantino Ramos <santinor@inferact.ai>
Signed-off-by: default avatarhaosdent <haosdent@gmail.com>
Signed-off-by: default avatarJartX <sagformas@epdcenter.es>
Signed-off-by: default avatarGeorge-ao <yuyiao772@gmail.com>
Signed-off-by: default avatarYuyi Ao <yuyiao772@gmail.com>
Signed-off-by: default avatarTyler Michael Smith <tlrmchlsmth@gmail.com>
Signed-off-by: default avatarMukesh Baphna <mukesh@hippocraticai.com>
Signed-off-by: default avatarPedram Razavi <pedram.razavi@gmail.com>
Co-authored-by: default avatarmergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: default avatarMichael Goin <mgoin64@gmail.com>
Co-authored-by: default avatarXinyu Chen <xinyu1.chen@intel.com>
Co-authored-by: default avatarEkagra Ranjan <3116519+ekagra-ranjan@users.noreply.github.com>
Co-authored-by: default avatarRishi Puri <riship@nvidia.com>
Co-authored-by: default avatarzzaebok <44357534+zzaebok@users.noreply.github.com>
Co-authored-by: default avatarCyrus Leung <tlleungac@connect.ust.hk>
Co-authored-by: default avatarYuwei An <ayw.sirius19@gmail.com>
Co-authored-by: default avataryuwei <yuwei@dev.local>
Co-authored-by: default avatarArtem Perevedentsev <aperevedents@nvidia.com>
Co-authored-by: default avatarIbrahim Arshad <38925737+ibrahim1023@users.noreply.github.com>
Co-authored-by: default avatarChuan (Richard) Li <chuali@amd.com>
Co-authored-by: default avatarChauncey <chaunceyjiang@gmail.com>
Co-authored-by: default avatarKunshang Ji <kunshang.ji@intel.com>
Co-authored-by: default avatarGanesh R <ganesh.r@amd.com>
Co-authored-by: default avatarLucas Wilkinson <LucasWilkinson@users.noreply.github.com>
Co-authored-by: default avatarRobert Shaw <114415538+robertgshaw2-redhat@users.noreply.github.com>
Co-authored-by: default avatarKyungmin Lee <30465912+lkm2835@users.noreply.github.com>
Co-authored-by: default avatarRonen Schaffer <ronen.schaffer@ibm.com>
Co-authored-by: default avatarSrreyansh Sethi <107075589+WorldExplored@users.noreply.github.com>
Co-authored-by: default avatarvnadathur <glvikramn@gmail.com>
Co-authored-by: default avatarvnadathur <236933696+vnadathur@users.noreply.github.com>
Co-authored-by: default avatarIsotr0py <mozf@mail2.sysu.edu.cn>
Co-authored-by: default avatarElham <elham.harirpoush@arm.com>
Co-authored-by: default avatarYan Ma <yan.ma@intel.com>
Co-authored-by: default avatarNick Hill <nickhill123@gmail.com>
Co-authored-by: jackwang2120's avatarChaofan Wang <jackcfwang@tencent.com>
Co-authored-by: default avatarChendi.Xue <chendi.xue@intel.com>
Co-authored-by: default avatarInjae Ryou <injaeryou@gmail.com>
Co-authored-by: default avatarRichard Zou <zou3519@users.noreply.github.com>
Co-authored-by: default avatarmilesial <milesial@users.noreply.github.com>
Co-authored-by: default avatarElvir Crnčević <elvircrn@gmail.com>
Co-authored-by: default avatarClaude Sonnet 4 <noreply@anthropic.com>
Co-authored-by: default avatarHexiang Wang <56632993+whx-sjtu@users.noreply.github.com>
Co-authored-by: default avatarLalithnarayan C <Lalithnarayan.C@amd.com>
Co-authored-by: default avatargemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: default avatarLuka Govedič <ProExpertProg@users.noreply.github.com>
Co-authored-by: default avatarPatchyTIS <58251192+PatchouliTIS@users.noreply.github.com>
Co-authored-by: default avatarPatchouliTaisa <patchychen@tencent.com>
Co-authored-by: default avatarjatseng-ai <janet.tseng@amd.com>
Co-authored-by: default avatarMatthias Gehre <matthias.gehre@amd.com>
Co-authored-by: default avatarxaguilar-amd <xavier.aguilarfruto@amd.com>
Co-authored-by: default avatarRavitez Dondeti <dondetir@users.noreply.github.com>
Co-authored-by: default avatarNicolò Lucchesi <nlucches@redhat.com>
Co-authored-by: default avatarPeter Nguyen <petern0408@gmail.com>
Co-authored-by: default avatarwang.yuqi <yuqi.wang@daocloud.io>
Co-authored-by: default avatarzhrrr <43847754+izhuhaoran@users.noreply.github.com>
Co-authored-by: default avatarJee Jee Li <pandaleefree@gmail.com>
Co-authored-by: default avatarTJian <tunjian.tan@embeddedllm.com>
Co-authored-by: default avatarJesus Federico <14651+jefp@users.noreply.github.com>
Co-authored-by: default avatarManu <efortin@users.noreply.github.com>
Co-authored-by: default avatarzhanqiuhu <49648934+ZhanqiuHu@users.noreply.github.com>
Co-authored-by: default avataryzong-rh <yzong@redhat.com>
Co-authored-by: default avatarFynn Schmitt-Ulms <fschmitt@redhat.com>
Co-authored-by: default avatarRahul-Tuli <rtuli@redhat.com>
Co-authored-by: default avatarHarry Mellor <19981378+hmellor@users.noreply.github.com>
Co-authored-by: default avatarBenjamin Chislett <bchislett@nvidia.com>
Co-authored-by: default avatarTianyu Guo <guoty9@mail2.sysu.edu.cn>
Co-authored-by: default avatarLee Yongjun <35302114+elwhyjay@users.noreply.github.com>
Co-authored-by: default avatarz1ying <55220715+z1ying@users.noreply.github.com>
Co-authored-by: default avatarLi, Jiang <jiang1.li@intel.com>
Co-authored-by: default avatarVibhav Agarwal <vibhavagarwal5@gmail.com>
Co-authored-by: default avatarvibhav-agarwal <vibhav.agarwal@glance.com>
Co-authored-by: default avatarShubyM <shubymishra20@gmail.com>
Co-authored-by: default avatarWei Zhao <51183510+wzhao18@users.noreply.github.com>
Co-authored-by: default avatarItay Etelis <92247226+Etelis@users.noreply.github.com>
Co-authored-by: default avatarItay Etelis <itay.etelis@ibm.com>
Co-authored-by: default avatarEdalatiAli <aliedalati@cohere.com>
Co-authored-by: default avatarAndreas Karatzas <akaratza@amd.com>
Co-authored-by: default avatarr266-tech <r2668940489@gmail.com>
Co-authored-by: default avatarRoger Wang <hey@rogerw.io>
Co-authored-by: default avatarMartin Hickey <martin.hickey@ie.ibm.com>
Co-authored-by: default avatarOr Ozeri <or@ozery.com>
Co-authored-by: default avatarMark McLoughlin <markmc@redhat.com>
Co-authored-by: default avatarLe Yang <562593859@qq.com>
Co-authored-by: default avatarAnimesh Jain <anijain@umich.edu>
Co-authored-by: default avatarYongye Zhu <zyy1102000@gmail.com>
Co-authored-by: default avatarZhengxu Chen <zhxchen17@fb.com>
Co-authored-by: default avatarKaicheng Yang <53411596+EricccYang@users.noreply.github.com>
Co-authored-by: default avatarmaobaolong <baoloongmao@tencent.com>
Co-authored-by: default avatarsihao_li <165983188+1643661061leo@users.noreply.github.com>
Co-authored-by: default avatarFlora Feng <4florafeng@gmail.com>
Co-authored-by: default avatarYufeng He <40085740+he-yufeng@users.noreply.github.com>
Co-authored-by: default avatarzofia <110436990+zufangzhu@users.noreply.github.com>
Co-authored-by: default avatarTihomir Elek <tiho.elek@gmail.com>
Co-authored-by: default avatarYi Liu <yi4.liu@intel.com>
Co-authored-by: default avatarWentao Ye <44945378+yewentao256@users.noreply.github.com>
Co-authored-by: default avatarSantino Ramos <51103228+santiramos27@users.noreply.github.com>
Co-authored-by: default avatarhaosdent <haosdent@gmail.com>
Co-authored-by: default avatarJartX <sagformas@epdcenter.es>
Co-authored-by: default avatarYuyi Ao <yuyiao772@gmail.com>
Co-authored-by: default avatarTyler Michael Smith <tyler@neuralmagic.com>
Co-authored-by: default avatarmukesh-hai <mukesh@hippocraticai.com>
Co-authored-by: default avatarPedram Razavi <pedram@sierra.ai>
parent 799973af
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Unit tests for tool_calls Iterable → list materialisation.
Regression tests for https://github.com/vllm-project/vllm/issues/34792.
Setting VLLM_LOGGING_LEVEL=debug caused tool calling to break for Mistral
models because:
1. The OpenAI Python SDK types tool_calls as Iterable[...] in
ChatCompletionAssistantMessageParam.
2. Pydantic v2, when validating from Python objects (not from raw JSON),
wraps Iterable fields in a one-shot lazy iterator.
3. Debug logging called model_dump_json() which consumed that iterator.
4. The Mistral tokenizer then saw empty tool_calls and raised
"ValueError: Unexpected tool call id ...".
"""
import pytest
from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
def _make_tool_call(tc_id: str, name: str, args: str) -> dict:
return {
"id": tc_id,
"type": "function",
"function": {"name": name, "arguments": args},
}
def _make_request(messages: list) -> ChatCompletionRequest:
return ChatCompletionRequest(
model="test-model",
messages=messages,
)
def test_tool_calls_list_preserved_after_model_dump():
"""tool_calls in assistant messages must be readable after model_dump_json.
When the request is built from Python dicts (as in the Anthropic → OpenAI
conversion path), Pydantic v2 previously wrapped the Iterable tool_calls
in a one-shot iterator. model_dump_json() consumed it, leaving subsequent
readers (e.g. the Mistral tokenizer) with an empty sequence.
"""
tool_call = _make_tool_call("call_abc123", "get_weather", '{"city": "Paris"}')
messages = [
{"role": "user", "content": "What is the weather in Paris?"},
{"role": "assistant", "content": None, "tool_calls": [tool_call]},
{
"role": "tool",
"tool_call_id": "call_abc123",
"content": '{"temperature": 20}',
},
]
req = _make_request(messages)
# Simulate debug logging: serialize the model (this was the trigger)
_ = req.model_dump_json()
# The assistant message must still have accessible tool_calls afterwards
assistant_msg = req.messages[1]
assert isinstance(assistant_msg, dict)
tool_calls = assistant_msg.get("tool_calls")
assert tool_calls is not None, "tool_calls must not be None after model_dump_json"
assert isinstance(tool_calls, list), "tool_calls must be a list"
assert len(tool_calls) > 0, "tool_calls must not be empty after model_dump_json"
def test_tool_calls_from_generator_are_materialised():
"""tool_calls passed as a generator must be converted to list on validation."""
tool_call = _make_tool_call("call_gen1", "search", '{"query": "vllm"}')
def tool_calls_gen():
yield tool_call
messages = [
{"role": "user", "content": "Search for vllm"},
{
"role": "assistant",
"content": None,
"tool_calls": tool_calls_gen(), # one-shot generator
},
]
req = _make_request(messages)
assistant_msg = req.messages[1]
assert isinstance(assistant_msg, dict)
# Iterate twice — must not raise or return empty on second pass
tool_calls_first = list(assistant_msg.get("tool_calls", []))
tool_calls_second = list(assistant_msg.get("tool_calls", []))
assert len(tool_calls_first) == 1, "First read must return the tool call"
assert len(tool_calls_second) == 1, "Second read must also return the tool call"
def test_tool_calls_list_passthrough():
"""tool_calls already provided as a list must remain a list."""
tool_call = _make_tool_call("call_list1", "calculate", '{"expr": "2+2"}')
messages = [
{"role": "user", "content": "Calculate 2+2"},
{"role": "assistant", "content": None, "tool_calls": [tool_call]},
]
req = _make_request(messages)
assistant_msg = req.messages[1]
assert isinstance(assistant_msg, dict)
assert isinstance(assistant_msg.get("tool_calls"), list)
def test_messages_without_tool_calls_unaffected():
"""Messages without tool_calls must be handled correctly."""
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Hello!"},
{"role": "assistant", "content": "Hi there!"},
]
req = _make_request(messages)
# None of the messages should have tool_calls injected
for msg in req.messages:
assert isinstance(msg, dict)
assert msg.get("tool_calls") is None or msg.get("tool_calls") == []
@pytest.mark.parametrize("num_tool_calls", [1, 3])
def test_multiple_tool_calls_materialised(num_tool_calls: int):
"""Multiple tool calls in a single message are all preserved."""
tool_calls = [
_make_tool_call(f"call_{i}", f"func_{i}", f'{{"arg": {i}}}')
for i in range(num_tool_calls)
]
messages = [
{"role": "user", "content": "Do things"},
{"role": "assistant", "content": None, "tool_calls": iter(tool_calls)},
]
req = _make_request(messages)
assistant_msg = req.messages[1]
assert isinstance(assistant_msg, dict)
result_tool_calls = assistant_msg.get("tool_calls")
assert isinstance(result_tool_calls, list)
assert len(result_tool_calls) == num_tool_calls
# Verify after model_dump_json too
_ = req.model_dump_json()
assert len(assistant_msg.get("tool_calls", [])) == num_tool_calls
...@@ -290,7 +290,7 @@ class CustomChatCompletionMessageParam(TypedDict, total=False): ...@@ -290,7 +290,7 @@ class CustomChatCompletionMessageParam(TypedDict, total=False):
tool_call_id: str | None tool_call_id: str | None
"""Tool call that this message is responding to.""" """Tool call that this message is responding to."""
tool_calls: Iterable[ChatCompletionMessageToolCallParam] | None tool_calls: list[ChatCompletionMessageToolCallParam] | None
"""The tool calls generated by the model, such as function calls.""" """The tool calls generated by the model, such as function calls."""
reasoning: str | None reasoning: str | None
...@@ -321,7 +321,7 @@ class ConversationMessage(TypedDict, total=False): ...@@ -321,7 +321,7 @@ class ConversationMessage(TypedDict, total=False):
name: str | None name: str | None
"""The name of the function to call""" """The name of the function to call"""
tool_calls: Iterable[ChatCompletionMessageToolCallParam] | None tool_calls: list[ChatCompletionMessageToolCallParam] | None
"""The tool calls generated by the model, such as function calls.""" """The tool calls generated by the model, such as function calls."""
reasoning: str | None reasoning: str | None
......
...@@ -357,6 +357,47 @@ class ChatCompletionRequest(OpenAIBaseModel): ...@@ -357,6 +357,47 @@ class ChatCompletionRequest(OpenAIBaseModel):
# --8<-- [end:chat-completion-extra-params] # --8<-- [end:chat-completion-extra-params]
@model_validator(mode="before")
@classmethod
def _materialize_tool_calls_before(cls, data: Any) -> Any:
"""Eagerly convert tool_calls generators/iterators to lists.
Must run before Pydantic field validation so that one-shot
generators are not consumed during union type matching of
ChatCompletionAssistantMessageParam (which types tool_calls
as Iterable[...]).
"""
if not isinstance(data, dict):
return data
messages = data.get("messages")
if not isinstance(messages, list):
return data
for msg in messages:
if not isinstance(msg, dict):
continue
tool_calls = msg.get("tool_calls")
if tool_calls is not None and not isinstance(tool_calls, list):
msg["tool_calls"] = list(tool_calls)
return data
@model_validator(mode="after")
def _materialize_tool_calls_after(self) -> "ChatCompletionRequest":
"""Convert Pydantic ValidatorIterator wrappers back to lists.
Even after the "before" validator converts iterables to lists,
Pydantic re-wraps them in a ValidatorIterator when validating
against ChatCompletionAssistantMessageParam's Iterable[...] type.
This "after" pass materialises those wrappers so downstream code
(tokenizers, model_dump_json) always sees plain lists.
"""
for msg in self.messages:
if not isinstance(msg, dict):
continue
tool_calls = msg.get("tool_calls")
if tool_calls is not None and not isinstance(tool_calls, list):
msg["tool_calls"] = list(tool_calls)
return self
def build_chat_params( def build_chat_params(
self, self,
default_template: str | None, default_template: str | None,
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment