File size: 62,715 Bytes
0f07ba7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
package functions

import (
	"encoding/json"
	"errors"
	"io"
	"regexp"
	"slices"
	"strings"
	"unicode/utf8"

	"github.com/mudler/LocalAI/pkg/functions/grammars"
	"github.com/mudler/LocalAI/pkg/utils"
	"github.com/mudler/xlog"
)

// @Description GrammarConfig contains configuration for grammar parsing
type GrammarConfig struct {
	// ParallelCalls enables the LLM to return multiple function calls in the same response
	ParallelCalls bool `yaml:"parallel_calls,omitempty" json:"parallel_calls,omitempty"`

	DisableParallelNewLines bool `yaml:"disable_parallel_new_lines,omitempty" json:"disable_parallel_new_lines,omitempty"`

	// MixedMode enables the LLM to return strings and not only JSON objects
	// This is useful for models to not constraining returning only JSON and also messages back to the user
	MixedMode bool `yaml:"mixed_mode,omitempty" json:"mixed_mode,omitempty"`

	// NoMixedFreeString disables the mixed mode for free strings
	// In this way if the LLM selects a free string, it won't be mixed necessarily with JSON objects.
	// For example, if enabled the LLM or returns a JSON object or a free string, but not a mix of both
	// If disabled(default): the LLM can return a JSON object surrounded by free strings (e.g. `this is the JSON result: { "bar": "baz" } for your question`). This forces the LLM to return at least a JSON object, but its not going to be strict
	NoMixedFreeString bool `yaml:"no_mixed_free_string,omitempty" json:"no_mixed_free_string,omitempty"`

	// NoGrammar disables the grammar parsing and parses the responses directly from the LLM
	NoGrammar bool `yaml:"disable,omitempty" json:"disable,omitempty"`

	// Prefix is the suffix to append to the grammar when being generated
	// This is useful when models prepend a tag before returning JSON
	Prefix string `yaml:"prefix,omitempty" json:"prefix,omitempty"`

	// ExpectStringsAfterJSON enables mixed string suffix
	ExpectStringsAfterJSON bool `yaml:"expect_strings_after_json,omitempty" json:"expect_strings_after_json,omitempty"`

	// PropOrder selects what order to print properties
	// for instance name,arguments will make print { "name": "foo", "arguments": { "bar": "baz" } }
	// instead of { "arguments": { "bar": "baz" }, "name": "foo" }
	PropOrder string `yaml:"properties_order,omitempty" json:"properties_order,omitempty"`

	// SchemaType can be configured to use a specific schema type to force the grammar
	// available : json, llama3.1
	SchemaType string `yaml:"schema_type,omitempty" json:"schema_type,omitempty"`

	GrammarTriggers []GrammarTrigger `yaml:"triggers,omitempty" json:"triggers,omitempty"`
}

// @Description GrammarTrigger defines a trigger word for grammar parsing
type GrammarTrigger struct {
	// Trigger is the string that triggers the grammar
	Word string `yaml:"word,omitempty" json:"word,omitempty"`
}

// @Description FunctionsConfig is the configuration for the tool/function call.
// It includes setting to map the function name and arguments from the response
// and, for instance, also if processing the requests with BNF grammars.
type FunctionsConfig struct {
	// DisableNoAction disables the "no action" tool
	// By default we inject a tool that does nothing and is used to return an answer from the LLM
	DisableNoAction bool `yaml:"disable_no_action,omitempty" json:"disable_no_action,omitempty"`

	// Grammar is the configuration for the grammar
	GrammarConfig GrammarConfig `yaml:"grammar,omitempty" json:"grammar,omitempty"`

	// NoActionFunctionName is the name of the function that does nothing. It defaults to "answer"
	NoActionFunctionName string `yaml:"no_action_function_name,omitempty" json:"no_action_function_name,omitempty"`

	// NoActionDescriptionName is the name of the function that returns the description of the no action function
	NoActionDescriptionName string `yaml:"no_action_description_name,omitempty" json:"no_action_description_name,omitempty"`

	// ResponseRegex is a named regex to extract the function name and arguments from the response
	ResponseRegex []string `yaml:"response_regex,omitempty" json:"response_regex,omitempty"`

	// JSONRegexMatch is a regex to extract the JSON object from the response
	JSONRegexMatch []string `yaml:"json_regex_match,omitempty" json:"json_regex_match,omitempty"`

	// ArgumentRegex is a named regex to extract the arguments from the response. Use ArgumentRegexKey and ArgumentRegexValue to set the names of the named regex for key and value of the arguments.
	ArgumentRegex []string `yaml:"argument_regex,omitempty" json:"argument_regex,omitempty"`
	// ArgumentRegex named regex names for key and value extractions. default: key and value
	ArgumentRegexKey   string `yaml:"argument_regex_key_name,omitempty" json:"argument_regex_key_name,omitempty"`     // default: key
	ArgumentRegexValue string `yaml:"argument_regex_value_name,omitempty" json:"argument_regex_value_name,omitempty"` // default: value

	// ReplaceFunctionResults allow to replace strings in the results before parsing them
	ReplaceFunctionResults []ReplaceResult `yaml:"replace_function_results,omitempty" json:"replace_function_results,omitempty"`

	// ReplaceLLMResult allow to replace strings in the results before parsing them
	ReplaceLLMResult []ReplaceResult `yaml:"replace_llm_results,omitempty" json:"replace_llm_results,omitempty"`

	// CaptureLLMResult is a regex to extract a string from the LLM response
	// that is used as return string when using tools.
	// This is useful for e.g. if the LLM outputs a reasoning and we want to get the reasoning as a string back
	CaptureLLMResult []string `yaml:"capture_llm_results,omitempty" json:"capture_llm_results,omitempty"`

	// FunctionName enable the LLM to return { "name": "function_name", "arguments": { "arg1": "value1", "arg2": "value2" } }
	// instead of { "function": "function_name", "arguments": { "arg1": "value1", "arg2": "value2" } }.
	// This might be useful for certain models trained with the function name as the first token.
	FunctionNameKey      string `yaml:"function_name_key,omitempty" json:"function_name_key,omitempty"`
	FunctionArgumentsKey string `yaml:"function_arguments_key,omitempty" json:"function_arguments_key,omitempty"`

	// XMLFormatPreset is an optional preset format name to force (e.g., "qwen3-coder", "glm-4.5", "minimax-m2")
	// If empty, auto-detection will try all formats
	XMLFormatPreset string `yaml:"xml_format_preset,omitempty" json:"xml_format_preset,omitempty"`
	// XMLFormat is an optional custom XML format configuration
	// If set, only this format will be tried (overrides XMLFormatPreset)
	XMLFormat *XMLToolCallFormat `yaml:"xml_format,omitempty" json:"xml_format,omitempty"`
}

// @Description ReplaceResult defines a key-value replacement for function results
type ReplaceResult struct {
	Key   string `yaml:"key,omitempty" json:"key,omitempty"`
	Value string `yaml:"value,omitempty" json:"value,omitempty"`
}

// @Description XMLToolCallFormat defines the structure for parsing XML-style tool calls
// This mirrors llama.cpp's xml_tool_call_format structure
type XMLToolCallFormat struct {
	// ScopeStart is the optional wrapper start tag (e.g., "<minimax:tool_call>")
	ScopeStart string `yaml:"scope_start,omitempty" json:"scope_start,omitempty"`
	// ToolStart is the tool call start tag (e.g., "<tool_call>", "<invoke name=\"")
	ToolStart string `yaml:"tool_start,omitempty" json:"tool_start,omitempty"`
	// ToolSep is the separator after tool name (e.g., ">", "\">")
	ToolSep string `yaml:"tool_sep,omitempty" json:"tool_sep,omitempty"`
	// KeyStart is the parameter key start tag (e.g., "<parameter=", "<arg_key>")
	KeyStart string `yaml:"key_start,omitempty" json:"key_start,omitempty"`
	// KeyValSep is the separator between key and value (e.g., ">", "</arg_key>")
	KeyValSep string `yaml:"key_val_sep,omitempty" json:"key_val_sep,omitempty"`
	// ValEnd is the parameter value end tag (e.g., "</parameter>", "</arg_value>")
	ValEnd string `yaml:"val_end,omitempty" json:"val_end,omitempty"`
	// ToolEnd is the tool call end tag (e.g., "</tool_call>", "</invoke>")
	ToolEnd string `yaml:"tool_end,omitempty" json:"tool_end,omitempty"`
	// ScopeEnd is the optional wrapper end tag (e.g., "</minimax:tool_call>")
	ScopeEnd string `yaml:"scope_end,omitempty" json:"scope_end,omitempty"`
	// KeyValSep2 is the optional second separator (for GLM 4.5 format: "</arg_key>\n<arg_value>")
	KeyValSep2 *string `yaml:"key_val_sep2,omitempty" json:"key_val_sep2,omitempty"`
	// RawArgVal indicates whether to treat values as raw strings (true) vs JSON (false), nil means both allowed
	RawArgVal *bool `yaml:"raw_argval,omitempty" json:"raw_argval,omitempty"`
	// LastValEnd is the alternative value end for last parameter
	LastValEnd *string `yaml:"last_val_end,omitempty" json:"last_val_end,omitempty"`
	// LastToolEnd is the alternative tool end for last tool call
	LastToolEnd *string `yaml:"last_tool_end,omitempty" json:"last_tool_end,omitempty"`
	// TrimRawArgVal indicates whether to trim whitespace from raw values
	TrimRawArgVal bool `yaml:"trim_raw_argval,omitempty" json:"trim_raw_argval,omitempty"`
	// AllowToolcallInThink allows tool calls inside thinking/reasoning blocks
	AllowToolcallInThink bool `yaml:"allow_toolcall_in_think,omitempty" json:"allow_toolcall_in_think,omitempty"`
}

