[Bugfix] Avoid JSON constraints for native tool parsers (#47512)

Signed-off-by: Flora Feng <4florafeng@gmail.com>
Co-authored-by: Flora Feng <4florafeng@gmail.com>
This commit is contained in:
hubunt
2026-09-29 19:37:12 +00:00
committed by GitHub
co-authored by Flora Feng
parent 7aa8372ef7
commit 19a4d66a0b
2 changed files with 104 additions and 0 deletions
@@ -6,15 +6,18 @@ from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
from openai.types.responses import ToolChoiceFunction
from xgrammar import Grammar, StructuralTag
from xgrammar.testing import _is_grammar_accept_string
import vllm.envs as envs
from vllm.entrypoints.openai.chat_completion.protocol import (
ChatCompletionNamedFunction,
ChatCompletionNamedToolChoiceParam,
ChatCompletionRequest,
ChatCompletionToolsParam,
)
from vllm.entrypoints.openai.responses.protocol import ResponsesRequest
from vllm.parser.abstract_parser import DelegatingParser
from vllm.parser.plamo3 import (
BEGIN_TOOL_ARGUMENTS,
@@ -700,6 +703,100 @@ def test_non_structural_tag_parser_uses_schema_constraints(
assert out.structured_outputs.structural_tag is None
_NAMED_CHAT_TOOL_CHOICE = {"type": "function", "function": {"name": "get_weather"}}
def _responses_request(tool_choice) -> ResponsesRequest:
return ResponsesRequest.model_validate(
{
"input": "hi",
"tools": [
{
"type": "function",
"name": "get_weather",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
},
}
],
"tool_choice": tool_choice,
}
)
@pytest.mark.parametrize("tool_choice", ["required", _NAMED_CHAT_TOOL_CHOICE])
def test_native_format_parser_skips_json_schema_constraint(
sample_tools: list[ChatCompletionToolsParam],
tool_choice,
):
# Parsers that extract required/named calls from their native format must
# not be forced into JSON, which their extractor cannot parse.
class NativeFormatToolParser(ToolParser):
supports_required_and_named = False
parser = NativeFormatToolParser(MagicMock())
request = ChatCompletionRequest(
messages=[],
model="m",
# Named tool choice validation reads tools in their JSON form.
tools=[tool.model_dump(exclude_none=True) for tool in sample_tools],
tool_choice=tool_choice,
)
out = parser.adjust_request(request)
assert out.structured_outputs is None
@pytest.mark.parametrize(
"tool_choice",
["required", ToolChoiceFunction(type="function", name="get_weather")],
)
def test_native_format_parser_skips_json_schema_constraint_responses(
tool_choice,
):
class NativeFormatToolParser(ToolParser):
supports_required_and_named = False
parser = NativeFormatToolParser(MagicMock())
request = _responses_request(tool_choice)
out = parser.adjust_request(request)
assert out.structured_outputs is None
assert out.text is None
@pytest.mark.parametrize("tool_choice", ["required", _NAMED_CHAT_TOOL_CHOICE])
def test_glm47_without_strict_tool_calling_skips_json_schema_constraint(
monkeypatch: pytest.MonkeyPatch,
sample_tools: list[ChatCompletionToolsParam],
tool_choice,
):
# With strict tool calling off, no structural tag is attached, so GLM-4.7
# must decode its native XML instead of a JSON tool-call list.
monkeypatch.setattr(envs, "VLLM_ENFORCE_STRICT_TOOL_CALLING", False)
class TestParser(DelegatingParser):
tool_parser_cls = Glm47MoeModelToolParser
request = ChatCompletionRequest(
messages=[],
model="m",
# Named tool choice validation reads tools in their JSON form.
tools=[tool.model_dump(exclude_none=True) for tool in sample_tools],
tool_choice=tool_choice,
)
parser = TestParser(MagicMock(), tools=sample_tools)
parser._reasoning_parser = MagicMock(adjust_request=lambda request: request)
out = parser.adjust_request(request)
assert out.structured_outputs is None
assert out.skip_special_tokens is False
def test_get_structural_tag_disables_reasoning(
monkeypatch: pytest.MonkeyPatch,
sample_tools_strict: list[ChatCompletionToolsParam],
@@ -133,6 +133,13 @@ class ToolParser:
):
return request
# Parsers that extract required/named tool calls from their native
# format (supports_required_and_named=False) must not be forced into
# the JSON tool-call format below: they would never find a call in
# the JSON output and would return it as content instead.
if not self.supports_required_and_named:
return request
json_schema_from_tool = get_json_schema_from_tools(
tool_choice=request.tool_choice, tools=request.tools
)