forked from Karylab-cklius/vllm
fix(openai): tolerate empty content in forced tool choice (#40148)
Signed-off-by: QwertyJack <7554089+QwertyJack@users.noreply.github.com> Signed-off-by: chaunceyjiang <chaunceyjiang@gmail.com> Co-authored-by: QwertyJack <7554089+QwertyJack@users.noreply.github.com> Co-authored-by: chaunceyjiang <chaunceyjiang@gmail.com>
This commit is contained in:
co-authored by
QwertyJack
chaunceyjiang
parent
df8e63f4ed
commit
6467213a9f
@@ -518,7 +518,13 @@ async def test_inconsistent_tool_choice_and_tools(
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_max_tokens_with_tool_choice_required(client: openai.AsyncOpenAI):
|
||||
@pytest.mark.parametrize(
|
||||
"tool_choice",
|
||||
["required", {"type": "function", "function": {"name": "get_current_weather"}}],
|
||||
)
|
||||
async def test_max_tokens_with_tool_choice_required(
|
||||
client: openai.AsyncOpenAI, tool_choice
|
||||
):
|
||||
""" """
|
||||
models = await client.models.list()
|
||||
model_name: str = models.data[0].id
|
||||
@@ -530,7 +536,7 @@ async def test_max_tokens_with_tool_choice_required(client: openai.AsyncOpenAI):
|
||||
max_completion_tokens=1,
|
||||
model=model_name,
|
||||
tools=tools,
|
||||
tool_choice="required",
|
||||
tool_choice=tool_choice,
|
||||
)
|
||||
# When `tool_choice="required"` and the tokens of `tools` exceed `max_tokens`,
|
||||
# both `tool_calls` and `content` should be empty.
|
||||
@@ -538,4 +544,3 @@ async def test_max_tokens_with_tool_choice_required(client: openai.AsyncOpenAI):
|
||||
choice = chat_completion.choices[0]
|
||||
assert choice.finish_reason == "length"
|
||||
assert len(choice.message.tool_calls) == 0
|
||||
assert choice.message.content == ""
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import pytest
|
||||
|
||||
from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
|
||||
from vllm.entrypoints.openai.engine.serving import OpenAIServing
|
||||
from vllm.entrypoints.openai.responses.protocol import ResponsesRequest
|
||||
from vllm.parser.abstract_parser import DelegatingParser
|
||||
|
||||
pytestmark = pytest.mark.skip_global_cleanup
|
||||
|
||||
|
||||
class _DummyDelegatingParser(DelegatingParser):
|
||||
def is_reasoning_end(self, input_ids: list[int]) -> bool:
|
||||
return False
|
||||
|
||||
def extract_content_ids(self, input_ids: list[int]) -> list[int]:
|
||||
return input_ids
|
||||
|
||||
def extract_reasoning(self, model_output: str, request):
|
||||
return None, model_output
|
||||
|
||||
def extract_reasoning_streaming(
|
||||
self,
|
||||
previous_text: str,
|
||||
current_text: str,
|
||||
delta_text: str,
|
||||
previous_token_ids: list[int],
|
||||
current_token_ids: list[int],
|
||||
delta_token_ids: list[int],
|
||||
):
|
||||
return None
|
||||
|
||||
def extract_tool_calls(self, model_output: str, request):
|
||||
return None
|
||||
|
||||
|
||||
def test_parse_tool_calls_from_content_allows_named_tool_choice_with_none_content():
|
||||
request = ChatCompletionRequest.model_validate(
|
||||
{
|
||||
"model": "test-model",
|
||||
"messages": [{"role": "user", "content": "test"}],
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
},
|
||||
}
|
||||
],
|
||||
"tool_choice": {"type": "function", "function": {"name": "get_weather"}},
|
||||
}
|
||||
)
|
||||
|
||||
tool_calls, content = OpenAIServing._parse_tool_calls_from_content(
|
||||
request=request,
|
||||
tokenizer=None,
|
||||
enable_auto_tools=True,
|
||||
tool_parser_cls=None,
|
||||
content=None,
|
||||
)
|
||||
|
||||
assert content is None
|
||||
assert tool_calls is not None
|
||||
assert tool_calls == []
|
||||
|
||||
|
||||
def test_responses_parser_allows_named_tool_choice_with_none_content():
|
||||
request = ResponsesRequest.model_validate(
|
||||
{
|
||||
"model": "test-model",
|
||||
"input": "test",
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"name": "get_weather",
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
}
|
||||
],
|
||||
"tool_choice": {"type": "function", "name": "get_weather"},
|
||||
}
|
||||
)
|
||||
parser = _DummyDelegatingParser(tokenizer=None)
|
||||
|
||||
tool_calls, content = parser._parse_tool_calls(
|
||||
request=request,
|
||||
content=None,
|
||||
enable_auto_tools=False,
|
||||
)
|
||||
|
||||
assert content is None
|
||||
assert tool_calls == []
|
||||
@@ -1307,8 +1307,8 @@ class OpenAIServingChat(OpenAIServing):
|
||||
request.tool_choice
|
||||
and type(request.tool_choice) is ChatCompletionNamedToolChoiceParam
|
||||
):
|
||||
assert tool_calls is not None and len(tool_calls) > 0
|
||||
tool_call_class_items = []
|
||||
tool_calls = tool_calls or []
|
||||
for idx, tc in enumerate(tool_calls):
|
||||
# Use native ID if available (e.g., Kimi K2),
|
||||
# otherwise generate ID with correct id_type
|
||||
|
||||
@@ -638,8 +638,9 @@ class OpenAIServing:
|
||||
and request.tool_choice
|
||||
and isinstance(request.tool_choice, ToolChoiceFunction)
|
||||
):
|
||||
assert content is not None
|
||||
# Forced Function Call (Responses API)
|
||||
if content is None:
|
||||
return [], None
|
||||
function_calls.append(
|
||||
FunctionCall(name=request.tool_choice.name, arguments=content)
|
||||
)
|
||||
@@ -651,7 +652,8 @@ class OpenAIServing:
|
||||
and (tool_parser_cls is None or tool_parser_cls.supports_required_and_named)
|
||||
):
|
||||
# Named function with standard JSON-based parsing
|
||||
assert content is not None
|
||||
if content is None:
|
||||
return [], None
|
||||
function_calls.append(
|
||||
FunctionCall(name=request.tool_choice.function.name, arguments=content)
|
||||
)
|
||||
|
||||
@@ -459,7 +459,8 @@ class DelegatingParser(Parser):
|
||||
(ToolChoiceFunction, ChatCompletionNamedToolChoiceParam),
|
||||
):
|
||||
# Forced Function Call
|
||||
assert content is not None
|
||||
if content is None:
|
||||
return [], None
|
||||
function_calls.append(
|
||||
FunctionCall(name=self._get_function_name(request), arguments=content)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user