Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions tests/entrypoints/openai/chat_completion/test_serving_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,50 @@ async def test_chat_per_request_metrics_suppressed_for_n_greater_than_one():
assert response.metrics is None


_WEATHER_TOOL = {
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather in a given city",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
},
}


@pytest.mark.asyncio
async def test_required_tool_choice_does_not_claim_a_tool_call_it_did_not_make():
"""``finish_reason`` must not say "tool_calls" when there are none.

With no tool parser configured the parse yields no calls, so a "required"
request would otherwise answer "tool_calls" beside an empty ``tool_calls``.
The streaming path already requires ``tools_streamed[i]``.
"""
serving = _build_minimal_metrics_serving_chat(enable_per_request_metrics=False)
response = await serving.chat_completion_full_generator(
ChatCompletionRequest(
model="test-model",
messages=[{"role": "user", "content": "Hello, how are you?"}],
tools=[_WEATHER_TOOL],
tool_choice="required",
max_tokens=10,
stream=False,
),
_single_request_output(_make_metrics_request_output()),
"chatcmpl-test-id",
"test-model",
conversation=[{"role": "user", "content": "Hello, how are you?"}],
tokenizer=MagicMock(),
request_metadata=RequestResponseMetadata(request_id="chatcmpl-test-id"),
)
choice = response.choices[0]
assert not choice.message.tool_calls
assert choice.finish_reason == "stop"


@pytest.mark.asyncio
async def test_chat_streaming_metrics_ride_on_usage_chunk():
serving = _build_minimal_metrics_serving_chat(enable_per_request_metrics=True)
Expand Down
3 changes: 3 additions & 0 deletions vllm/entrypoints/openai/chat_completion/serving.py
Original file line number Diff line number Diff line change
Expand Up @@ -1070,10 +1070,13 @@ async def chat_completion_full_generator(
# In OpenAI's API, when a tool is called, the finish_reason is:
# "tool_calls" for "auto" or "required" tool calls,
# and "stop" for named tool calls.
# Only translate when a call was actually produced; the streaming
# path guards the same way via ``tools_streamed[i]``.
is_finish_reason_tool_calls = auto_tools_called or (
request.tool_choice
and request.tool_choice == "required"
and output.finish_reason == "stop"
and bool(tool_calls)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you take a look at how OpenAI online handles this case? I think the original logic was correct.

)

routed_experts_b64 = (
Expand Down
Loading