204 lines
6.0 KiB
Python
204 lines
6.0 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
|
|
import pytest
|
|
from openai.types.chat.chat_completion import ChatCompletion as OpenAIChatCompletion
|
|
from openai.types.chat.chat_completion_chunk import ChatCompletionChunk
|
|
|
|
from vllm.entrypoints.openai.chat_completion.protocol import (
|
|
ChatCompletionRequest,
|
|
ChatCompletionResponse,
|
|
ChatCompletionResponseChoice,
|
|
ChatCompletionResponseStreamChoice,
|
|
ChatCompletionStreamResponse,
|
|
ChatMessage,
|
|
)
|
|
from vllm.entrypoints.openai.engine.protocol import (
|
|
DeltaFunctionCall,
|
|
DeltaMessage,
|
|
DeltaToolCall,
|
|
FunctionCall,
|
|
ToolCall,
|
|
UsageInfo,
|
|
)
|
|
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 test_chat_completion_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"}},
|
|
}
|
|
)
|
|
parser = _DummyDelegatingParser(tokenizer=None)
|
|
|
|
tool_calls, content = parser._extract_tool_calls(
|
|
content=None,
|
|
request=request,
|
|
enable_auto_tools=True,
|
|
)
|
|
|
|
assert content is 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._extract_tool_calls(
|
|
content=None,
|
|
request=request,
|
|
enable_auto_tools=False,
|
|
)
|
|
|
|
assert content is None
|
|
assert tool_calls == []
|
|
|
|
|
|
def _chat_response(message: ChatMessage) -> ChatCompletionResponse:
|
|
return ChatCompletionResponse(
|
|
model="test-model",
|
|
choices=[
|
|
ChatCompletionResponseChoice(
|
|
index=0,
|
|
message=message,
|
|
finish_reason="stop",
|
|
)
|
|
],
|
|
usage=UsageInfo(prompt_tokens=1, completion_tokens=1, total_tokens=2),
|
|
)
|
|
|
|
|
|
def test_chat_completion_response_omits_empty_tool_calls_payload():
|
|
response = _chat_response(ChatMessage(role="assistant", content="done"))
|
|
|
|
payload = response.model_dump()
|
|
payload_exclude_unset = response.model_dump(exclude_unset=True)
|
|
|
|
assert "tool_calls" not in payload["choices"][0]["message"]
|
|
assert "tool_calls" not in payload_exclude_unset["choices"][0]["message"]
|
|
parsed = OpenAIChatCompletion.model_validate(payload)
|
|
assert parsed.choices[0].message.tool_calls is None
|
|
|
|
|
|
def test_chat_completion_response_keeps_non_empty_tool_calls_payload():
|
|
response = _chat_response(
|
|
ChatMessage(
|
|
role="assistant",
|
|
content="",
|
|
tool_calls=[
|
|
ToolCall(
|
|
function=FunctionCall(
|
|
name="get_weather",
|
|
arguments='{"city": "Beijing"}',
|
|
)
|
|
)
|
|
],
|
|
)
|
|
)
|
|
|
|
message = response.model_dump()["choices"][0]["message"]
|
|
|
|
assert len(message["tool_calls"]) == 1
|
|
assert message["tool_calls"][0]["function"]["name"] == "get_weather"
|
|
|
|
|
|
def _stream_response(delta: DeltaMessage) -> ChatCompletionStreamResponse:
|
|
return ChatCompletionStreamResponse(
|
|
id="chatcmpl-test",
|
|
object="chat.completion.chunk",
|
|
created=1,
|
|
model="test-model",
|
|
choices=[
|
|
ChatCompletionResponseStreamChoice(
|
|
index=0,
|
|
delta=delta,
|
|
finish_reason=None,
|
|
)
|
|
],
|
|
)
|
|
|
|
|
|
def test_chat_completion_stream_response_omits_empty_tool_calls_payload():
|
|
response = _stream_response(DeltaMessage(content="done"))
|
|
|
|
payload = response.model_dump(exclude_unset=True)
|
|
payload_json = response.model_dump_json(exclude_unset=True)
|
|
|
|
assert "tool_calls" not in payload["choices"][0]["delta"]
|
|
parsed = ChatCompletionChunk.model_validate_json(payload_json)
|
|
assert parsed.choices[0].delta.tool_calls is None
|
|
|
|
|
|
def test_chat_completion_stream_response_keeps_non_empty_tool_calls_payload():
|
|
response = _stream_response(
|
|
DeltaMessage(
|
|
tool_calls=[
|
|
DeltaToolCall(
|
|
index=0,
|
|
id="call-test",
|
|
type="function",
|
|
function=DeltaFunctionCall(
|
|
name="get_weather",
|
|
arguments='{"city": "Beijing"}',
|
|
),
|
|
)
|
|
]
|
|
)
|
|
)
|
|
|
|
delta = response.model_dump(exclude_unset=True)["choices"][0]["delta"]
|
|
|
|
assert len(delta["tool_calls"]) == 1
|
|
assert delta["tool_calls"][0]["function"]["name"] == "get_weather"
|