@@ -104,7 +104,103 @@ static void common_chat_parse_generic(common_chat_msg_parser & builder) {
104104 }
105105}
106106
107- static void common_chat_parse_deepseek_r1 (common_chat_msg_parser & builder) {
107+ // Helper function from original llama.cpp
108+ static std::string wrap_code_as_arguments (common_chat_msg_parser & builder, const std::string & code) {
109+ std::string arguments;
110+ if (builder.is_partial ()) {
111+ arguments = (json {{" code" , code + builder.healing_marker ()}}).dump ();
112+ auto idx = arguments.find (builder.healing_marker ());
113+ if (idx != std::string::npos) {
114+ arguments.resize (idx);
115+ }
116+ } else {
117+ arguments = (json {{" code" , code}}).dump ();
118+ }
119+ return arguments;
120+ }
121+
122+ // Forward declaration
123+ static void parse_deepseek_r1_tools_array (common_chat_msg_parser & builder);
124+ static void parse_deepseek_r1_xml_wrapped (common_chat_msg_parser & builder);
125+
126+ // Helper function from original llama.cpp for parsing JSON tool calls
127+ static void parse_json_tool_calls (
128+ common_chat_msg_parser & builder,
129+ const std::optional<common_regex> & block_open,
130+ const std::optional<common_regex> & function_regex_start_only,
131+ const std::optional<common_regex> & function_regex,
132+ const common_regex & close_regex,
133+ const std::optional<common_regex> & block_close,
134+ bool allow_raw_python = false ,
135+ const std::function<std::string(const common_chat_msg_parser::find_regex_result & fres)> & get_function_name = nullptr) {
136+
137+ auto parse_tool_calls = [&]() {
138+ size_t from = std::string::npos;
139+ auto first = true ;
140+ while (true ) {
141+ auto res = function_regex_start_only && first
142+ ? builder.try_consume_regex (*function_regex_start_only)
143+ : function_regex
144+ ? builder.try_find_regex (*function_regex, from)
145+ : std::nullopt ;
146+ if (res) {
147+ std::string name;
148+ if (get_function_name) {
149+ name = get_function_name (*res);
150+ } else {
151+ if (res->groups .size () < 2 ) {
152+ from = res->groups [0 ].begin + 1 ;
153+ continue ;
154+ }
155+ name = builder.str (res->groups [1 ]);
156+ }
157+ first = false ;
158+ if (name.empty ()) {
159+ // get_function_name signalled us that we should skip this match and treat it as content.
160+ from = res->groups [0 ].begin + 1 ;
161+ continue ;
162+ }
163+ from = std::string::npos;
164+
165+ auto maybe_raw_python = name == " python" && allow_raw_python;
166+ if (builder.input ()[builder.pos ()] == ' {' || !maybe_raw_python) {
167+ if (auto arguments = builder.try_consume_json_with_dumped_args ({{}})) {
168+ if (!builder.add_tool_call (name, " " , arguments->value ) || arguments->is_partial ) {
169+ throw common_chat_msg_partial_exception (" incomplete tool call" );
170+ }
171+ builder.try_consume_regex (close_regex);
172+ }
173+ continue ;
174+ }
175+ if (maybe_raw_python) {
176+ auto arguments = wrap_code_as_arguments (builder, builder.consume_rest ());
177+ if (!builder.add_tool_call (name, " " , arguments)) {
178+ throw common_chat_msg_partial_exception (" incomplete tool call" );
179+ }
180+ return ;
181+ }
182+ throw common_chat_msg_partial_exception (" incomplete tool call" );
183+ }
184+ break ;
185+ }
186+ if (block_close) {
187+ builder.try_consume_regex (*block_close);
188+ }
189+ builder.consume_spaces ();
190+ builder.add_content (builder.consume_rest ());
191+ };
192+ if (block_open) {
193+ if (auto res = builder.try_find_regex (*block_open)) {
194+ parse_tool_calls ();
195+ } else {
196+ builder.add_content (builder.consume_rest ());
197+ }
198+ } else {
199+ parse_tool_calls ();
200+ }
201+ }
202+
203+ void common_chat_parse_deepseek_r1 (common_chat_msg_parser & builder) {
108204 builder.try_parse_reasoning (" <think>" , " </think>" );
109205 if (!builder.syntax ().enable_tool_calls ) {
110206 builder.add_content (builder.consume_rest ());
@@ -113,25 +209,159 @@ static void common_chat_parse_deepseek_r1(common_chat_msg_parser & builder) {
113209
114210 static const common_regex tool_calls_begin (" (?:<|tool▁calls▁begin|>|<|tool_calls_begin|>|<|tool calls begin|>|<|tool\\\\ _calls\\\\ _begin|>|<|tool▁calls|>)" );
115211 static const common_regex tool_calls_end (" <|tool▁calls▁end|>" );
212+ // Primary regex for correct format with separator
116213 static const common_regex function_regex (" (?:<|tool▁call▁begin|>)?function<|tool▁sep|>([^\n ]+)\n ```json\n " );
214+ // Fallback regex for format without separator (some models generate this)
215+ static const common_regex function_regex_no_sep (" (?:<|tool▁call▁begin|>)?function<([^>]+)>\n ```json\n " );
216+ // Third regex for new format: just "function" with no markers
217+ static const common_regex function_regex_simple (" function\n ```json\n " );
117218 static const common_regex close_regex (" ```[\\ s\\ r\\ n]*<|tool▁call▁end|>" );
219+ static const common_regex close_regex_simple (" ```" ); // For simple format without end markers
118220
119- // Simplified tool calls parsing for DEEPSEEK_R1
120- if (auto res = builder.try_find_regex (tool_calls_begin)) {
121- while (auto func_res = builder.try_find_regex (function_regex)) {
122- auto function_name = builder.str (func_res->groups [1 ]);
123- auto args_json = builder.try_consume_json ();
124- if (args_json) {
125- builder.add_tool_call (function_name, " " , args_json->json .dump ());
126- builder.try_consume_regex (close_regex);
127- } else {
128- throw common_chat_msg_partial_exception (" incomplete tool call JSON" );
221+ // Check for the new tools array format first (no DeepSeek markers)
222+ auto original_pos = builder.pos ();
223+
224+ // First, try the tools array format for content like "function\n```json\n{"tools": [...]}"
225+ if (builder.try_find_regex (function_regex_simple)) {
226+ builder.move_to (original_pos);
227+ try {
228+ parse_deepseek_r1_tools_array (builder);
229+ return ; // Success, we're done
230+ } catch (const common_chat_msg_partial_exception&) {
231+ // Fall through to try standard DeepSeek patterns
232+ }
233+ }
234+
235+ // If tools array format didn't work, try XML-wrapped format
236+ builder.move_to (original_pos);
237+ try {
238+ parse_deepseek_r1_xml_wrapped (builder);
239+ return ; // Success, we're done
240+ } catch (const common_chat_msg_partial_exception&) {
241+ // Fall through to try standard DeepSeek patterns
242+ }
243+
244+ // If XML wrapper format didn't work, try standard DeepSeek patterns
245+ builder.move_to (original_pos);
246+ try {
247+ parse_json_tool_calls (
248+ builder,
249+ /* block_open= */ tool_calls_begin,
250+ /* function_regex_start_only= */ std::nullopt ,
251+ function_regex,
252+ close_regex,
253+ tool_calls_end);
254+ } catch (const common_chat_msg_partial_exception&) {
255+ // If primary regex fails and we're not in partial mode, try fallback regex
256+ if (!builder.is_partial ()) {
257+ builder.move_to (original_pos);
258+ try {
259+ parse_json_tool_calls (
260+ builder,
261+ /* block_open= */ tool_calls_begin,
262+ /* function_regex_start_only= */ std::nullopt ,
263+ function_regex_no_sep,
264+ close_regex,
265+ tool_calls_end);
266+ } catch (const common_chat_msg_partial_exception&) {
267+ // Try the simple format without markers as final fallback
268+ builder.move_to (original_pos);
269+ parse_json_tool_calls (
270+ builder,
271+ /* block_open= */ std::nullopt ,
272+ /* function_regex_start_only= */ std::nullopt ,
273+ function_regex_simple,
274+ close_regex_simple,
275+ std::nullopt );
129276 }
277+ } else {
278+ throw ; // Re-throw for partial mode
130279 }
131- builder.try_consume_regex (tool_calls_end);
132- builder.add_content (builder.consume_rest ());
280+ }
281+ }
282+
283+ // Parse DeepSeek R1 tools array format following original llama.cpp parse_prefixed_json_tool_call_array pattern
284+ static void parse_deepseek_r1_tools_array (common_chat_msg_parser & builder) {
285+ static const common_regex prefix (" function\n ```json\n " );
286+
287+
288+ if (auto res = builder.try_find_regex (prefix)) {
289+ // Parse JSON and manually process tools array to convert arguments to strings
290+ auto json_result = builder.try_consume_json ();
291+ if (!json_result) {
292+ throw common_chat_msg_partial_exception (" invalid JSON" );
293+ }
294+
295+
296+ // DeepSeek R1 format has "tools" array, manually process each tool
297+ if (json_result->json .contains (" tools" ) && json_result->json .at (" tools" ).is_array ()) {
298+
299+ // Manually create tool calls array with string arguments (following original pattern)
300+ json tools_with_dumped_args = json::array ();
301+ for (const auto & tool : json_result->json .at (" tools" )) {
302+ if (tool.contains (" name" ) && tool.contains (" arguments" )) {
303+ json formatted_tool;
304+ formatted_tool[" name" ] = tool.at (" name" );
305+ // Convert arguments object to string (this is what consume_json_with_dumped_args does)
306+ formatted_tool[" arguments" ] = tool.at (" arguments" ).dump ();
307+ tools_with_dumped_args.push_back (formatted_tool);
308+ }
309+ }
310+
311+
312+ if (!builder.add_tool_calls (tools_with_dumped_args) || !json_result->healing_marker .marker .empty ()) {
313+ throw common_chat_msg_partial_exception (" incomplete tool call array" );
314+ }
315+ } else {
316+ throw common_chat_msg_partial_exception (" tools key not found or not array" );
317+ }
318+
319+ // Consume closing ```
320+ builder.try_consume_regex (common_regex (" ```" ));
133321 } else {
134- builder.add_content (builder.consume_rest ());
322+ throw common_chat_msg_partial_exception (" function prefix not found" );
323+ }
324+ }
325+
326+ // Parse DeepSeek R1 XML-wrapped format following original Hermes-2-Pro pattern
327+ static void parse_deepseek_r1_xml_wrapped (common_chat_msg_parser & builder) {
328+
329+ // Pattern for: <tool_call>\nfunction</think>FunctionName\n```json\n{...}\n```\n</tool_call>
330+ static const common_regex xml_pattern (
331+ " <tool_call>\\ s*" // Opening XML tag
332+ " function</think>([^\\ n]+)" // Function name after "function</think>"
333+ " \\ s*```json\\ s*" // JSON block start
334+ );
335+
336+ if (auto res = builder.try_find_regex (xml_pattern)) {
337+
338+ // Extract function name from capture group
339+ std::string function_name = builder.str (res->groups [1 ]);
340+
341+ // Parse JSON arguments
342+ auto json_result = builder.try_consume_json ();
343+ if (!json_result) {
344+ throw common_chat_msg_partial_exception (" invalid JSON in XML wrapper" );
345+ }
346+
347+
348+ // Create single tool call following original pattern
349+ json tool_call;
350+ tool_call[" name" ] = function_name;
351+ tool_call[" arguments" ] = json_result->json .dump (); // Convert to string
352+
353+ json tool_calls_array = json::array ();
354+ tool_calls_array.push_back (tool_call);
355+
356+
357+ if (!builder.add_tool_calls (tool_calls_array) || !json_result->healing_marker .marker .empty ()) {
358+ throw common_chat_msg_partial_exception (" incomplete XML wrapped tool call" );
359+ }
360+
361+ // Consume closing ```\n</tool_call>
362+ builder.try_consume_regex (common_regex (" ```\\ s*</tool_call>" ));
363+ } else {
364+ throw common_chat_msg_partial_exception (" XML wrapper pattern not found" );
135365 }
136366}
137367
0 commit comments