@@ -444,6 +444,7 @@ std::string common_chat_format_name(common_chat_format format) {
444
444
case COMMON_CHAT_FORMAT_HERMES_2_PRO: return " Hermes 2 Pro" ;
445
445
case COMMON_CHAT_FORMAT_COMMAND_R7B: return " Command R7B" ;
446
446
case COMMON_CHAT_FORMAT_COMMAND_R7B_EXTRACT_REASONING: return " Command R7B (extract reasoning)" ;
447
+ case COMMON_CHAT_FORMAT_PHI_4: return " Phi-4" ;
447
448
default :
448
449
throw std::runtime_error (" Unknown chat format" );
449
450
}
@@ -1344,6 +1345,184 @@ static common_chat_msg common_chat_parse_functionary_v3_1_llama_3_1(const std::s
1344
1345
return parse_json_tool_calls (input, std::nullopt, function_regex, close_regex);
1345
1346
}
1346
1347
1348
+ static common_chat_params common_chat_params_init_phi_4 (const common_chat_template & tmpl, const struct templates_params & inputs) {
1349
+ // Phi-4 has a unique format that expects tools in the system message with <|tool|> tags
1350
+ // and returns function calls as a JSON object after <|tool_call|> tag
1351
+ common_chat_params data;
1352
+
1353
+ data.grammar_lazy = inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED;
1354
+ data.grammar = build_grammar ([&](const common_grammar_builder & builder) {
1355
+ std::vector<std::string> tool_rules;
1356
+ std::vector<std::string> tool_call_alts;
1357
+ foreach_function (inputs.tools , [&](const json & tool) {
1358
+ const auto & function = tool.at (" function" );
1359
+ std::string name = function.at (" name" );
1360
+ auto parameters = function.at (" parameters" );
1361
+ builder.resolve_refs (parameters);
1362
+ tool_rules.push_back (builder.add_schema (name + " -call" , {
1363
+ {" type" , " object" },
1364
+ {" properties" , {
1365
+ {" name" , {{" const" , name}}},
1366
+ {" arguments" , parameters},
1367
+ }},
1368
+ {" required" , json::array ({" name" , " arguments" })},
1369
+ }));
1370
+ });
1371
+ auto any_tool_call = builder.add_rule (" any_tool_call" , " ( " + string_join (tool_rules, " | " ) + " ) space" );
1372
+ std::vector<std::string> alt_tags {
1373
+ any_tool_call,
1374
+ };
1375
+ tool_call_alts.push_back (any_tool_call);
1376
+ auto tool_call = builder.add_rule (" tool_call" , string_join (tool_call_alts, " | " ));
1377
+ builder.add_rule (" root" , inputs.parallel_tool_calls ? " (" + tool_call + " )+" : tool_call);
1378
+ data.grammar_triggers .push_back ({COMMON_GRAMMAR_TRIGGER_TYPE_WORD, " <|tool_call|>" });
1379
+ data.preserved_tokens = {
1380
+ " <|tool_call|>" ,
1381
+ " </|tool_call|>" ,
1382
+ };
1383
+ });
1384
+
1385
+ // For Phi-4, we need to inject tools into the system message
1386
+ // because the template expects tools in the system message with <|tool|> tags
1387
+ if (inputs.tools .empty ()) {
1388
+ // No tools, use normal approach
1389
+ data.prompt = apply (tmpl, inputs.messages , json::array (), inputs.add_generation_prompt );
1390
+ } else {
1391
+ // Make a copy of messages that we can modify
1392
+ json adjusted_messages = inputs.messages ;
1393
+
1394
+ // Extract just the function part of the OpenAI-formatted tools
1395
+ json phi4_tools = json::array ();
1396
+ foreach_function (inputs.tools , [&](const json & tool) {
1397
+ phi4_tools.push_back (tool.at (" function" ));
1398
+ });
1399
+
1400
+ // Phi-4 template expects tools in the system message with <|tool|> tags.
1401
+ // Find the system message, or add one if it doesn't exist
1402
+ bool found_system_msg = false ;
1403
+ for (auto & message : adjusted_messages) {
1404
+ if (message.contains (" role" ) && message[" role" ] == " system" ) {
1405
+ // Add tools to the existing system message and update content to mention tools
1406
+ message[" tools" ] = phi4_tools;
1407
+
1408
+ // If the system message doesn't mention tools, append that information
1409
+ std::string content = message[" content" ];
1410
+ if (content.find (" tool" ) == std::string::npos &&
1411
+ content.find (" function" ) == std::string::npos) {
1412
+ message[" content" ] = content + " You have access to some tools." ;
1413
+ }
1414
+
1415
+ found_system_msg = true ;
1416
+ break ;
1417
+ }
1418
+ }
1419
+
1420
+ // If no system message, add one with tools
1421
+ if (!found_system_msg && !adjusted_messages.empty ()) {
1422
+ json system_msg = {
1423
+ {" role" , " system" },
1424
+ {" content" , " You are a helpful assistant with access to tools.\n To use a tool, respond in this format: <|tool_call|>{\" name\" : \" foo\" , \" arguments\" : {\" a\" : 1}}<|/tool_call|>" },
1425
+ {" tools" , phi4_tools}
1426
+ };
1427
+ // Insert system message at the beginning
1428
+ adjusted_messages.insert (adjusted_messages.begin (), system_msg);
1429
+ }
1430
+
1431
+ // Apply template with tools embedded in system message, passing empty tools separately
1432
+ data.prompt = apply (tmpl, adjusted_messages, json (), inputs.add_generation_prompt );
1433
+ }
1434
+
1435
+ data.format = COMMON_CHAT_FORMAT_PHI_4;
1436
+ return data;
1437
+ }
1438
+
1439
+ static common_chat_msg common_chat_parse_phi_4 (const std::string & input) {
1440
+ common_chat_msg result;
1441
+ result.role = " assistant" ;
1442
+
1443
+ std::string final_content = " " ;
1444
+
1445
+ const std::string opening_tag = " <|tool_call|>" ;
1446
+ const std::string closing_tag = " </|tool_call|>" ;
1447
+
1448
+ size_t start_pos = 0 ;
1449
+ while (true ) {
1450
+ // Find next tool call
1451
+ size_t tool_start = input.find (opening_tag, start_pos);
1452
+ if (tool_start == std::string::npos) {
1453
+ // No more tool calls.
1454
+
1455
+ // Is start_pos within string bounds?
1456
+ if (start_pos < input.length ()) {
1457
+ // Add the rest of the string to final_content
1458
+ final_content += input.substr (start_pos);
1459
+ }
1460
+ break ;
1461
+ }
1462
+
1463
+ // Add content before the tool call to final_content
1464
+ final_content += input.substr (start_pos, tool_start - start_pos);
1465
+
1466
+ // Find closing tag
1467
+ size_t content_start = tool_start + opening_tag.length ();
1468
+ size_t tool_end = input.find (closing_tag, content_start);
1469
+
1470
+ if (tool_end == std::string::npos) {
1471
+ // No closing tag found, so just include the rest of the string as tool.
1472
+ tool_end = input.length ();
1473
+ }
1474
+
1475
+ // Extract tool call content
1476
+ std::string tool_content = input.substr (
1477
+ content_start,
1478
+ tool_end - content_start
1479
+ );
1480
+
1481
+ // Try to parse the tool call
1482
+ try {
1483
+ auto tool_call = json::parse (tool_content);
1484
+
1485
+ // Verify the required fields exist
1486
+ if (!tool_call.contains (" name" )) {
1487
+ throw std::runtime_error (" Missing 'name' field in tool call" );
1488
+ }
1489
+
1490
+ if (!tool_call.contains (" arguments" )) {
1491
+ throw std::runtime_error (" Missing 'arguments' field in tool call" );
1492
+ }
1493
+
1494
+ std::string name = tool_call[" name" ].get <std::string>();
1495
+
1496
+ std::string arguments;
1497
+ try {
1498
+ arguments = tool_call[" arguments" ].dump ();
1499
+ } catch (const std::exception & e) {
1500
+ LOG_ERR (" Failed to serialize arguments: %s\n " , e.what ());
1501
+ arguments = " {}" ;
1502
+ }
1503
+
1504
+ result.tool_calls .push_back ({
1505
+ name,
1506
+ arguments,
1507
+ /* id= */ " " ,
1508
+ });
1509
+ } catch (const std::exception & e) {
1510
+ // If parsing fails, include the entire tool call in the content
1511
+ final_content += input.substr (
1512
+ tool_start,
1513
+ tool_end + closing_tag.length () - tool_start
1514
+ );
1515
+ }
1516
+
1517
+ // Move past this tool call for next iteration
1518
+ start_pos = tool_end + closing_tag.length ();
1519
+ }
1520
+
1521
+ result.content = final_content;
1522
+ return result;
1523
+ }
1524
+
1525
+
1347
1526
static common_chat_params common_chat_params_init_hermes_2_pro (const common_chat_template & tmpl, const struct templates_params & inputs) {
1348
1527
common_chat_params data;
1349
1528
// (content)?(<tool_call>{"name": "foo", "arguments": {"a": 1}}</tool_call>)*
@@ -1622,6 +1801,11 @@ static common_chat_params common_chat_templates_apply_jinja(
1622
1801
return common_chat_params_init_firefunction_v2 (tmpl, params);
1623
1802
}
1624
1803
1804
+ // Phi-4 mini.
1805
+ if (src.find (" <|tool|>" ) != std::string::npos) {
1806
+ return common_chat_params_init_phi_4 (tmpl, params);
1807
+ }
1808
+
1625
1809
// Plain handler (no tools)
1626
1810
if (params.tools .is_null () || inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_NONE) {
1627
1811
return common_chat_params_init_without_tools (tmpl, params);
@@ -1756,6 +1940,8 @@ common_chat_msg common_chat_parse(const std::string & input, common_chat_format
1756
1940
return common_chat_parse_command_r7b (input, /* extract_reasoning= */ false );
1757
1941
case COMMON_CHAT_FORMAT_COMMAND_R7B_EXTRACT_REASONING:
1758
1942
return common_chat_parse_command_r7b (input, /* extract_reasoning= */ true );
1943
+ case COMMON_CHAT_FORMAT_PHI_4:
1944
+ return common_chat_parse_phi_4 (input);
1759
1945
default :
1760
1946
throw std::runtime_error (" Unsupported format: " + common_chat_format_name (format));
1761
1947
}
0 commit comments