type FuncCallResults struct {
	Name      string
	Arguments string
}

func (g FunctionsConfig) GrammarOptions() []func(o *grammars.GrammarOption) {
	opts := []func(o *grammars.GrammarOption){}
	if g.GrammarConfig.MixedMode {
		opts = append(opts, grammars.EnableMaybeString)
	}
	if g.GrammarConfig.ParallelCalls {
		opts = append(opts, grammars.EnableMaybeArray)
	}
	if g.GrammarConfig.DisableParallelNewLines {
		opts = append(opts, grammars.DisableParallelNewLines)
	}
	if g.GrammarConfig.Prefix != "" {
		opts = append(opts, grammars.SetPrefix(g.GrammarConfig.Prefix))
	}
	if g.GrammarConfig.NoMixedFreeString {
		opts = append(opts, grammars.NoMixedFreeString)
	}
	if g.GrammarConfig.ExpectStringsAfterJSON {
		opts = append(opts, grammars.ExpectStringsAfterJSON)
	}

	if g.GrammarConfig.SchemaType != "" {
		opts = append(opts, grammars.WithSchemaType(grammars.NewType(g.GrammarConfig.SchemaType)))
	}

	if g.FunctionNameKey != "" {
		opts = append(opts, grammars.WithFunctionName(g.FunctionNameKey))
	}

	opts = append(opts, grammars.SetPropOrder(g.GrammarConfig.PropOrder))
	return opts
}

func CleanupLLMResult(llmresult string, functionConfig FunctionsConfig) string {
	xlog.Debug("LLM result", "result", llmresult)

	for _, item := range functionConfig.ReplaceLLMResult {
		k, v := item.Key, item.Value
		xlog.Debug("Replacing", "key", k, "value", v)
		re := regexp.MustCompile(k)
		llmresult = re.ReplaceAllString(llmresult, v)
	}
	xlog.Debug("LLM result(processed)", "result", llmresult)

	return llmresult
}

func ParseTextContent(llmresult string, functionConfig FunctionsConfig) string {
	xlog.Debug("ParseTextContent", "result", llmresult)
	xlog.Debug("CaptureLLMResult", "config", functionConfig.CaptureLLMResult)

	for _, r := range functionConfig.CaptureLLMResult {
		// We use a regex to extract the JSON object from the response
		var respRegex = regexp.MustCompile(r)
		match := respRegex.FindStringSubmatch(llmresult)
		if len(match) >= 1 {
			m := strings.TrimSpace(match[1])
			return m
		}
	}

	return ""
}

// ParseJSON is a function that parses a JSON string that might contain multiple JSON objects
// and syntax errors in between by shifting the offset
// This for e.g. allow to parse
// { "foo": "bar" } invalid { "baz": "qux" }
// into
// [ { "foo": "bar" }, { "baz": "qux" } ]
// Credits to Michael Yang (https://github.com/mxyng) for the original implementation
// This is a slightly reworked version, improved for readability and error handling
// ParseJSON parses JSON objects from a string, supporting multiple JSON objects
// Now defaults to iterative parser for better streaming support
// Falls back to legacy parser if iterative parser fails
func ParseJSON(s string) ([]map[string]any, error) {
	// Try iterative parser first (non-partial mode for complete parsing)
	results, err := ParseJSONIterative(s, false)
	if err == nil && len(results) > 0 {
		return results, nil
	}
	// Fall back to legacy parser for backward compatibility
	return parseJSONLegacy(s)
}

// ParseJSONIterative parses JSON using the iterative parser
// Supports partial parsing for streaming scenarios
// Returns objects and arrays (matching llama.cpp behavior)
func ParseJSONIterative(s string, isPartial bool) ([]map[string]any, error) {
	parser := NewChatMsgParser(s, isPartial)
	var results []map[string]any

	// Try to parse JSON values one by one
	for parser.Pos() < len(parser.Input()) {
		jsonValue, isPartialJSON, _, err := parser.TryConsumeJSON()
		if err != nil {
			// If it's a partial exception and we're in partial mode, return what we have
			if _, ok := err.(*ChatMsgPartialException); ok && isPartial {
				break
			}
			// For non-partial errors or when not in partial mode, try legacy parsing
			return parseJSONLegacy(s)
		}
		if jsonValue != nil {
			// Convert to map[string]any if it's an object, or handle arrays
			if obj, ok := jsonValue.(map[string]any); ok {
				results = append(results, obj)
			} else if arr, ok := jsonValue.([]any); ok {
				// Handle arrays: extract objects from array
				for _, item := range arr {
					if obj, ok := item.(map[string]any); ok {
						results = append(results, obj)
					}
				}
			}
		}
		if isPartialJSON {
			break
		}
		// Skip whitespace between JSON values
		parser.ConsumeSpaces()
	}

	if len(results) > 0 {
		return results, nil
	}

	// Fallback to legacy parsing if iterative parser found nothing
	return parseJSONLegacy(s)
}

// parseJSONLegacy is the original decoder-based JSON parsing (kept for compatibility)
func parseJSONLegacy(s string) ([]map[string]any, error) {
	var objs []map[string]any
	offset := 0

	for offset < len(s) {
		var obj map[string]any
		decoder := json.NewDecoder(strings.NewReader(s[offset:]))

		err := decoder.Decode(&obj)
		switch {
		case errors.Is(err, io.EOF):
			return objs, nil
		case err == nil:
			offset += int(decoder.InputOffset())
			objs = append(objs, obj)
		default: // handle the error type
			var syntaxErr *json.SyntaxError
			var unmarshalTypeErr *json.UnmarshalTypeError

			switch {
			case errors.As(err, &syntaxErr):
				offset += int(syntaxErr.Offset)
			case errors.As(err, &unmarshalTypeErr):
				offset += int(unmarshalTypeErr.Offset)
			default:
				return objs, err
			}
		}
	}

	return objs, nil
}

// GetXMLFormatPreset returns a preset XML format by name, or nil if not found
// This is exported for use in chat.go streaming integration
func GetXMLFormatPreset(name string) *XMLToolCallFormat {
	formats := getAllXMLFormats()
	for _, format := range formats {
		if format.name == name {
			return format.format
		}
	}
	return nil
}

// xmlFormatPreset holds a preset format with its name
type xmlFormatPreset struct {
	name   string
	format *XMLToolCallFormat
}

