1313import regex as re
1414from fastapi import Request
1515from openai_harmony import Message as OpenAIMessage
16- from pydantic import TypeAdapter
1716
1817from vllm .engine .protocol import EngineClient
1918from vllm .entrypoints .chat_utils import (
4746 DeltaMessage ,
4847 DeltaToolCall ,
4948 ErrorResponse ,
50- FunctionCall ,
51- FunctionDefinition ,
5249 PromptTokenUsageInfo ,
5350 RequestResponseMetadata ,
5451 ToolCall ,
@@ -1394,6 +1391,16 @@ async def chat_completion_full_generator(
13941391 auto_tools_called = False
13951392 # if auto tools are not enabled, and a named tool choice using
13961393 # outlines is not being used
1394+ tool_calls , content = self ._parse_tool_calls_from_content (
1395+ request = request ,
1396+ tokenizer = tokenizer ,
1397+ content = content ,
1398+ enable_auto_tools = self .enable_auto_tools ,
1399+ tool_parser_cls = self .tool_parser ,
1400+ )
1401+ tool_call_class = (
1402+ MistralToolCall if isinstance (tokenizer , MistralTokenizer ) else ToolCall
1403+ )
13971404 if (not self .enable_auto_tools or not self .tool_parser ) and (
13981405 not isinstance (request .tool_choice , ChatCompletionNamedToolChoiceParam )
13991406 and request .tool_choice != "required"
@@ -1407,63 +1414,33 @@ async def chat_completion_full_generator(
14071414 request .tool_choice
14081415 and type (request .tool_choice ) is ChatCompletionNamedToolChoiceParam
14091416 ):
1410- tool_call_class = (
1411- MistralToolCall
1412- if isinstance (tokenizer , MistralTokenizer )
1413- else ToolCall
1414- )
1417+ assert tool_calls is not None and len (tool_calls ) > 0
14151418 message = ChatMessage (
14161419 role = role ,
14171420 reasoning_content = reasoning_content ,
14181421 content = "" ,
1419- tool_calls = [
1420- tool_call_class (
1421- function = FunctionCall (
1422- name = request .tool_choice .function .name ,
1423- arguments = content ,
1424- )
1425- )
1426- ],
1422+ tool_calls = [tool_call_class (function = tc ) for tc in tool_calls ],
14271423 )
14281424
14291425 elif request .tool_choice and request .tool_choice == "required" :
1430- tool_call_class = (
1431- MistralToolCall
1432- if isinstance (tokenizer , MistralTokenizer )
1433- else ToolCall
1434- )
1435-
1436- # the fields of FunctionDefinition are a superset of the
1437- # tool call outputs and can be used for parsing
1438- assert content is not None
1439- tool_calls = TypeAdapter (list [FunctionDefinition ]).validate_json (
1440- content
1441- )
1442- tool_call_ids = []
1426+ tool_call_class_items = []
1427+ assert tool_calls is not None and len (tool_calls ) > 0
14431428 for tool_call in tool_calls :
1444- tool_call_ids .append (
1445- make_tool_call_id (
1446- id_type = self .tool_call_id_type ,
1447- func_name = tool_call .name ,
1448- idx = history_tool_call_cnt ,
1429+ tool_call_class_items .append (
1430+ tool_call_class (
1431+ id = make_tool_call_id (
1432+ id_type = self .tool_call_id_type ,
1433+ func_name = tool_call .name ,
1434+ idx = history_tool_call_cnt ,
1435+ ),
1436+ function = tool_call ,
14491437 )
14501438 )
14511439 history_tool_call_cnt += 1
14521440 message = ChatMessage (
14531441 role = role ,
14541442 content = "" ,
1455- tool_calls = [
1456- tool_call_class (
1457- id = tool_call_ids [i ],
1458- function = FunctionCall (
1459- name = tool_call .name ,
1460- arguments = json .dumps (
1461- tool_call .parameters , ensure_ascii = False
1462- ),
1463- ),
1464- )
1465- for i , tool_call in enumerate (tool_calls )
1466- ],
1443+ tool_calls = tool_call_class_items ,
14671444 reasoning_content = reasoning_content ,
14681445 )
14691446
@@ -1481,25 +1458,22 @@ async def chat_completion_full_generator(
14811458 and self .enable_auto_tools
14821459 and self .tool_parser
14831460 ):
1484- try :
1485- tool_parser = self .tool_parser (tokenizer )
1486- except RuntimeError as e :
1487- logger .exception ("Error in tool parser creation." )
1488- return self .create_error_response (str (e ))
1489-
1490- tool_call_info = tool_parser .extract_tool_calls (
1491- content if content is not None else "" , request = request
1492- )
14931461 # In the OpenAI API the finish_reason is "tools_called"
14941462 # if the tool choice is auto and the model produced a tool
14951463 # call. The same is not true for named function calls
1496- auto_tools_called = tool_call_info . tools_called
1497- if tool_call_info . tools_called :
1464+ auto_tools_called = tool_calls is not None and len ( tool_calls ) > 0
1465+ if tool_calls :
14981466 message = ChatMessage (
14991467 role = role ,
15001468 reasoning_content = reasoning_content ,
1501- content = tool_call_info .content ,
1502- tool_calls = tool_call_info .tool_calls ,
1469+ content = content ,
1470+ tool_calls = [
1471+ ToolCall (
1472+ function = tc ,
1473+ type = "function" ,
1474+ )
1475+ for tc in tool_calls
1476+ ],
15031477 )
15041478
15051479 else :
@@ -1509,8 +1483,8 @@ async def chat_completion_full_generator(
15091483
15101484 # try to use content return from tool parser first,
15111485 # tool parser may do some modify for the content.
1512- if tool_call_info . content and len (tool_call_info . content ) > 0 :
1513- ret_content = tool_call_info . content
1486+ if content and len (content ) > 0 :
1487+ ret_content = content
15141488 message = ChatMessage (
15151489 role = role ,
15161490 reasoning_content = reasoning_content ,
0 commit comments