@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 1785dc5501
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 5, 16:00 UTC

Revert "[Bugfix] Fix Qwen3CoderToolParser anyOf/oneOf type resolution for nullable params (#37831)" (#38751)

Kevin H. Luu committed Apr 1, 2026 at 15:34 UTC 1785dc55014cfd39c488158a0542cd987fed53c8
2 files changed +14 -254
tests/tool_parsers/test_qwen3coder_tool_parser.py
-202
@@ -429,208 +429,6 @@ hello world
429 assert args["obj_param"] == {"key": "value"}
430
431
432 -def test_extract_tool_calls_anyof_type_conversion(qwen3_tool_parser):
433 - """Test type conversion for anyOf/oneOf nullable schemas (Pydantic v2).
434 -
435 - Pydantic v2 emits anyOf for Optional[T] fields, e.g.:
436 - Optional[int] -> {"anyOf": [{"type": "integer"}, {"type": "null"}]}
437 - The parser must extract the non-null type and apply the correct
438 - conversion (int(), float(), etc.) instead of returning a raw string.
439 - """
440 - tools = [
441 - ChatCompletionToolsParam(
442 - type="function",
443 - function={
444 - "name": "test_anyof",
445 - "parameters": {
446 - "type": "object",
447 - "properties": {
448 - "anyof_int": {
449 - "anyOf": [
450 - {"type": "integer"},
451 - {"type": "null"},
452 - ],
453 - "default": 5,
454 - },
455 - "anyof_str": {
456 - "anyOf": [
457 - {"type": "string"},
458 - {"type": "null"},
459 - ],
460 - },
461 - "anyof_array": {
462 - "anyOf": [
463 - {"type": "array", "items": {"type": "string"}},
464 - {"type": "null"},
465 - ],
466 - },
467 - "anyof_obj": {
468 - "anyOf": [
469 - {"type": "object"},
470 - {"type": "null"},
471 - ],
472 - },
473 - "type_as_array": {
474 - "type": ["integer", "null"],
475 - },
476 - "multi_non_null": {
477 - "anyOf": [
478 - {"type": "string"},
479 - {"type": "integer"},
480 - {"type": "null"},
481 - ],
482 - },
483 - "ref_param": {
484 - "$ref": "#/$defs/ToolInput",
485 - },
486 - },
487 - },
488 - },
489 - )
490 - ]
491 -
492 - model_output = """<tool_call>
493 -<function=test_anyof>
494 -<parameter=anyof_int>
495 -5
496 -</parameter>
497 -<parameter=anyof_str>
498 -hello
499 -</parameter>
500 -<parameter=anyof_array>
501 -["a", "b", "c"]
502 -</parameter>
503 -<parameter=anyof_obj>
504 -{"key": "value"}
505 -</parameter>
506 -<parameter=type_as_array>
507 -42
508 -</parameter>
509 -<parameter=multi_non_null>
510 -some text
511 -</parameter>
512 -<parameter=ref_param>
513 -{"city": "Paris"}
514 -</parameter>
515 -</function>
516 -</tool_call>"""
517 -
518 - request = ChatCompletionRequest(model=MODEL, messages=[], tools=tools)
519 - extracted = qwen3_tool_parser.extract_tool_calls(model_output, request=request)
520 -
521 - args = json.loads(extracted.tool_calls[0].function.arguments)
522 - assert args["anyof_int"] == 5
523 - assert isinstance(args["anyof_int"], int)
524 - assert args["anyof_str"] == "hello"
525 - assert isinstance(args["anyof_str"], str)
526 - assert args["anyof_array"] == ["a", "b", "c"]
527 - assert isinstance(args["anyof_array"], list)
528 - assert args["anyof_obj"] == {"key": "value"}
529 - assert isinstance(args["anyof_obj"], dict)
530 - assert args["type_as_array"] == 42
531 - assert isinstance(args["type_as_array"], int)
532 - # Multi non-null: anyOf[string, integer, null] → first non-null is string
533 - assert args["multi_non_null"] == "some text"
534 - assert isinstance(args["multi_non_null"], str)
535 - # $ref: treated as object, parsed via json.loads
536 - assert args["ref_param"] == {"city": "Paris"}
537 - assert isinstance(args["ref_param"], dict)
538 -
539 -
540 -def test_extract_tool_calls_anyof_type_conversion_streaming(
541 - qwen3_tool_parser, qwen3_tokenizer
542 -):
543 - """Test streaming e2e for anyOf/oneOf nullable schemas (Pydantic v2).
544 -
545 - Verifies that the full streaming pipeline — tokenize, incrementally
546 - decode, extract_tool_calls_streaming — correctly resolves types from
547 - anyOf schemas and produces valid JSON with properly typed values.
548 - """
549 - tools = [
550 - ChatCompletionToolsParam(
551 - type="function",
552 - function={
553 - "name": "search_web",
554 - "parameters": {
555 - "type": "object",
556 - "properties": {
557 - "query": {
558 - "anyOf": [
559 - {"type": "string"},
560 - {"type": "null"},
561 - ],
562 - },
563 - "count": {
564 - "anyOf": [
565 - {"type": "integer"},
566 - {"type": "null"},
567 - ],
568 - "default": 5,
569 - },
570 - "verbose": {
571 - "anyOf": [
572 - {"type": "boolean"},
573 - {"type": "null"},
574 - ],
575 - },
576 - "filters": {
577 - "$ref": "#/$defs/SearchFilters",
578 - },
579 - },
580 - },
581 - },
582 - )
583 - ]
584 -
585 - model_output = """<tool_call>
586 -<function=search_web>
587 -<parameter=query>
588 -vllm tool parser
589 -</parameter>
590 -<parameter=count>
591 -10
592 -</parameter>
593 -<parameter=verbose>
594 -true
595 -</parameter>
596 -<parameter=filters>
597 -{"lang": "en", "year": 2025}
598 -</parameter>
599 -</function>
600 -</tool_call>"""
601 -
602 - request = ChatCompletionRequest(model=MODEL, messages=[], tools=tools)
603 -
604 - tool_states = {}
605 - for delta_message in stream_delta_message_generator(
606 - qwen3_tool_parser, qwen3_tokenizer, model_output, request
607 - ):
608 - if delta_message.tool_calls:
609 - for tool_call in delta_message.tool_calls:
610 - idx = tool_call.index
611 - if idx not in tool_states:
612 - tool_states[idx] = {"name": None, "arguments": ""}
613 - if tool_call.function:
614 - if tool_call.function.name:
615 - tool_states[idx]["name"] = tool_call.function.name
616 - if tool_call.function.arguments is not None:
617 - tool_states[idx]["arguments"] += tool_call.function.arguments
618 -
619 - assert len(tool_states) == 1
620 - assert tool_states[0]["name"] == "search_web"
621 - assert tool_states[0]["arguments"] is not None
622 - args = json.loads(tool_states[0]["arguments"])
623 - assert args["query"] == "vllm tool parser"
624 - assert isinstance(args["query"], str)
625 - assert args["count"] == 10
626 - assert isinstance(args["count"], int)
627 - assert args["verbose"] is True
628 - assert isinstance(args["verbose"], bool)
629 - # $ref: treated as object, parsed via json.loads
630 - assert args["filters"] == {"lang": "en", "year": 2025}
631 - assert isinstance(args["filters"], dict)
632 -
633 -
432 @pytest.mark.parametrize(
433 ids=[
434 "no_tools",
vllm/tool_parsers/qwen3coder_tool_parser.py
+14 -52
@@ -131,58 +131,11 @@ class Qwen3CoderToolParser(ToolParser):
131 logger.debug("Tool '%s' is not defined in the tools list.", func_name)
132 return {}
133
134 - @staticmethod
135 - def _first_non_null_type(type_value: Any) -> str | None:
136 - """Extract the first non-null type from a type value.
137 -
138 - Handles both scalar types ("integer") and type-as-array
139 - (["integer", "null"]) per JSON Schema spec.
140 - """
141 - if isinstance(type_value, list):
142 - return next(
143 - (
144 - str(t).strip().lower()
145 - for t in type_value
146 - if t is not None and str(t).lower() != "null"
147 - ),
148 - None,
149 - )
150 - if type_value is not None and str(type_value).lower() != "null":
151 - return str(type_value).strip().lower()
152 - return None
153 -
154 - def _resolve_param_type(self, param_def: dict) -> str:
155 - """Resolve the effective type string from a parameter definition.
156 -
157 - Handles direct "type" fields (including type-as-array),
158 - anyOf/oneOf schemas emitted by Pydantic v2 for Optional[T],
159 - and $ref schemas from Pydantic model inputs.
160 - """
161 - if "type" in param_def:
162 - resolved = self._first_non_null_type(param_def["type"])
163 - return resolved or "string"
164 -
165 - if "anyOf" in param_def or "oneOf" in param_def:
166 - variants = param_def.get("anyOf") or param_def.get("oneOf", [])
167 - for v in variants:
168 - if not isinstance(v, dict):
169 - continue
170 - resolved = self._first_non_null_type(v.get("type"))
171 - if resolved:
172 - return resolved
173 -
174 - # $ref points to a schema definition (e.g. a Pydantic model).
175 - # The referenced type is almost always an object, so treat it
176 - # as such to route through json.loads.
177 - if "$ref" in param_def:
178 - return "object"
179 -
180 - return "string"
181 -
134 def _convert_param_value(
135 self, param_value: str, param_name: str, param_config: dict, func_name: str
136 ) -> Any:
137 """Convert parameter value based on its type in the schema."""
138 + # Handle null value for any type
139 if param_value.lower() == "null":
140 return None
141
@@ -197,10 +150,19 @@ class Qwen3CoderToolParser(ToolParser):
150 )
151 return param_value
152
200 - if not isinstance(param_config[param_name], dict):
201 - return param_value
202 -
203 - param_type = self._resolve_param_type(param_config[param_name])
153 + if (
154 + isinstance(param_config[param_name], dict)
155 + and "type" in param_config[param_name]
156 + ):
157 + param_type = str(param_config[param_name]["type"]).strip().lower()
158 + elif (
159 + isinstance(param_config[param_name], dict)
160 + and "anyOf" in param_config[param_name]
161 + ):
162 + # anyOf has no top-level "type"; treat as object to trigger json.loads.
163 + param_type = "object"
164 + else:
165 + param_type = "string"
166 if param_type in ["string", "str", "text", "varchar", "char", "enum"]:
167 return param_value
168 elif (