// getAllXMLFormats returns all preset XML formats matching llama.cpp's formats
func getAllXMLFormats() []xmlFormatPreset {
	falseVal := false
	commaSpace := ", "
	emptyValEnd := ""

	return []xmlFormatPreset{
		{
			name: "functionary",
			format: &XMLToolCallFormat{
				ScopeStart: "",
				ToolStart:  "<function=",
				ToolSep:    ">",
				KeyStart:   "", // Parameters are JSON, not XML tags
				KeyValSep:  "",
				ValEnd:     "",
				ToolEnd:    "</function>",
				ScopeEnd:   "",
				RawArgVal:  &falseVal, // JSON only
			},
		},
		{
			name: "qwen3-coder",
			format: &XMLToolCallFormat{
				ScopeStart:    "<tool_call>",
				ToolStart:     "<function=",
				ToolSep:       ">",
				KeyStart:      "<parameter=",
				KeyValSep:     ">",
				ValEnd:        "</parameter>",
				ToolEnd:       "</function>",
				ScopeEnd:      "</tool_call>",
				TrimRawArgVal: true,
			},
		},
		{
			name: "glm-4.5",
			format: &XMLToolCallFormat{
				ScopeStart: "",
				ToolStart:  "<tool_call>",
				ToolSep:    "",
				KeyStart:   "<arg_key>",
				KeyValSep:  "</arg_key>",
				KeyValSep2: func() *string { s := "<arg_value>"; return &s }(),
				ValEnd:     "</arg_value>",
				ToolEnd:    "</tool_call>",
				ScopeEnd:   "",
			},
		},
		{
			name: "minimax-m2",
			format: &XMLToolCallFormat{
				ScopeStart: "<minimax:tool_call>",
				ToolStart:  "<invoke name=\"",
				ToolSep:    "\">",
				KeyStart:   "<parameter name=\"",
				KeyValSep:  "\">",
				ValEnd:     "</parameter>",
				ToolEnd:    "</invoke>",
				ScopeEnd:   "</minimax:tool_call>",
			},
		},
		{
			name: "kimi-k2",
			format: &XMLToolCallFormat{
				ScopeStart:           "<|tool_calls_section_begin|>",
				ToolStart:            "<|tool_call_begin|>",
				ToolSep:              "<|tool_call_argument_begin|>{",
				KeyStart:             "\"",
				KeyValSep:            "\":",
				ValEnd:               ",",
				ToolEnd:              "}<|tool_call_end|>",
				ScopeEnd:             "<|tool_calls_section_end|>",
				LastValEnd:           &emptyValEnd,
				RawArgVal:            &falseVal,
				AllowToolcallInThink: true, // Kimi-K2 supports tool calls in thinking blocks
			},
		},
		{
			name: "apriel-1.5",
			format: &XMLToolCallFormat{
				ScopeStart:  "<tool_calls>[",
				ToolStart:   "{\"name\": \"",
				ToolSep:     "\", \"arguments\": {",
				KeyStart:    "\"",
				KeyValSep:   "\": ",
				ValEnd:      commaSpace,
				ToolEnd:     "}, ",
				ScopeEnd:    "]</tool_calls>",
				LastValEnd:  &emptyValEnd,
				LastToolEnd: func() *string { s := "}"; return &s }(),
				RawArgVal:   &falseVal,
			},
		},
		{
			name: "xiaomi-mimo",
			format: &XMLToolCallFormat{
				ScopeStart: "",
				ToolStart:  "<tool_call>\n{\"name\": \"",
				ToolSep:    "\", \"arguments\": {",
				KeyStart:   "\"",
				KeyValSep:  "\": ",
				ValEnd:     commaSpace,
				ToolEnd:    "}\n</tool_call>",
				ScopeEnd:   "",
				LastValEnd: &emptyValEnd,
				RawArgVal:  &falseVal,
			},
		},
	}
}

// parseXMLAutoDetect tries all preset formats in sequence and returns results from the first one that succeeds
func parseXMLAutoDetect(s string) ([]FuncCallResults, error) {
	formats := getAllXMLFormats()
	for _, preset := range formats {
		results, err := parseXMLWithFormat(s, preset.format)
		if err == nil && len(results) > 0 {
			xlog.Debug("XML auto-detection succeeded", "format", preset.name, "count", len(results))
			return results, nil
		}
	}
	return nil, nil
}

// ParseXML is a function that parses XML-style tool calls from a string that might contain
// text and valid XML tool calls. If format is nil, it will auto-detect by trying all formats.
// Returns a slice of FuncCallResults with function names and JSON-encoded arguments.
// Now defaults to iterative parser for better streaming and partial parsing support.
// Falls back to regex parser if iterative parser fails for backward compatibility.
func ParseXML(s string, format *XMLToolCallFormat) ([]FuncCallResults, error) {
	// Try iterative parser first (non-partial mode for complete parsing)
	results, err := ParseXMLIterative(s, format, false)
	if err == nil && len(results) > 0 {
		return results, nil
	}
	// Fall back to regex parser for backward compatibility
	if format == nil {
		return parseXMLAutoDetect(s)
	}
	return parseXMLWithFormat(s, format)
}

// ParseXMLIterative parses XML tool calls using the iterative parser
// This provides better streaming and partial parsing support
func ParseXMLIterative(s string, format *XMLToolCallFormat, isPartial bool) ([]FuncCallResults, error) {
	parser := NewChatMsgParser(s, isPartial)

	// Auto-detect format if not provided
	if format == nil {
		formats := getAllXMLFormats()
		for _, fmtPreset := range formats {
			if fmtPreset.format != nil {
				// Try parsing with this format
				parser.MoveTo(0)
				parser.ClearTools()
				success, err := parser.TryConsumeXMLToolCalls(fmtPreset.format)
				if err != nil {
					// Check if it's a partial exception (recoverable)
					if _, ok := err.(*ChatMsgPartialException); ok {
						// Partial parse, return what we have
						return parser.ToolCalls(), nil
					}
					// Try next format
					continue
				}
				if success && len(parser.ToolCalls()) > 0 {
					return parser.ToolCalls(), nil
				}
			}
		}
		// No format matched, return empty
		return []FuncCallResults{}, nil
	}

	// Use specified format
	success, err := parser.TryConsumeXMLToolCalls(format)
	if err != nil {
		// Check if it's a partial exception (recoverable)
		if _, ok := err.(*ChatMsgPartialException); ok {
			// Partial parse, return what we have
			return parser.ToolCalls(), nil
		}
		return nil, err
	}

	if !success {
		return []FuncCallResults{}, nil
	}

	return parser.ToolCalls(), nil
}

// ParseXMLPartial parses XML tool calls that may be incomplete (for streaming support)
// It returns both complete results and partial results that can be emitted during streaming
// Reference: llama.cpp's partial parsing support
// Uses iterative parser for better partial detection
func ParseXMLPartial(s string, format *XMLToolCallFormat) (*PartialXMLResult, error) {
	// Use iterative parser with partial flag enabled for better streaming support
	results, err := ParseXMLIterative(s, format, true)
	if err != nil {
		return nil, err
	}

	// Check if the input ends with incomplete XML tags (indicating partial content)
	isPartial := false
	trimmed := strings.TrimSpace(s)

	// Auto-detect format if not provided to check for partial content
	if format == nil {
		formats := getAllXMLFormats()
		for _, fmtPreset := range formats {
			if fmtPreset.format != nil {
				format = fmtPreset.format
				break
			}
		}
	}

	if format != nil {
		// Check if string ends with incomplete tool_end or val_end
		// Also check for incomplete tags like "</parameter" (missing >)
		if !strings.HasSuffix(trimmed, format.ToolEnd) {
			if format.LastToolEnd != nil && !strings.HasSuffix(trimmed, *format.LastToolEnd) {
				// Check if it starts with tool_end but is incomplete
				if len(trimmed) > 0 && len(format.ToolEnd) > 0 {
					suffix := trimmed[max(0, len(trimmed)-len(format.ToolEnd)):]
					if strings.HasPrefix(format.ToolEnd, suffix) && suffix != format.ToolEnd {
						isPartial = true
					}
				}
			}
			// Also check for incomplete closing tags (ends with < but not complete)
			if strings.HasSuffix(trimmed, "<") || strings.HasSuffix(trimmed, "</") {
				isPartial = true
			}
		}
		if !strings.HasSuffix(trimmed, format.ValEnd) {
			if format.LastValEnd != nil && !strings.HasSuffix(trimmed, *format.LastValEnd) {
				if len(trimmed) > 0 && len(format.ValEnd) > 0 {
					suffix := trimmed[max(0, len(trimmed)-len(format.ValEnd)):]
					if strings.HasPrefix(format.ValEnd, suffix) && suffix != format.ValEnd {
						isPartial = true
					}
				}
			}
			// Check for incomplete closing tags
			if strings.HasSuffix(trimmed, "<") || strings.HasSuffix(trimmed, "</") {
				isPartial = true
			}
		}
		// Check for incomplete parameter tags
		if format.KeyStart != "" && (strings.HasSuffix(trimmed, "<parameter") || strings.HasSuffix(trimmed, "<parameter=")) {
			isPartial = true
		}
		// Check if we have tool_start but missing tool_end (incomplete tool call)
		if strings.Contains(trimmed, format.ToolStart) && !strings.HasSuffix(trimmed, format.ToolEnd) {
			if format.LastToolEnd == nil || !strings.HasSuffix(trimmed, *format.LastToolEnd) {
				// Check if tool_end appears anywhere (if not, it's partial)
				if !strings.Contains(trimmed, format.ToolEnd) {
					isPartial = true
				}
			}
		}
	}

	return &PartialXMLResult{
		Results:   results,
		IsPartial: isPartial,
	}, nil
}

func max(a, b int) int {
	if a > b {
		return a
	}
	return b
}

// parseXMLWithFormat parses XML tool calls using a specific format configuration
// Returns parsed results and error. Handles errors gracefully by continuing to parse other tool calls.
func parseXMLWithFormat(s string, format *XMLToolCallFormat) ([]FuncCallResults, error) {
	var results []FuncCallResults

	// Handle Functionary format (JSON parameters inside XML tags)
	if format.KeyStart == "" && format.ToolStart == "<function=" {
		return parseFunctionaryFormat(s, format)
	}

	// Handle formats with JSON-like structure (Apriel-1.5, Xiaomi-MiMo)
	// Note: Kimi-K2 is NOT JSON-like - it uses standard XML format with JSON arguments
	if format.ToolStart != "" && strings.Contains(format.ToolStart, "{\"name\"") {
		return parseJSONLikeXMLFormat(s, format)
	}

	// Handle GLM 4.5 format specially (function name on separate line after <tool_call>)
	if format.ToolStart == "<tool_call>" && format.ToolSep == "" && format.KeyStart == "<arg_key>" {
		return parseGLM45Format(s, format)
	}

	// Build regex patterns from format configuration
	// Escape special regex characters in format strings
	escapeRegex := func(str string) string {
		return regexp.QuoteMeta(str)
	}

	// Build scope pattern (optional)
	// llama.cpp validates that only whitespace appears before scope_start
	var scopePattern *regexp.Regexp
	if format.ScopeStart != "" {
		// Match scope_start with optional whitespace before it, but validate it's only whitespace
		scopeRegex := `(?s)(\s*)` + escapeRegex(format.ScopeStart) + `\s*(.*?)\s*` + escapeRegex(format.ScopeEnd)
		scopePattern = regexp.MustCompile(scopeRegex)
	}

	// Build tool call patterns - try both primary and alternative tool_end
	var toolCallPatterns []*regexp.Regexp

	buildToolCallPattern := func(toolEnd string) string {
		toolCallRegex := `(?s)` + escapeRegex(format.ToolStart)
		if format.ToolSep != "" {
			// Tool name is between ToolStart and ToolSep
			// Use non-greedy match to capture function name until ToolSep
			// We can't use [^...] for multi-character strings, so use .*? with ToolSep
			toolCallRegex += `(.*?)` + escapeRegex(format.ToolSep)
			toolCallRegex += `(.*?)` + escapeRegex(toolEnd)
		} else {
			// Tool name might be on a separate line (GLM 4.5) or after ToolStart
			// For GLM 4.5: <tool_call>\nfunction_name\n<arg_key>...
			// Match function name until we find key_start or newline
			if format.KeyStart != "" {
				// Match whitespace/newlines, then function name, then whitespace, then key_start
				// We'll capture the function name and the rest (including key_start)
				toolCallRegex += `\s*([^\n` + escapeRegex(format.KeyStart) + `]+?)\s*` + escapeRegex(format.KeyStart) + `(.*?)` + escapeRegex(toolEnd)
			} else {
				// Match until newline
				toolCallRegex += `\s*([^\n]+)\s*(.*?)` + escapeRegex(toolEnd)
			}
		}
		return toolCallRegex
	}

	// Primary pattern with tool_end
	toolCallPatterns = append(toolCallPatterns, regexp.MustCompile(buildToolCallPattern(format.ToolEnd)))
	// Alternative pattern with last_tool_end if specified
	if format.LastToolEnd != nil && *format.LastToolEnd != "" {
		toolCallPatterns = append(toolCallPatterns, regexp.MustCompile(buildToolCallPattern(*format.LastToolEnd)))
	}

	// Extract content to search in
	searchContent := s
	if scopePattern != nil {
		scopeMatches := scopePattern.FindAllStringSubmatch(s, -1)
		if len(scopeMatches) == 0 {
			// Scope not found
			// If scope_end is not empty/whitespace, this might be an error
			// But scope is optional, so try parsing without scope
			if strings.TrimSpace(format.ScopeEnd) != "" {
				// Scope expected but not found - this might indicate incomplete input
				// For now, try parsing without scope (scope is optional)
				xlog.Debug("scope_start not found but scope_end is non-empty", "scope_end", format.ScopeEnd)
			}
			searchContent = s
		} else {
			// Process each scope match separately
			for _, scopeMatch := range scopeMatches {
				if len(scopeMatch) >= 3 {
					// scopeMatch[1] is the whitespace before scope_start (we validate it's only whitespace)
					// scopeMatch[2] is the content inside the scope
					prelude := scopeMatch[1]
					// Validate that prelude contains only whitespace (llama.cpp behavior)
					allWhitespace := true
					for _, r := range prelude {
						if !strings.ContainsRune(" \t\n\r", r) {
							allWhitespace = false
							break
						}
					}
					if !allWhitespace {
						// Non-whitespace before scope_start, skip this match
						// This matches llama.cpp's behavior (line 394)
						xlog.Debug("non-whitespace before scope_start, skipping match", "prelude", prelude)
						continue
					}
					scopeContent := scopeMatch[2]
					// Validate scope_end is present in the match (scope pattern should include it)
					// The regex pattern already includes scope_end, so if we matched, it should be there
					// But we can verify the match is complete
					// Find all tool calls within this scope - try both patterns
					var toolCallMatches [][]string
					for _, pattern := range toolCallPatterns {
						matches := pattern.FindAllStringSubmatch(scopeContent, -1)
						toolCallMatches = append(toolCallMatches, matches...)
					}
					for _, match := range toolCallMatches {
						if len(match) >= 3 {
							functionName := strings.TrimSpace(match[1])

							// Handle Kimi-K2 function name prefix stripping: "functions.name:index" -> "name"
							if strings.HasPrefix(functionName, "functions.") {
								// Remove "functions." prefix
								functionName = functionName[10:]
								// Remove ":index" suffix if present
								if idx := strings.LastIndex(functionName, ":"); idx != -1 {
									// Check if what follows ":" is all digits
									suffix := functionName[idx+1:]
									if len(suffix) > 0 {
										allDigits := true
										for _, r := range suffix {
											if r < '0' || r > '9' {
												allDigits = false
												break
											}
										}
										if allDigits {
											functionName = functionName[:idx]
										}
									}
								}
							}

							var functionContent string
							if format.ToolSep == "" && format.KeyStart != "" {
								// Content includes key_start, so prepend it
								functionContent = format.KeyStart + match[2]
							} else {
								functionContent = match[2]
							}

							// Check for empty tool call: if tool_end appears in function name or content is empty
							// This matches llama.cpp's behavior (lines 419-424)
							if strings.Contains(functionName, format.ToolEnd) || (format.LastToolEnd != nil && strings.Contains(functionName, *format.LastToolEnd)) {
								// Empty tool call - emit with empty arguments
								cleanName := strings.TrimSpace(functionName)
								if idx := strings.Index(cleanName, format.ToolEnd); idx != -1 {
									cleanName = strings.TrimSpace(cleanName[:idx])
								} else if format.LastToolEnd != nil {
									if idx := strings.Index(cleanName, *format.LastToolEnd); idx != -1 {
										cleanName = strings.TrimSpace(cleanName[:idx])
									}
								}
								results = append(results, FuncCallResults{
									Name:      cleanName,
									Arguments: "{}",
								})
								continue
							}

							// Check if content is empty or only whitespace
							if strings.TrimSpace(functionContent) == "" {
								// Empty tool call - emit with empty arguments
								results = append(results, FuncCallResults{
									Name:      functionName,
									Arguments: "{}",
								})
								continue
							}

							// Parse parameters based on format
							args, err := parseXMLParametersWithFormat(functionContent, format)
							if err != nil {
								xlog.Debug("error parsing XML parameters", "error", err, "content", functionContent)
								continue
							}

							// If no parameters were parsed and content was not empty, still create tool call with empty args
							if len(args) == 0 && strings.TrimSpace(functionContent) != "" {
								// Check if there's any parameter-like content that just didn't match
								if !strings.Contains(functionContent, format.KeyStart) {
									argsJSON, _ := json.Marshal(args)
									results = append(results, FuncCallResults{
										Name:      functionName,
										Arguments: string(argsJSON),
									})
									continue
								}
							}

							argsJSON, _ := json.Marshal(args)
							results = append(results, FuncCallResults{
								Name:      functionName,
								Arguments: string(argsJSON),
							})
						}
					}
				}
			}
			return results, nil
		}
	}

	// No scope, find all tool calls directly in the string - try both patterns
	var toolCallMatches [][]string
	for _, pattern := range toolCallPatterns {
		matches := pattern.FindAllStringSubmatch(searchContent, -1)
		toolCallMatches = append(toolCallMatches, matches...)
	}
	if len(toolCallMatches) == 0 {
		return nil, nil
	}

	// Process each tool call
	for _, match := range toolCallMatches {
		if len(match) < 3 {
			continue
		}

		// Validate tool_end is complete (exact size match)
		// This matches llama.cpp's behavior (line 595)
		fullMatch := match[0]
		expectedToolEnd := format.ToolEnd
		if format.LastToolEnd != nil && strings.HasSuffix(fullMatch, *format.LastToolEnd) {
			expectedToolEnd = *format.LastToolEnd
		}
		if !strings.HasSuffix(fullMatch, expectedToolEnd) {
			// tool_end not found at end, skip this match
			xlog.Debug("tool_end validation failed", "expected", expectedToolEnd, "match", fullMatch)
			continue
		}
		// Verify the tool_end is exactly the expected size (not a partial match)
		// Extract the tool_end from the end of the match
		if len(fullMatch) < len(expectedToolEnd) {
			// Match is shorter than expected tool_end, skip
			continue
		}
		actualToolEnd := fullMatch[len(fullMatch)-len(expectedToolEnd):]
		if actualToolEnd != expectedToolEnd {
			// tool_end doesn't match exactly, skip
			xlog.Debug("tool_end size validation failed", "expected", expectedToolEnd, "actual", actualToolEnd)
			continue
		}

		functionName := strings.TrimSpace(match[1])

		// Handle Kimi-K2 function name prefix stripping: "functions.name:index" -> "name"
		if strings.HasPrefix(functionName, "functions.") {
			// Remove "functions." prefix
			functionName = functionName[10:]
			// Remove ":index" suffix if present
			if idx := strings.LastIndex(functionName, ":"); idx != -1 {
				// Check if what follows ":" is all digits
				suffix := functionName[idx+1:]
				if len(suffix) > 0 {
					allDigits := true
					for _, r := range suffix {
						if r < '0' || r > '9' {
							allDigits = false
							break
						}
					}
					if allDigits {
						functionName = functionName[:idx]
					}
				}
			}
		}

		var functionContent string
		if len(match) >= 3 {
			if format.ToolSep == "" && format.KeyStart != "" {
				// For GLM 4.5 format, match[2] contains the content starting from key_start
				functionContent = match[2]
			} else {
				functionContent = match[2]
			}
		}

		// Check for empty tool call: if tool_end appears in function name prelude or content is empty
		// This matches llama.cpp's behavior (lines 419-424)
		// If the function name contains tool_end, it indicates the tool call has no arguments
		if strings.Contains(functionName, format.ToolEnd) || (format.LastToolEnd != nil && strings.Contains(functionName, *format.LastToolEnd)) {
			// Empty tool call - emit with empty arguments
			results = append(results, FuncCallResults{
				Name:      strings.TrimSpace(strings.Split(functionName, format.ToolEnd)[0]),
				Arguments: "{}",
			})
			continue
		}

		// Check if content is empty or only whitespace (another indicator of empty tool call)
		if strings.TrimSpace(functionContent) == "" {
			// Empty tool call - emit with empty arguments
			results = append(results, FuncCallResults{
				Name:      functionName,
				Arguments: "{}",
			})
			continue
		}

		// Parse parameters based on format
		args, err := parseXMLParametersWithFormat(functionContent, format)
		if err != nil {
			xlog.Debug("error parsing XML parameters", "error", err, "content", functionContent)
			continue
		}

		// If no parameters were parsed and content was not empty, still create tool call with empty args
		// This handles cases where parameters exist but couldn't be parsed
		if len(args) == 0 && strings.TrimSpace(functionContent) != "" {
			// Check if there's any parameter-like content that just didn't match
			// If not, treat as empty tool call
			if !strings.Contains(functionContent, format.KeyStart) {
				argsJSON, _ := json.Marshal(args)
				results = append(results, FuncCallResults{
					Name:      functionName,
					Arguments: string(argsJSON),
				})
				continue
			}
		}

		argsJSON, _ := json.Marshal(args)
		results = append(results, FuncCallResults{
			Name:      functionName,
			Arguments: string(argsJSON),
		})
	}

	return results, nil
}

