@@ -429,208 +429,6 @@ def test_extract_tool_calls_type_conversion(qwen3_tokenizer):
429429 assert args ["obj_param" ] == {"key" : "value" }
430430
431431
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-
634432@pytest .mark .parametrize (
635433 ids = [
636434 "no_tools" ,
0 commit comments