[Frontend] Support strict mode for tool calling with ResponsesAPI (#45396)

Signed-off-by: chaunceyjiang <chaunceyjiang@gmail.com>
This commit is contained in:
Chauncey
2026-06-12 10:59:59 -04:00
committed by GitHub
parent 9ff278b1d2
commit 3b8fc3fe6d
5 changed files with 97 additions and 22 deletions
@@ -390,7 +390,6 @@ def server_with_store(default_server_args):
env_dict={
"VLLM_ENABLE_RESPONSES_API_STORE": "1",
"VLLM_SERVER_DEV_MODE": "1",
"VLLM_ENFORCE_STRICT_TOOL_CALLING": "0",
},
) as remote_server:
yield remote_server
+9 -4
View File
@@ -438,8 +438,7 @@ class DelegatingParser(Parser):
self, request: ChatCompletionRequest | ResponsesRequest
) -> ChatCompletionRequest | ResponsesRequest:
if (
not isinstance(request, ChatCompletionRequest)
or self._tool_parser is None
self._tool_parser is None
or self._tool_parser.structural_tag_model is None
or not request.tools
):
@@ -448,7 +447,10 @@ class DelegatingParser(Parser):
need_tool_calling = (
request.tool_choice == "auto"
or request.tool_choice == "required"
or isinstance(request.tool_choice, ChatCompletionNamedToolChoiceParam)
or isinstance(
request.tool_choice,
(ChatCompletionNamedToolChoiceParam, ToolChoiceFunction),
)
)
if not need_tool_calling:
return request
@@ -464,7 +466,10 @@ class DelegatingParser(Parser):
request.structured_outputs = StructuredOutputsParams(
structural_tag=structural_tag,
)
request.response_format = None
if isinstance(request, ResponsesRequest):
request.text = None
else:
request.response_format = None
return request
def extract_reasoning_streaming(
+1 -2
View File
@@ -181,9 +181,8 @@ class ReasoningParser:
) -> str | None:
"""
Instance method that is implemented for preparing the structured tag
Otherwise, None is returned
"""
return None
return original_tag
class ReasoningParserManager:
+4 -1
View File
@@ -165,7 +165,10 @@ class ToolParser:
return request
def get_structural_tag(
self, request: ChatCompletionRequest, *, reasoning: bool = False
self,
request: ChatCompletionRequest | ResponsesRequest,
*,
reasoning: bool = False,
):
if self.structural_tag_model is None:
return None
+83 -14
View File
@@ -1,9 +1,14 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from collections.abc import Callable
from typing import Any, Literal
from collections.abc import Callable, Sequence
from typing import Any, Literal, TypeAlias
from openai.types.responses import FunctionTool
from openai.types.responses.response import ToolChoice as ResponsesToolChoice
from openai.types.responses.tool import Tool as ResponsesTool
from openai.types.responses.tool_choice_allowed import ToolChoiceAllowed
from openai.types.responses.tool_choice_function import ToolChoiceFunction
from xgrammar import StructuralTag, normalize_tool_choice
from xgrammar import get_model_structural_tag as get_xgrammar_model_structural_tag
from xgrammar.openai_tool_call_schema import (
@@ -25,11 +30,15 @@ from vllm.entrypoints.openai.chat_completion.protocol import (
ChatCompletionToolsParam,
)
ToolChoice = (
Literal["none", "auto", "required"] | ChatCompletionNamedToolChoiceParam | None
ToolChoice: TypeAlias = (
Literal["none", "auto", "required"]
| ChatCompletionNamedToolChoiceParam
| ResponsesToolChoice
| None
)
SimplifiedToolChoice = Literal["auto", "required", "forced"]
StructuralTagBuilder = Callable[
AllowedToolRef: TypeAlias = dict[str, object]
SimplifiedToolChoice: TypeAlias = Literal["auto", "required", "forced"]
StructuralTagBuilder: TypeAlias = Callable[
[
list[FunctionToolParam],
list[BuiltinToolParam],
@@ -77,7 +86,7 @@ def register_vllm_structural_tag(model: str):
def get_model_structural_tag(
model: str,
tools: list[ChatCompletionToolsParam] | None,
tools: Sequence[ChatCompletionToolsParam | ResponsesTool] | None,
tool_choice: ToolChoice,
reasoning: bool,
) -> StructuralTag | None:
@@ -86,8 +95,8 @@ def get_model_structural_tag(
if not tools or tool_choice == "none":
return None
dumped_tools = [_model_dump(tool) for tool in tools]
dumped_tool_choice = _model_dump(tool_choice)
dumped_tools = [_dump_tool_for_xgrammar(tool) for tool in tools]
dumped_tool_choice = _dump_tool_choice_for_xgrammar(tool_choice)
if model in _VLLM_STRUCTURAL_TAG_REGISTRY:
function_tools, builtin_tools, simplified_tool_choice = normalize_tool_choice(
@@ -113,12 +122,72 @@ def get_model_structural_tag(
)
def _model_dump(value: Any) -> Any:
"""Convert vLLM/Pydantic request objects to xgrammar's dict protocol."""
def _dump_tool_for_xgrammar(
tool: ChatCompletionToolsParam | ResponsesTool,
) -> dict[str, Any]:
"""Convert tool objects to xgrammar's Chat Completions tool protocol."""
if hasattr(value, "model_dump"):
return value.model_dump(exclude_none=True)
return value
if isinstance(tool, FunctionTool):
function: dict[str, Any] = {"name": tool.name}
if tool.description is not None:
function["description"] = tool.description
if tool.parameters is not None:
function["parameters"] = tool.parameters
if tool.strict is not None:
function["strict"] = tool.strict
return {"type": "function", "function": function}
dumped_tool = tool.model_dump(mode="json", exclude_none=True)
if isinstance(tool, ChatCompletionToolsParam):
return dumped_tool
return dict(dumped_tool)
def _dump_tool_choice_for_xgrammar(
tool_choice: ToolChoice,
) -> dict[str, Any] | str | None:
"""Convert tool_choice objects to xgrammar's expected protocol."""
if tool_choice is None:
return None
if isinstance(tool_choice, str):
return tool_choice
if isinstance(tool_choice, ChatCompletionNamedToolChoiceParam):
return tool_choice.model_dump(mode="json", exclude_none=True)
if isinstance(tool_choice, ToolChoiceFunction):
return {
"type": "function",
"function": {"name": tool_choice.name},
}
if isinstance(tool_choice, ToolChoiceAllowed):
return {
"type": "allowed_tools",
"allowed_tools": {
"mode": tool_choice.mode,
"tools": [
_dump_allowed_tool_ref_for_xgrammar(tool)
for tool in tool_choice.tools
],
},
}
return tool_choice.model_dump(mode="json", exclude_none=True)
def _dump_allowed_tool_ref_for_xgrammar(tool_ref: AllowedToolRef) -> AllowedToolRef:
if (
tool_ref.get("type") == "function"
and "function" not in tool_ref
and "name" in tool_ref
):
return {
"type": "function",
"function": {"name": tool_ref["name"]},
}
return tool_ref
def _get_function_parameters(function) -> dict[str, Any] | bool: