diff --git a/tests/tool_parsers/test_qwen3coder_tool_parser.py b/tests/tool_parsers/test_qwen3coder_tool_parser.py
index 8f92d5b3745b..7db1b6857152 100644
--- a/tests/tool_parsers/test_qwen3coder_tool_parser.py
+++ b/tests/tool_parsers/test_qwen3coder_tool_parser.py
@@ -429,208 +429,6 @@ def test_extract_tool_calls_type_conversion(qwen3_tokenizer):
assert args["obj_param"] == {"key": "value"}
-def test_extract_tool_calls_anyof_type_conversion(qwen3_tool_parser):
- """Test type conversion for anyOf/oneOf nullable schemas (Pydantic v2).
-
- Pydantic v2 emits anyOf for Optional[T] fields, e.g.:
- Optional[int] -> {"anyOf": [{"type": "integer"}, {"type": "null"}]}
- The parser must extract the non-null type and apply the correct
- conversion (int(), float(), etc.) instead of returning a raw string.
- """
- tools = [
- ChatCompletionToolsParam(
- type="function",
- function={
- "name": "test_anyof",
- "parameters": {
- "type": "object",
- "properties": {
- "anyof_int": {
- "anyOf": [
- {"type": "integer"},
- {"type": "null"},
- ],
- "default": 5,
- },
- "anyof_str": {
- "anyOf": [
- {"type": "string"},
- {"type": "null"},
- ],
- },
- "anyof_array": {
- "anyOf": [
- {"type": "array", "items": {"type": "string"}},
- {"type": "null"},
- ],
- },
- "anyof_obj": {
- "anyOf": [
- {"type": "object"},
- {"type": "null"},
- ],
- },
- "type_as_array": {
- "type": ["integer", "null"],
- },
- "multi_non_null": {
- "anyOf": [
- {"type": "string"},
- {"type": "integer"},
- {"type": "null"},
- ],
- },
- "ref_param": {
- "$ref": "#/$defs/ToolInput",
- },
- },
- },
- },
- )
- ]
-
- model_output = """
-
-
-5
-
-
-hello
-
-
-["a", "b", "c"]
-
-
-{"key": "value"}
-
-
-42
-
-
-some text
-
-
-{"city": "Paris"}
-
-
-"""
-
- request = ChatCompletionRequest(model=MODEL, messages=[], tools=tools)
- extracted = qwen3_tool_parser.extract_tool_calls(model_output, request=request)
-
- args = json.loads(extracted.tool_calls[0].function.arguments)
- assert args["anyof_int"] == 5
- assert isinstance(args["anyof_int"], int)
- assert args["anyof_str"] == "hello"
- assert isinstance(args["anyof_str"], str)
- assert args["anyof_array"] == ["a", "b", "c"]
- assert isinstance(args["anyof_array"], list)
- assert args["anyof_obj"] == {"key": "value"}
- assert isinstance(args["anyof_obj"], dict)
- assert args["type_as_array"] == 42
- assert isinstance(args["type_as_array"], int)
- # Multi non-null: anyOf[string, integer, null] → first non-null is string
- assert args["multi_non_null"] == "some text"
- assert isinstance(args["multi_non_null"], str)
- # $ref: treated as object, parsed via json.loads
- assert args["ref_param"] == {"city": "Paris"}
- assert isinstance(args["ref_param"], dict)
-
-
-def test_extract_tool_calls_anyof_type_conversion_streaming(
- qwen3_tool_parser, qwen3_tokenizer
-):
- """Test streaming e2e for anyOf/oneOf nullable schemas (Pydantic v2).
-
- Verifies that the full streaming pipeline — tokenize, incrementally
- decode, extract_tool_calls_streaming — correctly resolves types from
- anyOf schemas and produces valid JSON with properly typed values.
- """
- tools = [
- ChatCompletionToolsParam(
- type="function",
- function={
- "name": "search_web",
- "parameters": {
- "type": "object",
- "properties": {
- "query": {
- "anyOf": [
- {"type": "string"},
- {"type": "null"},
- ],
- },
- "count": {
- "anyOf": [
- {"type": "integer"},
- {"type": "null"},
- ],
- "default": 5,
- },
- "verbose": {
- "anyOf": [
- {"type": "boolean"},
- {"type": "null"},
- ],
- },
- "filters": {
- "$ref": "#/$defs/SearchFilters",
- },
- },
- },
- },
- )
- ]
-
- model_output = """
-
-
-vllm tool parser
-
-
-10
-
-
-true
-
-
-{"lang": "en", "year": 2025}
-
-
-"""
-
- request = ChatCompletionRequest(model=MODEL, messages=[], tools=tools)
-
- tool_states = {}
- for delta_message in stream_delta_message_generator(
- qwen3_tool_parser, qwen3_tokenizer, model_output, request
- ):
- if delta_message.tool_calls:
- for tool_call in delta_message.tool_calls:
- idx = tool_call.index
- if idx not in tool_states:
- tool_states[idx] = {"name": None, "arguments": ""}
- if tool_call.function:
- if tool_call.function.name:
- tool_states[idx]["name"] = tool_call.function.name
- if tool_call.function.arguments is not None:
- tool_states[idx]["arguments"] += tool_call.function.arguments
-
- assert len(tool_states) == 1
- assert tool_states[0]["name"] == "search_web"
- assert tool_states[0]["arguments"] is not None
- args = json.loads(tool_states[0]["arguments"])
- assert args["query"] == "vllm tool parser"
- assert isinstance(args["query"], str)
- assert args["count"] == 10
- assert isinstance(args["count"], int)
- assert args["verbose"] is True
- assert isinstance(args["verbose"], bool)
- # $ref: treated as object, parsed via json.loads
- assert args["filters"] == {"lang": "en", "year": 2025}
- assert isinstance(args["filters"], dict)
-
-
@pytest.mark.parametrize(
ids=[
"no_tools",
diff --git a/vllm/tool_parsers/qwen3coder_tool_parser.py b/vllm/tool_parsers/qwen3coder_tool_parser.py
index 8a5b4f5a2726..ea25ea2be923 100644
--- a/vllm/tool_parsers/qwen3coder_tool_parser.py
+++ b/vllm/tool_parsers/qwen3coder_tool_parser.py
@@ -131,58 +131,11 @@ def _get_arguments_config(self, func_name: str, tools: list[Tool] | None) -> dic
logger.debug("Tool '%s' is not defined in the tools list.", func_name)
return {}
- @staticmethod
- def _first_non_null_type(type_value: Any) -> str | None:
- """Extract the first non-null type from a type value.
-
- Handles both scalar types ("integer") and type-as-array
- (["integer", "null"]) per JSON Schema spec.
- """
- if isinstance(type_value, list):
- return next(
- (
- str(t).strip().lower()
- for t in type_value
- if t is not None and str(t).lower() != "null"
- ),
- None,
- )
- if type_value is not None and str(type_value).lower() != "null":
- return str(type_value).strip().lower()
- return None
-
- def _resolve_param_type(self, param_def: dict) -> str:
- """Resolve the effective type string from a parameter definition.
-
- Handles direct "type" fields (including type-as-array),
- anyOf/oneOf schemas emitted by Pydantic v2 for Optional[T],
- and $ref schemas from Pydantic model inputs.
- """
- if "type" in param_def:
- resolved = self._first_non_null_type(param_def["type"])
- return resolved or "string"
-
- if "anyOf" in param_def or "oneOf" in param_def:
- variants = param_def.get("anyOf") or param_def.get("oneOf", [])
- for v in variants:
- if not isinstance(v, dict):
- continue
- resolved = self._first_non_null_type(v.get("type"))
- if resolved:
- return resolved
-
- # $ref points to a schema definition (e.g. a Pydantic model).
- # The referenced type is almost always an object, so treat it
- # as such to route through json.loads.
- if "$ref" in param_def:
- return "object"
-
- return "string"
-
def _convert_param_value(
self, param_value: str, param_name: str, param_config: dict, func_name: str
) -> Any:
"""Convert parameter value based on its type in the schema."""
+ # Handle null value for any type
if param_value.lower() == "null":
return None
@@ -197,10 +150,19 @@ def _convert_param_value(
)
return param_value
- if not isinstance(param_config[param_name], dict):
- return param_value
-
- param_type = self._resolve_param_type(param_config[param_name])
+ if (
+ isinstance(param_config[param_name], dict)
+ and "type" in param_config[param_name]
+ ):
+ param_type = str(param_config[param_name]["type"]).strip().lower()
+ elif (
+ isinstance(param_config[param_name], dict)
+ and "anyOf" in param_config[param_name]
+ ):
+ # anyOf has no top-level "type"; treat as object to trigger json.loads.
+ param_type = "object"
+ else:
+ param_type = "string"
if param_type in ["string", "str", "text", "varchar", "char", "enum"]:
return param_value
elif (