diff --git a/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs index 4f05ee8c..dbaeb82d 100644 --- a/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs @@ -284,20 +284,28 @@ public class ChatCompletionProvider : IChatCompletion if (choice.FinishReason == ChatFinishReason.ToolCalls || choice.FinishReason == ChatFinishReason.FunctionCall) { - // 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)) + // The model may emit several tool calls in one response. The OpenAI SDK streams + // arguments as interleaved fragments PER CALL INDEX: the first fragment of a call + // carries the name/id and an empty arguments payload, later fragments carry only + // the argument text (FunctionName is null on them). We must accumulate fragments + // grouped by Index and must NOT pre-filter on FunctionName (that would drop the + // argument-carrying fragments and yield empty FunctionArgs). + // + // Old bug: string.Join over ALL fragments of ALL calls produced corrupted + // arguments (get_node({"node_id": "10"}{"maxDepth": 2})), which made every + // tool call fail with e.g. "node_id is required". + var groups = toolCalls .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())) + var namedGroup = groups.FirstOrDefault(g => g.Any(x => !string.IsNullOrEmpty(x.FunctionName))); + var namedUpdate = namedGroup?.FirstOrDefault(x => !string.IsNullOrEmpty(x.FunctionName)); + + var functionName = namedUpdate?.FunctionName; + var toolCallId = namedUpdate?.ToolCallId; + var functionArgument = namedGroup != null + ? string.Concat(namedGroup.Where(x => x.FunctionArgumentsUpdate != null).Select(x => x.FunctionArgumentsUpdate.ToString())) : string.Empty; #if DEBUG