// parseGLM45Format handles GLM 4.5 format: <tool_call>\nfunction_name\n<arg_key>...</arg_key><arg_value>...</arg_value>...
func parseGLM45Format(s string, format *XMLToolCallFormat) ([]FuncCallResults, error) {
	var results []FuncCallResults

	// Pattern: <tool_call>\nfunction_name\n<arg_key>...</arg_key><arg_value>...</arg_value>...</tool_call>
	pattern := regexp.MustCompile(`(?s)<tool_call>\s*([^\n<]+)\s*(.*?)\s*</tool_call>`)
	matches := pattern.FindAllStringSubmatch(s, -1)

	for _, match := range matches {
		if len(match) >= 3 {
			functionName := strings.TrimSpace(match[1])

			// Handle Kimi-K2 function name prefix stripping: "functions.name:index" -> "name"
			if strings.HasPrefix(functionName, "functions.") {
				// Remove "functions." prefix
				functionName = functionName[10:]
				// Remove ":index" suffix if present
				if idx := strings.LastIndex(functionName, ":"); idx != -1 {
					// Check if what follows ":" is all digits
					suffix := functionName[idx+1:]
					if len(suffix) > 0 {
						allDigits := true
						for _, r := range suffix {
							if r < '0' || r > '9' {
								allDigits = false
								break
							}
						}
						if allDigits {
							functionName = functionName[:idx]
						}
					}
				}
			}

			functionContent := match[2]

			// Check for empty tool call: if content is empty or only whitespace
			if strings.TrimSpace(functionContent) == "" {
				// Empty tool call - emit with empty arguments
				results = append(results, FuncCallResults{
					Name:      functionName,
					Arguments: "{}",
				})
				continue
			}

			// Parse parameters using GLM 4.5 format
			args, err := parseXMLParametersWithFormat(functionContent, format)
			if err != nil {
				xlog.Debug("error parsing GLM 4.5 parameters", "error", err, "content", functionContent)
				continue
			}

			// If no parameters were parsed, still create tool call with empty args
			if len(args) == 0 {
				argsJSON, _ := json.Marshal(args)
				results = append(results, FuncCallResults{
					Name:      functionName,
					Arguments: string(argsJSON),
				})
				continue
			}

			argsJSON, _ := json.Marshal(args)
			results = append(results, FuncCallResults{
				Name:      functionName,
				Arguments: string(argsJSON),
			})
		}
	}

	return results, nil
}

// parseFunctionaryFormat handles Functionary format: <function=name>{"key": "value"}</function>
func parseFunctionaryFormat(s string, format *XMLToolCallFormat) ([]FuncCallResults, error) {
	var results []FuncCallResults

	// Pattern: <function=name>JSON</function>
	pattern := regexp.MustCompile(`(?s)<function=([^>]+)>(.*?)</function>`)
	matches := pattern.FindAllStringSubmatch(s, -1)

	for _, match := range matches {
		if len(match) >= 3 {
			functionName := strings.TrimSpace(match[1])
			jsonContent := strings.TrimSpace(match[2])

			// Parse JSON content as arguments
			var args map[string]any
			if err := json.Unmarshal([]byte(jsonContent), &args); err != nil {
				xlog.Debug("error parsing Functionary JSON", "error", err, "content", jsonContent)
				continue
			}

			argsJSON, _ := json.Marshal(args)
			results = append(results, FuncCallResults{
				Name:      functionName,
				Arguments: string(argsJSON),
			})
		}
	}

	return results, nil
}

