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 (