Skip to content

Commit 4cfd173

Browse files
committed
[Frontend] OpenAI Responses API supports Tool/Function calling
Signed-off-by: chaunceyjiang <chaunceyjiang@gmail.com>
1 parent 50e9124 commit 4cfd173

File tree

5 files changed

+142
-95
lines changed

5 files changed

+142
-95
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3+
import pytest
4+
import pytest_asyncio
5+
6+
from tests.utils import RemoteOpenAIServer
7+
8+
# Use a small reasoning model to test the responses API.
9+
MODEL_NAME = "Qwen/Qwen3-0.6B"
10+
11+
12+
@pytest.fixture(scope="module")
13+
def default_server_args():
14+
return [
15+
"--max-model-len",
16+
"8192",
17+
"--enforce-eager", # For faster startup.
18+
"--enable-auto-tool-choice",
19+
"--structured-outputs-config.backend",
20+
"xgrammar",
21+
"--tool-call-parser",
22+
"hermes",
23+
"--reasoning-parser",
24+
"qwen3",
25+
]
26+
27+
28+
@pytest.fixture(scope="module")
29+
def server_with_store(default_server_args):
30+
with RemoteOpenAIServer(
31+
MODEL_NAME,
32+
default_server_args,
33+
env_dict={"VLLM_ENABLE_RESPONSES_API_STORE": "1"},
34+
) as remote_server:
35+
yield remote_server
36+
37+
38+
@pytest_asyncio.fixture
39+
async def client(server_with_store):
40+
async with server_with_store.get_async_client() as async_client:
41+
yield async_client

tests/v1/entrypoints/openai/responses/test_function_call.py renamed to tests/entrypoints/openai/serving_responses/test_function_call.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
import openai # use the official client for correctness check
77
import pytest
88