// parseJSONLikeXMLFormat handles formats like Apriel-1.5, Xiaomi-MiMo, Kimi-K2 that have JSON-like structure
func parseJSONLikeXMLFormat(s string, format *XMLToolCallFormat) ([]FuncCallResults, error) {
	var results []FuncCallResults

	// Build pattern to match the JSON-like structure
	escapeRegex := func(str string) string {
		return regexp.QuoteMeta(str)
	}

	// Pattern: scope_start + tool_start + name + tool_sep + arguments + tool_end + scope_end
	var pattern *regexp.Regexp
	if format.ScopeStart != "" {
		patternStr := `(?s)` + escapeRegex(format.ScopeStart) + `(.*?)` + escapeRegex(format.ScopeEnd)
		pattern = regexp.MustCompile(patternStr)
	} else {
		patternStr := `(?s)` + escapeRegex(format.ToolStart) + `([^"]+)"` + escapeRegex(format.ToolSep) + `(.*?)` + escapeRegex(format.ToolEnd)
		pattern = regexp.MustCompile(patternStr)
	}

	matches := pattern.FindAllStringSubmatch(s, -1)
	for _, match := range matches {
		if len(match) < 2 {
			continue
		}

		// Extract JSON content
		jsonContent := match[1]
		if format.ScopeStart != "" {
			// Need to extract individual tool calls from the array
			// Pattern: {"name": "...", "arguments": {...}}
			toolPattern := regexp.MustCompile(`(?s)\{\s*"name"\s*:\s*"([^"]+)"\s*,\s*"arguments"\s*:\s*(\{.*?\})\s*\}`)
			toolMatches := toolPattern.FindAllStringSubmatch(jsonContent, -1)
			for _, toolMatch := range toolMatches {
				if len(toolMatch) >= 3 {
					functionName := strings.TrimSpace(toolMatch[1])
					argsJSON := toolMatch[2]
					results = append(results, FuncCallResults{
						Name:      functionName,
						Arguments: argsJSON,
					})
				}
			}
		} else {
			// Single tool call
			namePattern := regexp.MustCompile(`"name"\s*:\s*"([^"]+)"`)
			nameMatch := namePattern.FindStringSubmatch(jsonContent)
			if len(nameMatch) >= 2 {
				functionName := strings.TrimSpace(nameMatch[1])
				argsPattern := regexp.MustCompile(`"arguments"\s*:\s*(\{.*\})`)
				argsMatch := argsPattern.FindStringSubmatch(jsonContent)
				argsJSON := "{}"
				if len(argsMatch) >= 2 {
					argsJSON = argsMatch[1]
				}
				results = append(results, FuncCallResults{
					Name:      functionName,
					Arguments: argsJSON,
				})
			}
		}
	}

	return results, nil
}

// utf8TruncateSafe truncates a string at a safe UTF-8 boundary
// This prevents truncation in the middle of multi-byte characters
// Reference: llama.cpp/common/chat-parser-xml-toolcall.cpp lines 27-58
func utf8TruncateSafe(s string) string {
	if len(s) == 0 {
		return s
	}
	// Check if the string ends at a valid UTF-8 boundary
	// If not, truncate to the last valid boundary
	for i := len(s); i > 0 && i > len(s)-4; i-- {
		if utf8.ValidString(s[:i]) {
			return s[:i]
		}
	}
	// If we can't find a valid boundary in the last 4 bytes, truncate conservatively
	if len(s) > 3 {
		return s[:len(s)-3]
	}
	return ""
}

// PartialXMLResult represents a partial XML parsing result that can be emitted during streaming
type PartialXMLResult struct {
	Results    []FuncCallResults
	IsPartial  bool
	PartialArg string // The argument that was partially parsed
}

// XML_TOOL_CALL_PARTIAL_FLAG is a marker used to indicate partial JSON in tool calls
// Reference: llama.cpp/common/chat-parser-xml-toolcall.cpp line 314
const XML_TOOL_CALL_PARTIAL_FLAG = "XML_TOOL_CALL_PARTIAL_FLAG"

// partialJSON cleans up partial JSON by removing incomplete parts marked with XML_TOOL_CALL_PARTIAL_FLAG
// Reference: llama.cpp/common/chat-parser-xml-toolcall.cpp lines 314-330
func partialJSON(jsonStr string) (string, bool) {
	pos := strings.LastIndex(jsonStr, XML_TOOL_CALL_PARTIAL_FLAG)
	if pos == -1 {
		return jsonStr, false
	}
	// Check that only valid JSON characters follow the flag
	for i := pos + len(XML_TOOL_CALL_PARTIAL_FLAG); i < len(jsonStr); i++ {
		ch := jsonStr[i]
		if ch != '\'' && ch != '"' && ch != '}' && ch != ':' && ch != ']' && !strings.ContainsRune(" \t\n\r", rune(ch)) {
			return jsonStr, false
		}
	}
	// Remove the flag and everything after it
	if pos > 0 && jsonStr[pos-1] == '"' {
		pos--
	}
	return jsonStr[:pos], true
}

// genPartialJSON generates partial JSON with XML_TOOL_CALL_PARTIAL_FLAG marker
// Reference: llama.cpp/common/chat-parser-xml-toolcall.cpp lines 332-343
func genPartialJSON(args map[string]any, functionName string, rest string, needle string) (string, bool) {
	// Add the partial argument with the flag
	args[rest+needle] = XML_TOOL_CALL_PARTIAL_FLAG
	jsonBytes, err := json.Marshal(args)
	if err != nil {
		return "", false
	}
	jsonStr := string(jsonBytes)
	// Try to clean up the partial JSON
	if cleaned, isPartial := partialJSON(jsonStr); isPartial {
		return cleaned, true
	}
	return jsonStr, false
}

// parseXMLParametersWithFormat extracts parameters from XML content based on format configuration
func parseXMLParametersWithFormat(content string, format *XMLToolCallFormat) (map[string]any, error) {
	args := make(map[string]any)

	// Handle GLM 4.5 format: <arg_key>key</arg_key><arg_value>value</arg_value>
	if format.KeyValSep2 != nil && *format.KeyValSep2 == "<arg_value>" {
		return parseGLM45Parameters(content, format)
	}

	// Special case: If content is already valid JSON and format expects JSON (like Kimi-K2),
	// try to parse it as JSON first
	if format.KeyStart == "\"" && format.KeyValSep == "\":" && (format.RawArgVal == nil || !*format.RawArgVal) {
		// Try parsing as complete JSON object first
		content = strings.TrimSpace(content)
		if strings.HasPrefix(content, "{") && strings.HasSuffix(content, "}") {
			var jsonArgs map[string]any
			if err := json.Unmarshal([]byte(content), &jsonArgs); err == nil {
				// Successfully parsed as JSON, return it
				return jsonArgs, nil
			}
		}
	}

	// Handle standard parameter format: <parameter=name>value</parameter> or <parameter name="name">value</parameter>
	if format.KeyStart != "" {
		return parseStandardParameters(content, format)
	}

	return args, nil
}

