Skip to content

Commit 3b8fc3f

Browse files
[Frontend] Support strict mode for tool calling with ResponsesAPI (#45396)
Signed-off-by: chaunceyjiang <chaunceyjiang@gmail.com>
1 parent 9ff278b commit 3b8fc3f

5 files changed

Lines changed: 98 additions & 23 deletions

File tree

tests/entrypoints/openai/responses/conftest.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,6 @@ def server_with_store(default_server_args):
390390
env_dict={
391391
"VLLM_ENABLE_RESPONSES_API_STORE": "1",
392392
"VLLM_SERVER_DEV_MODE": "1",
393-
"VLLM_ENFORCE_STRICT_TOOL_CALLING": "0",
394393
},
395394
) as remote_server:
396395
yield remote_server

vllm/parser/abstract_parser.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -438,8 +438,7 @@ def _apply_structural_tag(
438438
self, request: ChatCompletionRequest | ResponsesRequest
439439
) -> ChatCompletionRequest | ResponsesRequest:
440440
if (
441-
not isinstance(request, ChatCompletionRequest)
442-
or self._tool_parser is None
441+
self._tool_parser is None
443442
or self._tool_parser.structural_tag_model is None
444443
or not request.tools
445444
):
@@ -448,7 +447,10 @@ def _apply_structural_tag(
448447
need_tool_calling = (
449448
request.tool_choice == "auto"
450449
or request.tool_choice == "required"
451-
or isinstance(request.tool_choice, ChatCompletionNamedToolChoiceParam)
450+
or isinstance(
451+
request.tool_choice,
452+
(ChatCompletionNamedToolChoiceParam, ToolChoiceFunction),
453+
)
452454
)
453455
if not need_tool_calling:
454456
return request
@@ -464,7 +466,10 @@ def _apply_structural_tag(
464466
request.structured_outputs = StructuredOutputsParams(
465467
structural_tag=structural_tag,
466468
)
467-
request.response_format = None
469+
if isinstance(request, ResponsesRequest):
470+
request.text = None
471+
else:
472+
request.response_format = None
468473
return request
469474

470475
def extract_reasoning_streaming(

vllm/reasoning/abs_reasoning_parsers.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,8 @@ def prepare_structured_tag(
181181
) -> str | None:
182182
"""
183183
Instance method that is implemented for preparing the structured tag
184-
Otherwise, None is returned
185184
"""
186-
return None
185+
return original_tag
187186

188187

189188
class ReasoningParserManager:

vllm/tool_parsers/abstract_tool_parser.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,10 @@ def adjust_request(
165165
return request
166166

167167
def get_structural_tag(
168-
self, request: ChatCompletionRequest, *, reasoning: bool = False
168+
self,
169+
request: ChatCompletionRequest | ResponsesRequest,
170+
*,
171+
reasoning: bool = False,
169172
):
170173
if self.structural_tag_model is None:
171174
return None

vllm/tool_parsers/structural_tag_registry.py

Lines changed: 84 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
# SPDX-License-Identifier: Apache-2.0
22
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
33

4-
from collections.abc import Callable
5-
from typing import Any, Literal
6-
4+
from collections.abc import Callable, Sequence
5+
from typing import Any, Literal, TypeAlias
6+
7+
from openai.types.responses import FunctionTool
8+
from openai.types.responses.response import ToolChoice as ResponsesToolChoice
9+
from openai.types.responses.tool import Tool as ResponsesTool
10+
from openai.types.responses.tool_choice_allowed import ToolChoiceAllowed
11+
from openai.types.responses.tool_choice_function import ToolChoiceFunction
712
from xgrammar import StructuralTag, normalize_tool_choice
813
from xgrammar import get_model_structural_tag as get_xgrammar_model_structural_tag
914
from xgrammar.openai_tool_call_schema import (
@@ -25,11 +30,15 @@
2530
ChatCompletionToolsParam,
2631
)
2732

28-
ToolChoice = (
29-
Literal["none", "auto", "required"] | ChatCompletionNamedToolChoiceParam | None
33+
ToolChoice: TypeAlias = (
34+
Literal["none", "auto", "required"]
35+
| ChatCompletionNamedToolChoiceParam
36+
| ResponsesToolChoice
37+
| None
3038
)
31-
SimplifiedToolChoice = Literal["auto", "required", "forced"]
32-
StructuralTagBuilder = Callable[
39+
AllowedToolRef: TypeAlias = dict[str, object]
40+
SimplifiedToolChoice: TypeAlias = Literal["auto", "required", "forced"]
41+
StructuralTagBuilder: TypeAlias = Callable[
3342
[
3443
list[FunctionToolParam],
3544
list[BuiltinToolParam],
@@ -77,7 +86,7 @@ def decorator(func: StructuralTagBuilder) -> StructuralTagBuilder:
7786

7887
def get_model_structural_tag(
7988
model: str,
80-
tools: list[ChatCompletionToolsParam] | None,
89+
tools: Sequence[ChatCompletionToolsParam | ResponsesTool] | None,
8190
tool_choice: ToolChoice,
8291
reasoning: bool,
8392
) -> StructuralTag | None:
@@ -86,8 +95,8 @@ def get_model_structural_tag(
8695
if not tools or tool_choice == "none":
8796
return None
8897

89-
dumped_tools = [_model_dump(tool) for tool in tools]
90-
dumped_tool_choice = _model_dump(tool_choice)
98+
dumped_tools = [_dump_tool_for_xgrammar(tool) for tool in tools]
99+
dumped_tool_choice = _dump_tool_choice_for_xgrammar(tool_choice)
91100

92101
if model in _VLLM_STRUCTURAL_TAG_REGISTRY:
93102
function_tools, builtin_tools, simplified_tool_choice = normalize_tool_choice(
@@ -113,12 +122,72 @@ def get_model_structural_tag(
113122
)
114123

115124

116-
def _model_dump(value: Any) -> Any:
117-
"""Convert vLLM/Pydantic request objects to xgrammar's dict protocol."""
125+
def _dump_tool_for_xgrammar(
126+
tool: ChatCompletionToolsParam | ResponsesTool,
127+
) -> dict[str, Any]:
128+
"""Convert tool objects to xgrammar's Chat Completions tool protocol."""
129+
130+
if isinstance(tool, FunctionTool):
131+
function: dict[str, Any] = {"name": tool.name}
132+
if tool.description is not None:
133+
function["description"] = tool.description
134+
if tool.parameters is not None:
135+
function["parameters"] = tool.parameters
136+
if tool.strict is not None:
137+
function["strict"] = tool.strict
138+
return {"type": "function", "function": function}
139+
dumped_tool = tool.model_dump(mode="json", exclude_none=True)
140+
if isinstance(tool, ChatCompletionToolsParam):
141+
return dumped_tool
142+
return dict(dumped_tool)
143+
144+
145+
def _dump_tool_choice_for_xgrammar(
146+
tool_choice: ToolChoice,
147+
) -> dict[str, Any] | str | None:
148+
"""Convert tool_choice objects to xgrammar's expected protocol."""
149+
150+
if tool_choice is None:
151+
return None
118152

119-
if hasattr(value, "model_dump"):
120-
return value.model_dump(exclude_none=True)
121-
return value
153+
if isinstance(tool_choice, str):
154+
return tool_choice
155+
156+
if isinstance(tool_choice, ChatCompletionNamedToolChoiceParam):
157+
return tool_choice.model_dump(mode="json", exclude_none=True)
158+
159+
if isinstance(tool_choice, ToolChoiceFunction):
160+
return {
161+
"type": "function",
162+
"function": {"name": tool_choice.name},
163+
}
164+
165+
if isinstance(tool_choice, ToolChoiceAllowed):
166+
return {
167+
"type": "allowed_tools",
168+
"allowed_tools": {
169+
"mode": tool_choice.mode,
170+
"tools": [
171+
_dump_allowed_tool_ref_for_xgrammar(tool)
172+
for tool in tool_choice.tools
173+
],
174+
},
175+
}
176+
177+
return tool_choice.model_dump(mode="json", exclude_none=True)
178+
179+
180+
def _dump_allowed_tool_ref_for_xgrammar(tool_ref: AllowedToolRef) -> AllowedToolRef:
181+
if (
182+
tool_ref.get("type") == "function"
183+
and "function" not in tool_ref
184+
and "name" in tool_ref
185+
):
186+
return {
187+
"type": "function",
188+
"function": {"name": tool_ref["name"]},
189+
}
190+
return tool_ref
122191

123192

124193
def _get_function_parameters(function) -> dict[str, Any] | bool:

0 commit comments

Comments
 (0)