diff --git a/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs index f81143e3..4f05ee8c 100644 --- a/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs @@ -284,11 +284,21 @@ public class ChatCompletionProvider : IChatCompletion if (choice.FinishReason == ChatFinishReason.ToolCalls || choice.FinishReason == ChatFinishReason.FunctionCall) { - var meta = toolCalls.FirstOrDefault(x => !string.IsNullOrEmpty(x.FunctionName)); - var functionName = meta?.FunctionName; - var toolCallId = meta?.ToolCallId; - var args = toolCalls.Where(x => x.FunctionArgumentsUpdate != null).Select(x => x.FunctionArgumentsUpdate.ToString()).ToList(); - var functionArgument = string.Join(string.Empty, args); + // The model may emit several tool calls in one response. The OpenAI SDK streams each + // call's arguments as interleaved fragments per call index, so we must accumulate them + // PER CALL (grouped by index), not concatenate every fragment of every call together + // (that would corrupt arguments, e.g. get_node({"node_id": "10"}{"maxDepth": 2})). + var grouped = toolCalls + .Where(x => !string.IsNullOrEmpty(x.FunctionName)) + .GroupBy(x => x.Index) + .OrderBy(g => g.Key) + .ToList(); + + var functionName = grouped.Count > 0 ? grouped[0].First().FunctionName : null; + var toolCallId = grouped.Count > 0 ? grouped[0].First().ToolCallId : null; + var functionArgument = grouped.Count > 0 + ? string.Concat(grouped[0].Where(x => x.FunctionArgumentsUpdate != null).Select(x => x.FunctionArgumentsUpdate.ToString())) + : string.Empty; #if DEBUG _logger.LogCritical($"Tool Call (id: {toolCallId}) => {functionName}({functionArgument})"); @@ -402,7 +412,20 @@ public class ChatCompletionProvider : IChatCompletion private static string? DecodePatchString(BinaryData data) { + if (data == null) + { + return null; + } + var bytes = data.ToArray(); + + // JSON null literal (DeepSeek sends "reasoning_content": null in the final delta). + if (bytes.Length == 4 && bytes[0] == (byte)'n' && bytes[1] == (byte)'u' && bytes[2] == (byte)'l' && bytes[3] == (byte)'l') + { + return null; + } + + // Quoted JSON string. if (bytes.Length >= 2 && bytes[0] == (byte)'"' && bytes[^1] == (byte)'"') { return System.Text.Encoding.UTF8.GetString(bytes, 1, bytes.Length - 2);