// parseMsgWithXMLToolCalls parses content with reasoning blocks and XML tool calls
// This handles <think> or <think> tags and extracts tool calls
// Reference: llama.cpp/common/chat-parser-xml-toolcall.cpp lines 654-872
func parseMsgWithXMLToolCalls(s string, format *XMLToolCallFormat, startThink string, endThink string) ([]FuncCallResults, string, error) {
	if startThink == "" {
		startThink = "<think>"
	}
	if endThink == "" {
		endThink = "</think>"
	}

	var results []FuncCallResults
	var reasoningContent strings.Builder
	var content strings.Builder

	// Simple approach: find reasoning blocks and tool calls
	// For more complex scenarios, we'd need iterative parsing
	thinkStartIdx := strings.Index(s, startThink)

	if thinkStartIdx == -1 {
		// No reasoning blocks, just parse tool calls
		xmlResults, err := parseXMLWithFormat(s, format)
		return xmlResults, "", err
	}

	// Process content before first thinking block
	if thinkStartIdx > 0 {
		preContent := s[:thinkStartIdx]
		xmlResults, _ := parseXMLWithFormat(preContent, format)
		results = append(results, xmlResults...)
		content.WriteString(preContent)
	}

	// Process thinking blocks and tool calls
	pos := 0
	for pos < len(s) {
		thinkStart := strings.Index(s[pos:], startThink)
		if thinkStart == -1 {
			// No more thinking blocks, process rest
			remaining := s[pos:]
			xmlResults, _ := parseXMLWithFormat(remaining, format)
			results = append(results, xmlResults...)
			content.WriteString(remaining)
			break
		}
		thinkStart += pos

		thinkEnd := strings.Index(s[thinkStart+len(startThink):], endThink)
		if thinkEnd == -1 {
			// Unclosed thinking block
			if format.AllowToolcallInThink {
				// Allow tool calls in unclosed thinking block
				thinkingContent := s[thinkStart+len(startThink):]
				reasoningContent.WriteString(thinkingContent)
				// Try to parse tool calls from thinking content
				xmlResults, _ := parseXMLWithFormat(thinkingContent, format)
				results = append(results, xmlResults...)
			} else {
				// Skip tool calls in unclosed thinking block
				content.WriteString(s[pos:thinkStart])
			}
			break
		}
		thinkEnd += thinkStart + len(startThink)

		// Extract thinking content
		thinkingContent := s[thinkStart+len(startThink) : thinkEnd]
		reasoningContent.WriteString(thinkingContent)

		// Check for tool calls between thinking blocks
		betweenContent := s[pos:thinkStart]
		if len(betweenContent) > 0 {
			xmlResults, _ := parseXMLWithFormat(betweenContent, format)
			results = append(results, xmlResults...)
			content.WriteString(betweenContent)
		}

		// Check for tool calls after thinking block
		pos = thinkEnd + len(endThink)
	}

	return results, reasoningContent.String(), nil
}

// parseGLM45Parameters handles GLM 4.5 format with <arg_key> and <arg_value> pairs
func parseGLM45Parameters(content string, format *XMLToolCallFormat) (map[string]any, error) {
	args := make(map[string]any)

	// Pattern: <arg_key>key</arg_key><arg_value>value</arg_value>
	pattern := regexp.MustCompile(`(?s)<arg_key>(.*?)</arg_key>\s*<arg_value>(.*?)</arg_value>`)
	matches := pattern.FindAllStringSubmatch(content, -1)

	for _, match := range matches {
		if len(match) >= 3 {
			paramName := strings.TrimSpace(match[1])
			paramValue := strings.TrimSpace(match[2])
			args[paramName] = parseParameterValue(paramValue, format)
		}
	}

	return args, nil
}

// parseStandardParameters handles standard parameter formats
func parseStandardParameters(content string, format *XMLToolCallFormat) (map[string]any, error) {
	args := make(map[string]any)

	escapeRegex := func(str string) string {
		return regexp.QuoteMeta(str)
	}

	// Build parameter patterns - try both primary and alternative endings
	var parameterPatterns []*regexp.Regexp

	if strings.Contains(format.KeyStart, "=") {
		// Format: <parameter=name>value</parameter>
		patternStr := `(?s)` + escapeRegex(format.KeyStart) + `([^>]+)` + escapeRegex(format.KeyValSep) + `(.*?)` + escapeRegex(format.ValEnd)
		parameterPatterns = append(parameterPatterns, regexp.MustCompile(patternStr))
		// Add alternative ending if specified
		if format.LastValEnd != nil && *format.LastValEnd != "" {
			altPatternStr := `(?s)` + escapeRegex(format.KeyStart) + `([^>]+)` + escapeRegex(format.KeyValSep) + `(.*?)` + escapeRegex(*format.LastValEnd)
			parameterPatterns = append(parameterPatterns, regexp.MustCompile(altPatternStr))
		}
	} else if strings.Contains(format.KeyStart, "name=\"") {
		// Format: <parameter name="name">value</parameter>
		patternStr := `(?s)` + escapeRegex(format.KeyStart) + `([^"]+)"` + escapeRegex(format.KeyValSep) + `(.*?)` + escapeRegex(format.ValEnd)
		parameterPatterns = append(parameterPatterns, regexp.MustCompile(patternStr))
		// Add alternative ending if specified
		if format.LastValEnd != nil && *format.LastValEnd != "" {
			altPatternStr := `(?s)` + escapeRegex(format.KeyStart) + `([^"]+)"` + escapeRegex(format.KeyValSep) + `(.*?)` + escapeRegex(*format.LastValEnd)
			parameterPatterns = append(parameterPatterns, regexp.MustCompile(altPatternStr))
		}
	} else {
		// Fallback: try to match key_start...key_val_sep...val_end
		patternStr := `(?s)` + escapeRegex(format.KeyStart) + `([^` + escapeRegex(format.KeyValSep) + `]+)` + escapeRegex(format.KeyValSep)
		if format.KeyValSep2 != nil {
			patternStr += escapeRegex(*format.KeyValSep2)
		}
		patternStr += `(.*?)` + escapeRegex(format.ValEnd)
		parameterPatterns = append(parameterPatterns, regexp.MustCompile(patternStr))
		// Add alternative ending if specified
		if format.LastValEnd != nil && *format.LastValEnd != "" {
			altPatternStr := `(?s)` + escapeRegex(format.KeyStart) + `([^` + escapeRegex(format.KeyValSep) + `]+)` + escapeRegex(format.KeyValSep)
			if format.KeyValSep2 != nil {
				altPatternStr += escapeRegex(*format.KeyValSep2)
			}
			altPatternStr += `(.*?)` + escapeRegex(*format.LastValEnd)
			parameterPatterns = append(parameterPatterns, regexp.MustCompile(altPatternStr))
		}
	}

	// Track which parameters we've parsed to avoid duplicates
	// Use a map to store position info so we can handle last_val_end correctly
	type paramMatch struct {
		name     string
		value    string
		position int
	}
	var allMatches []paramMatch

	// Collect all matches from all patterns
	for _, pattern := range parameterPatterns {
		matches := pattern.FindAllStringSubmatch(content, -1)
		for _, match := range matches {
			if len(match) >= 3 {
				paramName := strings.TrimSpace(match[1])
				paramValue := strings.TrimSpace(match[2])
				// Find the position of this match in the content
				pos := strings.Index(content, match[0])
				if pos != -1 {
					allMatches = append(allMatches, paramMatch{
						name:     paramName,
						value:    paramValue,
						position: pos,
					})
				}
			}
		}
	}

	// Sort by position to process in order
	// If we have last_val_end, the last parameter should use it
	// For now, we'll use the first match for each parameter name (primary pattern takes precedence)
	seenParams := make(map[string]bool)
	for _, match := range allMatches {
		if !seenParams[match.name] {
			args[match.name] = parseParameterValue(match.value, format)
			seenParams[match.name] = true
		}
	}

	return args, nil
}

// parseParameterValue parses a parameter value based on format configuration
// Implements JSON-first parsing: tries JSON parsing first (if raw_argval is false/null),
// validates JSON is complete, then falls back to text parsing.
// This matches llama.cpp's behavior in chat-parser-xml-toolcall.cpp lines 501-555
func parseParameterValue(paramValue string, format *XMLToolCallFormat) any {
	// Trim if configured
	if format.TrimRawArgVal {
		paramValue = strings.TrimSpace(paramValue)
	}

	// Handle raw_argval option
	if format.RawArgVal != nil {
		if *format.RawArgVal {
			// Raw string only - no JSON parsing
			return paramValue
		}
		// raw_argval is false - JSON only, must be valid JSON
		var jsonValue any
		if err := json.Unmarshal([]byte(paramValue), &jsonValue); err == nil {
			// Valid JSON - return parsed value (including primitives)
			return jsonValue
		}
		// JSON parsing failed but raw_argval is false - return as string anyway
		// (llama.cpp would throw an error, but we're more lenient)
		return paramValue
	}

	// Default: raw_argval is nil - try JSON first, fallback to text
	// This matches llama.cpp's behavior where both are allowed when raw_argval is nullopt
	var jsonValue any
	if err := json.Unmarshal([]byte(paramValue), &jsonValue); err != nil {
		// Not valid JSON, treat as plain text string
		return paramValue
	}

	// Valid JSON was parsed - return the parsed value
	// This includes objects, arrays, and primitives (null, true, false, numbers, strings)
	// This matches llama.cpp's behavior where JSON values (including primitives) are used as-is
	return jsonValue
}

