main
py 199 lines 6.63 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import openai
5 import pytest
6
7 from .utils import (
8 MESSAGES_WITHOUT_TOOLS,
9 SEED,
10 WEATHER_TOOL,
11 ServerConfig,
12 ensure_system_prompt,
13 )
14
15
16 # test: make sure chat completions without tools provided work even when tools
17 # are enabled. This makes sure tool call chat templates work, AND that the tool
18 # parser stream processing doesn't change the output of the model.
19 @pytest.mark.asyncio
20 async def test_chat_completion_without_tools(
21 client: openai.AsyncOpenAI, server_config: ServerConfig
22 ):
23 models = await client.models.list()
24 model_name: str = models.data[0].id
25 chat_completion = await client.chat.completions.create(
26 messages=ensure_system_prompt(MESSAGES_WITHOUT_TOOLS, server_config),
27 temperature=0,
28 max_completion_tokens=150,
29 model=model_name,
30 logprobs=False,
31 seed=SEED,
32 )
33 choice = chat_completion.choices[0]
34 stop_reason = chat_completion.choices[0].finish_reason
35 output_text = chat_completion.choices[0].message.content
36
37 # check to make sure we got text
38 assert output_text is not None
39 assert len(output_text) > 0
40 assert stop_reason != "tool_calls"
41
42 # check to make sure no tool calls were returned
43 assert choice.message.tool_calls is None or len(choice.message.tool_calls) == 0
44
45 # make the same request, streaming
46 stream = await client.chat.completions.create(
47 messages=ensure_system_prompt(MESSAGES_WITHOUT_TOOLS, server_config),
48 temperature=0,
49 max_completion_tokens=150,
50 model=model_name,
51 logprobs=False,
52 seed=SEED,
53 stream=True,
54 )
55 chunks: list[str] = []
56 finish_reason_count = 0
57 role_sent: bool = False
58
59 # assemble streamed chunks
60 async for chunk in stream:
61 delta = chunk.choices[0].delta
62
63 # make sure the role is assistant
64 if delta.role:
65 assert not role_sent
66 assert delta.role == "assistant"
67 role_sent = True
68
69 if delta.content:
70 chunks.append(delta.content)
71
72 if chunk.choices[0].finish_reason is not None:
73 finish_reason_count += 1
74 assert chunk.choices[0].finish_reason == choice.finish_reason
75
76 # make sure tool call chunks aren't being streamed
77 assert not delta.tool_calls or len(delta.tool_calls) == 0
78
79 # make sure the role was sent, only 1 finish reason was sent, that chunks
80 # were in fact sent, and that the chunks match non-streaming
81 assert role_sent
82 assert finish_reason_count == 1
83 assert len(chunks)
84 assert "".join(chunks) == output_text
85
86
87 # test: conversation with tools enabled and provided that should not invoke
88 # tools, to make sure we can still get normal chat completion responses
89 # and that they won't be parsed as tools
90 @pytest.mark.asyncio
91 async def test_chat_completion_with_tools(
92 client: openai.AsyncOpenAI, server_config: ServerConfig
93 ):
94 models = await client.models.list()
95 model_name: str = models.data[0].id
96 chat_completion = await client.chat.completions.create(
97 messages=ensure_system_prompt(MESSAGES_WITHOUT_TOOLS, server_config),
98 temperature=0,
99 max_completion_tokens=150,
100 model=model_name,
101 tools=[WEATHER_TOOL],
102 logprobs=False,
103 seed=SEED,
104 )
105 choice = chat_completion.choices[0]
106 stop_reason = chat_completion.choices[0].finish_reason
107 output_text = chat_completion.choices[0].message.content
108
109 # check to make sure we got text
110 assert output_text is not None
111 assert stop_reason != "tool_calls"
112 assert len(output_text) > 0
113
114 # check to make sure no tool calls were returned
115 assert choice.message.tool_calls is None or len(choice.message.tool_calls) == 0
116
117 # make the same request, streaming
118 stream = await client.chat.completions.create(
119 messages=ensure_system_prompt(MESSAGES_WITHOUT_TOOLS, server_config),
120 temperature=0,
121 max_completion_tokens=150,
122 model=model_name,
123 logprobs=False,
124 tools=[WEATHER_TOOL],
125 seed=SEED,
126 stream=True,
127 )
128
129 chunks: list[str] = []
130 finish_reason_count = 0
131 role_sent: bool = False
132
133 # assemble streamed chunks
134 async for chunk in stream:
135 delta = chunk.choices[0].delta
136
137 # make sure the role is assistant
138 if delta.role:
139 assert delta.role == "assistant"
140 role_sent = True
141
142 if delta.content:
143 chunks.append(delta.content)
144
145 if chunk.choices[0].finish_reason is not None:
146 finish_reason_count += 1
147
148 # make sure tool call chunks aren't being streamed
149 assert not delta.tool_calls or len(delta.tool_calls) == 0
150
151 # make sure the role was sent, only 1 finish reason was sent, that chunks
152 # were in fact sent, and that the chunks match non-streaming
153 assert role_sent
154 assert finish_reason_count == 1
155 assert chunk.choices[0].finish_reason == stop_reason
156 assert chunk.choices[0].finish_reason != "tool_calls"
157 assert len(chunks)
158 assert "".join(chunks) == output_text
159
160
161 # Regression test for https://github.com/vllm-project/vllm/issues/32006
162 # Engine crash when combining response_format: json_object with
163 # tool_choice: required
164 @pytest.mark.asyncio
165 @pytest.mark.timeout(120)
166 async def test_response_format_with_tool_choice_required(
167 client: openai.AsyncOpenAI, server_config: ServerConfig
168 ):
169 """Test that combining response_format: json_object with tool_choice: required
170 doesn't crash the engine.
171
172 Before the fix, this would cause a validation error:
173 "You can only use one kind of structured outputs constraint but multiple
174 are specified" because both json_object and json (from tool schema) would
175 be set in StructuredOutputsParams.
176 """
177 models = await client.models.list()
178 model_name: str = models.data[0].id
179
180 # This combination previously crashed the engine
181 chat_completion = await client.chat.completions.create(
182 messages=ensure_system_prompt(
183 [{"role": "user", "content": "What is the weather in Dallas, Texas?"}],
184 server_config,
185 ),
186 temperature=0,
187 max_completion_tokens=150,
188 model=model_name,
189 tools=[WEATHER_TOOL],
190 tool_choice="required",
191 response_format={"type": "json_object"},
192 )
193
194 # The fix clears response_format when tool_choice forces tool calling,
195 # so the request should complete successfully with tool calls
196 choice = chat_completion.choices[0]
197 assert choice.finish_reason == "tool_calls"
198 assert choice.message.tool_calls is not None
199 assert len(choice.message.tool_calls) > 0