9+
# Use a small reasoning model to test the responses API.
910
MODEL_NAME = "Qwen/Qwen3-0.6B"
11+
12+
1013
tools = [
1114
{
1215
"type": "function",

tests/v1/entrypoints/openai/responses/conftest.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,8 @@ def default_server_args():
1515
"--max-model-len",
1616
"8192",
1717
"--enforce-eager", # For faster startup.
18-
"--enable-auto-tool-choice",
19-
"--structured-outputs-config.backend",
20-
"xgrammar",
21-
"--tool-call-parser",
22-
"hermes",
2318
"--reasoning-parser",
24-
"qwen3",
19+
"deepseek_r1",
2520
]
2621

2722

vllm/entrypoints/openai/protocol.py

Lines changed: 1 addition & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
)
1717
from openai.types.chat.chat_completion_message import Annotation as OpenAIAnnotation
1818
from openai.types.responses import (
19-
FunctionTool,
2019
ResponseCodeInterpreterCallCodeDeltaEvent,
2120
ResponseCodeInterpreterCallCodeDoneEvent,
2221
ResponseCodeInterpreterCallCompletedEvent,
@@ -37,7 +36,6 @@
3736
ResponseWebSearchCallCompletedEvent,
3837
ResponseWebSearchCallInProgressEvent,
3938
ResponseWebSearchCallSearchingEvent,
40-
ToolChoiceFunction,
4139
)
4240
from openai.types.responses import (
4341
ResponseCompletedEvent as OpenAIResponseCompletedEvent,
@@ -73,6 +71,7 @@
7371

7472
from vllm import envs
7573
from vllm.entrypoints.chat_utils import ChatCompletionMessageParam, make_tool_call_id
74+
from vllm.entrypoints.openai.utils import get_json_schema_from_tool
7675
from vllm.entrypoints.score_utils import ScoreContentPartParam, ScoreMultiModalParam
7776
from vllm.logger import init_logger
7877
from vllm.logprobs import Logprob
@@ -296,91 +295,6 @@ def get_logits_processors(
296295
return None
297296

298297

299-
def get_json_schema_from_tool(
300-
tool_choice: str | ToolChoiceFunction | ChatCompletionNamedToolChoiceParam,
301-
tools: list[FunctionTool | ChatCompletionToolsParam] | None,
302-
) -> str | dict | None:
303-
if tool_choice in ("none", None) or tools is None:
304-
return None
305-
if (not isinstance(tool_choice, str)) and isinstance(
306-
tool_choice, ToolChoiceFunction
307-
):
308-
tool_name = tool_choice.name
309-
tool_map = {tool.name: tool for tool in tools if isinstance(tool, FunctionTool)}
310-
if tool_name not in tool_map:
311-
raise ValueError(f"Tool '{tool_name}' has not been passed in `tools`.")
312-
return tool_map[tool_name].parameters
313-
314-
if (not isinstance(tool_choice, str)) and isinstance(
315-
tool_choice, ChatCompletionNamedToolChoiceParam
316-
):
317-
tool_name = tool_choice.function.name
318-
tool_map = {
319-
tool.function.name: tool
320-
for tool in tools
321-
if isinstance(tool, ChatCompletionToolsParam)
322-
}
323-
if tool_name not in tool_map:
324-
raise ValueError(f"Tool '{tool_name}' has not been passed in `tools`.")
325-
return tool_map[tool_name].function.parameters
326-
327-
if tool_choice == "required":
328-
329-
def extract_tool_info(
330-
tool: Tool | ChatCompletionToolsParam,
331-
) -> tuple[str, dict[str, Any] | None]:
332-
if isinstance(tool, FunctionTool):
333-
return tool.name, tool.parameters
334-
elif isinstance(tool, ChatCompletionToolsParam):
335-
return tool.function.name, tool.function.parameters
336-
else:
337-
raise TypeError(f"Unsupported tool type: {type(tool)}")
338-
339-
def get_tool_schema(tool: Tool | ChatCompletionToolsParam) -> dict:
340-
name, params = extract_tool_info(tool)
341-
params = params if params else {"type": "object", "properties": {}}
342-
return {
343-
"properties": {
344-
"name": {"type": "string", "enum": [name]},
345-
"parameters": params,
346-
},
347-
"required": ["name", "parameters"],
348-
}
349-
350-
def get_tool_schema_defs(
351-
tools: list[Tool | ChatCompletionToolsParam],
352-
) -> dict:
353-
all_defs: dict[str, dict[str, Any]] = {}
354-
for tool in tools:
355-
_, params = extract_tool_info(tool)
356-
if params is None:
357-
continue
358-
defs = params.pop("$defs", {})
359-
for def_name, def_schema in defs.items():
360-
if def_name in all_defs and all_defs[def_name] != def_schema:
361-
raise ValueError(
362-
f"Tool definition '{def_name}' has multiple schemas, "
363-
"which is not supported."
364-
)
365-
all_defs[def_name] = def_schema
366-
return all_defs
367-
368-
json_schema = {
369-
"type": "array",
370-
"minItems": 1,
371-
"items": {
372-
"type": "object",
373-
"anyOf": [get_tool_schema(tool) for tool in tools],
374-
},
375-
}
376-
json_schema_defs = get_tool_schema_defs(tools)
377-
if json_schema_defs:
378-
json_schema["$defs"] = json_schema_defs
379-
return json_schema
380-
381-
return None
382-
383-
384298
ResponseInputOutputItem: TypeAlias = (
385299
ResponseInputItemParam | ResponseReasoningItem | ResponseFunctionToolCall
386300
)

vllm/entrypoints/openai/utils.py

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
# SPDX-License-Identifier: Apache-2.0
22
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
33
import base64
4-
from typing import Literal
4+
from typing import Any, Literal
55

66
import torch
7+
from openai.types.responses import (
8+
FunctionTool,
9+
ToolChoiceFunction,
10+
)
11+
from openai.types.responses.tool import Tool
712
from typing_extensions import assert_never
813

914
from vllm import PoolingRequestOutput
10-
from vllm.entrypoints.openai.protocol import EMBED_DTYPE_TO_TORCH_DTYPE
15+
from vllm.entrypoints.openai.protocol import (
16+
EMBED_DTYPE_TO_TORCH_DTYPE,
17+
ChatCompletionNamedToolChoiceParam,
18+
ChatCompletionToolsParam,
19+
)
1120

1221

1322
def encoding_pooling_output(
@@ -31,3 +40,88 @@ def encoding_pooling_output(
3140
return base64.b64encode(embedding_bytes).decode("utf-8")
3241

3342
assert_never(encoding_format)
43+
44+
45+
def get_json_schema_from_tool(
46+
tool_choice: str | ToolChoiceFunction | ChatCompletionNamedToolChoiceParam,
47+
tools: list[FunctionTool | ChatCompletionToolsParam] | None,
48+
) -> str | dict | None:
49+
if tool_choice in ("none", None) or tools is None:
50+
return None
51+
if (not isinstance(tool_choice, str)) and isinstance(
52+
tool_choice, ToolChoiceFunction
53+
):
54+
tool_name = tool_choice.name
55+
tool_map = {tool.name: tool for tool in tools if isinstance(tool, FunctionTool)}
56+
if tool_name not in tool_map:
57+
raise ValueError(f"Tool '{tool_name}' has not been passed in `tools`.")
58+
return tool_map[tool_name].parameters
59+
60+
if (not isinstance(tool_choice, str)) and isinstance(
61+
tool_choice, ChatCompletionNamedToolChoiceParam
62+
):
63+
tool_name = tool_choice.function.name
64+
tool_map = {
65+
tool.function.name: tool
66+
for tool in tools
67+
if isinstance(tool, ChatCompletionToolsParam)
68+
}
69+
if tool_name not in tool_map:
70+
raise ValueError(f"Tool '{tool_name}' has not been passed in `tools`.")
71+
return tool_map[tool_name].function.parameters
72+
73+
if tool_choice == "required":
74+
75+
def extract_tool_info(
76+
tool: Tool | ChatCompletionToolsParam,
77+
) -> tuple[str, dict[str, Any] | None]:
78+
if isinstance(tool, FunctionTool):
79+
return tool.name, tool.parameters
80+
elif isinstance(tool, ChatCompletionToolsParam):
81+
return tool.function.name, tool.function.parameters
82+
else:
83+
raise TypeError(f"Unsupported tool type: {type(tool)}")
84+
85+
def get_tool_schema(tool: Tool | ChatCompletionToolsParam) -> dict:
86+
name, params = extract_tool_info(tool)
87+
params = params if params else {"type": "object", "properties": {}}
88+
return {
89+
"properties": {
90+
"name": {"type": "string", "enum": [name]},
91+
"parameters": params,
92+
},
93+
"required": ["name", "parameters"],
94+
}
95+
96+
def get_tool_schema_defs(
97+
tools: list[Tool | ChatCompletionToolsParam],
98+
) -> dict:
99+
all_defs: dict[str, dict[str, Any]] = {}
100+
for tool in tools:
101+
_, params = extract_tool_info(tool)
102+
if params is None:
103+
continue
104+
defs = params.pop("$defs", {})
105+
for def_name, def_schema in defs.items():
106+
if def_name in all_defs and all_defs[def_name] != def_schema:
107+
raise ValueError(
108+
f"Tool definition '{def_name}' has multiple schemas, "
109+
"which is not supported."
110+
)
111+
all_defs[def_name] = def_schema
112+
return all_defs
113+
114+
json_schema = {
115+
"type": "array",
116+
"minItems": 1,
117+
"items": {
118+
"type": "object",
119+
"anyOf": [get_tool_schema(tool) for tool in tools],
120+
},
121+
}
122+
json_schema_defs = get_tool_schema_defs(tools)
123+
if json_schema_defs:
124+
json_schema["$defs"] = json_schema_defs
125+
return json_schema
126+
127+
return None

0 commit comments

Comments
 (0)