func ParseFunctionCall(llmresult string, functionConfig FunctionsConfig) []FuncCallResults {

	xlog.Debug("LLM result", "result", llmresult)

	for _, item := range functionConfig.ReplaceFunctionResults {
		k, v := item.Key, item.Value
		xlog.Debug("Replacing", "key", k, "value", v)
		re := regexp.MustCompile(k)
		llmresult = re.ReplaceAllString(llmresult, v)
	}
	xlog.Debug("LLM result(function cleanup)", "result", llmresult)

	functionNameKey := defaultFunctionNameKey
	functionArgumentsKey := defaultFunctionArgumentsKey
	if functionConfig.FunctionNameKey != "" {
		functionNameKey = functionConfig.FunctionNameKey
	}
	if functionConfig.FunctionArgumentsKey != "" {
		functionArgumentsKey = functionConfig.FunctionArgumentsKey
	}

	results := []FuncCallResults{}
	llmResults := []string{}

	extractJSON := func(results []string) (result []FuncCallResults, e error) {
		// As we have to change the result before processing, we can't stream the answer token-by-token (yet?)
		result = make([]FuncCallResults, 0)

		for _, s := range results {
			var ss []map[string]any

			s = utils.EscapeNewLines(s)
			ss, err := ParseJSON(s)
			//err := json.Unmarshal([]byte(s), &ss)
			if err != nil {
				xlog.Debug("unable to unmarshal llm result in a single object or an array of JSON objects", "error", err, "escapedLLMResult", s)
			}

			xlog.Debug("Function return", "result", s, "parsed", ss)

			for _, s := range ss {
				// The grammar defines the function name as "function", while OpenAI returns "name"
				func_name, ok := s[functionNameKey]
				if !ok {
					continue
					//return result, fmt.Errorf("unable to find function name in result")
				}
				// Arguments from grammar result is a map[string]interface{}, but OpenAI expects a stringified JSON object
				// We marshal it to JSON string here to match OpenAI's format
				args, ok := s[functionArgumentsKey]
				if !ok {
					continue
					//return result, fmt.Errorf("unable to find arguments in result")
				}
				// Marshal arguments to JSON string (handles both object and string cases)
				var d []byte
				if argsStr, ok := args.(string); ok {
					// Already a string, use it directly
					d = []byte(argsStr)
				} else {
					// Object, marshal to JSON
					d, _ = json.Marshal(args)
				}
				funcName, ok := func_name.(string)
				if !ok {
					continue
					//return result, fmt.Errorf("unable to cast function name to string")
				}

				result = append(result, FuncCallResults{Name: funcName, Arguments: string(d)})
			}
		}

		return result, nil
	}

	// the response is a string that we have to parse
	result := make(map[string]string)
	if len(functionConfig.JSONRegexMatch) != 0 {
		for _, r := range functionConfig.JSONRegexMatch {
			// We use a regex to extract the JSON object from the response
			var respRegex = regexp.MustCompile(r)
			match := respRegex.FindAllStringSubmatch(llmresult, -1)
			var allMatches []string
			for _, m := range match {
				if len(m) > 1 {
					// we match the first group
					allMatches = append(allMatches, m[1])
				}
			}
			if len(allMatches) > 0 {
				llmResults = append(llmResults, allMatches...)
				break
			}
		}
	}

	if len(functionConfig.ResponseRegex) > 0 {
		// We use named regexes here to extract the function name and arguments
		// obviously, this expects the LLM to be stable and return correctly formatted JSON
		// Pre-compile regexes for better performance
		compiledRegexes := make([]*regexp.Regexp, 0, len(functionConfig.ResponseRegex))
		for _, r := range functionConfig.ResponseRegex {
			compiledRegexes = append(compiledRegexes, regexp.MustCompile(r))
		}
		for _, respRegex := range compiledRegexes {
			matches := respRegex.FindAllStringSubmatch(llmresult, -1)
			for _, match := range matches {
				for i, name := range respRegex.SubexpNames() {
					if i != 0 && name != "" && len(match) > i {
						result[name] = match[i]
					}
				}

				functionName := result[functionNameKey]
				if functionName == "" {
					return results
				}
				results = append(results, FuncCallResults{Name: result[functionNameKey], Arguments: ParseFunctionCallArgs(result[functionArgumentsKey], functionConfig)})
			}
		}
	} else {
		if len(llmResults) == 0 {
			llmResults = append(llmResults, llmresult)
		}
		results, _ = extractJSON(llmResults)
	}

	// Determine which XML format to use (if any)
	var xmlFormat *XMLToolCallFormat
	if functionConfig.XMLFormat != nil {
		// Custom format specified
		xmlFormat = functionConfig.XMLFormat
		xlog.Debug("Using custom XML format")
	} else if functionConfig.XMLFormatPreset != "" {
		// Preset format specified
		xmlFormat = GetXMLFormatPreset(functionConfig.XMLFormatPreset)
		if xmlFormat == nil {
			xlog.Debug("Unknown XML format preset, falling back to auto-detection", "preset", functionConfig.XMLFormatPreset)
		} else {
			xlog.Debug("Using XML format preset", "preset", functionConfig.XMLFormatPreset)
		}
	}
	// If xmlFormat is still nil, ParseXML will auto-detect

	// If no results from JSON parsing, try XML parsing
	// This handles cases where the response contains XML tool calls instead of JSON,
	// or mixed content with XML tool calls
	// Skip XML parsing if JSONRegexMatch or ResponseRegex was used and found results (to avoid double-parsing)
	// ResponseRegex extracts content that might look like XML (e.g., <function=name>args</function>)
	// but we've already parsed it, so we shouldn't try XML parsing on the same content
	skipXMLParsing := (len(functionConfig.JSONRegexMatch) > 0 || len(functionConfig.ResponseRegex) > 0) && len(results) > 0
	if len(results) == 0 && !skipXMLParsing {
		xmlResults, err := ParseXML(llmresult, xmlFormat)
		if err == nil && len(xmlResults) > 0 {
			xlog.Debug("Found XML tool calls", "count", len(xmlResults))
			results = append(results, xmlResults...)
		}
	} else if len(results) > 0 && !skipXMLParsing {
		// Even if we found JSON results, check for XML tool calls in the response
		// This handles mixed content scenarios (text + JSON + XML)
		// But skip if JSONRegexMatch or ResponseRegex was used (they already extracted the content)
		xmlResults, err := ParseXML(llmresult, xmlFormat)
		if err == nil && len(xmlResults) > 0 {
			// Check if JSON is inside XML tags, if so, skip it
			for _, result := range xmlResults {
				jsonResults, _ := extractJSON([]string{result.Name})
				if len(jsonResults) > 0 {
					xlog.Debug("Found valid JSON inside XML tags, skipping XML parsing", "json_count", len(jsonResults))
				} else {
					xlog.Debug("Found additional XML tool calls alongside JSON", "xml_count", len(xmlResults))
					results = append(results, xmlResults...)
				}
			}
		}
	}

	return results
}

func ParseFunctionCallArgs(functionArguments string, functionConfig FunctionsConfig) string {
	// Clean up double curly braces (common issue with template engines)
	// Replace {{ with { and }} with } but only if they appear at the start/end
	// This handles cases like {{"key":"value"}} -> {"key":"value"}
	cleaned := functionArguments
	//if strings.HasPrefix(cleaned, "{{") && strings.HasSuffix(cleaned, "}}") {
	// Check if it's double braces at the boundaries
	//	cleaned = strings.TrimPrefix(cleaned, "{")
	//	cleaned = strings.TrimSuffix(cleaned, "}")
	//}

	if len(functionConfig.ArgumentRegex) == 0 {
		return cleaned
	}

	// We use named regexes here to extract the function argument key value pairs and convert this to valid json.
	// TODO: there might be responses where an object as a value is expected/required. This is currently not handled.
	args := make(map[string]string)

	agrsRegexKeyName := "key"
	agrsRegexValueName := "value"

	if functionConfig.ArgumentRegexKey != "" {
		agrsRegexKeyName = functionConfig.ArgumentRegexKey
	}
	if functionConfig.ArgumentRegexValue != "" {
		agrsRegexValueName = functionConfig.ArgumentRegexValue
	}

	for _, r := range functionConfig.ArgumentRegex {
		var respRegex = regexp.MustCompile(r)
		var nameRange []string = respRegex.SubexpNames()
		var keyIndex = slices.Index(nameRange, agrsRegexKeyName)
		var valueIndex = slices.Index(nameRange, agrsRegexValueName)
		matches := respRegex.FindAllStringSubmatch(functionArguments, -1)
		for _, match := range matches {
			args[match[keyIndex]] = match[valueIndex]
		}
	}

	jsonBytes, _ := json.Marshal(args)

	return string(jsonBytes)
}