fix(DeepSeekAI): accumulate streaming tool-call args per call index
When the model emits several parallel tool calls in one streaming response,
the OpenAI SDK interleaves argument fragments by call index. The old code
joined ALL fragments of ALL calls into a single FunctionArgs string, which
produced corrupted arguments like get_node({"node_id": "10"}{"maxDepth": 2})
and made every tool call fail (e.g. 'node_id is required').
Group tool-call updates by Index and concatenate fragments only within the
first call that has a function name; other parallel calls are ignored (same
as before), but the selected call now gets clean, valid JSON arguments.
Also fix DecodePatchString: DeepSeek sends "reasoning_content": null in the
final delta; the decoder returned the literal string 'null' which was
appended to the accumulated reasoning text. Return null for the JSON null
literal instead.
This commit is contained in:
parent
ce08fc132b
commit
9fe969a47c
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue