diff --git a/Directory.Packages.props b/Directory.Packages.props
index ee123555..015db501 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -45,7 +45,7 @@
-
+
diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/Conversation.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/Conversation.cs
index 675c0885..f95a0579 100644
--- a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/Conversation.cs
+++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/Conversation.cs
@@ -62,6 +62,10 @@ public class DialogElement
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Payload { get; set; }
+ [JsonPropertyName("reasoning_content")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? ReasoningContent { get; set; }
+
public DialogElement()
{
@@ -114,6 +118,10 @@ public class DialogMetaData
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? ToolCallId { get; set; }
+ [JsonPropertyName("reasoning_content")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? ReasoningContent { get; set; }
+
[JsonPropertyName("sender_id")]
public string? SenderId { get; set; }
diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs
index 90cc4488..dc3cc24b 100644
--- a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs
+++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs
@@ -71,6 +71,15 @@ public class RoleDialogModel : ITrackableMessage
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? FunctionArgs { get; set; }
+ ///
+ /// DeepSeek thinking mode: the chain-of-thought text returned by the model in
+ /// reasoning_content. Must be echoed back to the API in subsequent requests
+ /// when the conversation carries tools, otherwise DeepSeek answers 400.
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("reasoning_content")]
+ public string? ReasoningContent { get; set; }
+
///
/// Set this flag is in OnFunctionExecuting, if true, it won't be executed by InvokeFunction.
///
@@ -192,6 +201,7 @@ public class RoleDialogModel : ITrackableMessage
FunctionArgs = source.FunctionArgs,
FunctionName = source.FunctionName,
ToolCallId = source.ToolCallId,
+ ReasoningContent = source.ReasoningContent,
Indication = source.Indication,
PostbackFunctionName = source.PostbackFunctionName,
RichContent = source.RichContent,
diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs
index 9e927acb..b1f29925 100644
--- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs
+++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs
@@ -72,6 +72,7 @@ public class ConversationStorage : IConversationStorage
FunctionName = meta?.FunctionName,
FunctionArgs = meta?.FunctionArgs,
ToolCallId = meta?.ToolCallId,
+ ReasoningContent = dialog.ReasoningContent ?? meta?.ReasoningContent,
RichContent = richContent,
SecondaryContent = secondaryContent,
SecondaryRichContent = secondaryRichContent,
@@ -109,6 +110,7 @@ public class ConversationStorage : IConversationStorage
FunctionName = dialog.FunctionName,
FunctionArgs = dialog.FunctionArgs,
ToolCallId = dialog.ToolCallId,
+ ReasoningContent = dialog.ReasoningContent,
CreatedTime = dialog.CreatedAt
};
@@ -120,6 +122,7 @@ public class ConversationStorage : IConversationStorage
MetaData = meta,
Content = dialog.Content,
SecondaryContent = dialog.SecondaryContent,
+ ReasoningContent = dialog.ReasoningContent,
Payload = dialog.Payload
};
}
@@ -135,6 +138,7 @@ public class ConversationStorage : IConversationStorage
MessageLabel = dialog.MessageLabel,
SenderId = dialog.SenderId,
FunctionName = dialog.FunctionName,
+ ReasoningContent = dialog.ReasoningContent,
CreatedTime = dialog.CreatedAt
};
@@ -150,6 +154,7 @@ public class ConversationStorage : IConversationStorage
SecondaryContent = dialog.SecondaryContent,
RichContent = richContent,
SecondaryRichContent = secondaryRichContent,
+ ReasoningContent = dialog.ReasoningContent,
Payload = dialog.Payload
};
}
diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs
index e0175a70..d8c85853 100644
--- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs
+++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs
@@ -56,6 +56,7 @@ public partial class RoutingService
message.ToolCallId = response.ToolCallId;
message.FunctionName = response.FunctionName;
message.FunctionArgs = response.FunctionArgs;
+ message.ReasoningContent = response.ReasoningContent;
message.Indication = response.Indication;
message.CurrentAgentId = agent.Id;
message.IsStreaming = response.IsStreaming;
diff --git a/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs
index 6349b1ed..f81143e3 100644
--- a/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs
+++ b/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs
@@ -1,4 +1,5 @@
#pragma warning disable OPENAI001
+#pragma warning disable SCME0001
using BotSharp.Abstraction.Conversations.Enums;
using BotSharp.Abstraction.Files;
using BotSharp.Abstraction.Files.Models;
@@ -10,6 +11,7 @@ using BotSharp.Core.MessageHub;
using BotSharp.Plugin.DeepSeek.Providers;
using Microsoft.Extensions.Logging;
using OpenAI.Chat;
+using System.ClientModel.Primitives;
namespace BotSharp.Plugin.DeepSeekAI.Providers.Chat;
@@ -62,7 +64,8 @@ public class ChatCompletionProvider : IChatCompletion
ToolCallId = toolCall?.Id,
FunctionName = toolCall?.FunctionName,
FunctionArgs = toolCall?.FunctionArguments?.ToString(),
- RenderedInstruction = string.Join("\r\n", renderedInstructions)
+ RenderedInstruction = string.Join("\r\n", renderedInstructions),
+ ReasoningContent = ExtractReasoningContent(value)
};
// Somethings LLM will generate a function name with agent name.
@@ -78,6 +81,7 @@ public class ChatCompletionProvider : IChatCompletion
CurrentAgentId = agent.Id,
MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty,
RenderedInstruction = string.Join("\r\n", renderedInstructions),
+ ReasoningContent = ExtractReasoningContent(value),
Annotations = value.Annotations?.Select(x => new ChatAnnotation
{
Title = x.WebResourceTitle,
@@ -131,7 +135,8 @@ public class ChatCompletionProvider : IChatCompletion
var msg = new RoleDialogModel(AgentRole.Assistant, text)
{
CurrentAgentId = agent.Id,
- RenderedInstruction = string.Join("\r\n", renderedInstructions)
+ RenderedInstruction = string.Join("\r\n", renderedInstructions),
+ ReasoningContent = ExtractReasoningContent(value)
};
var tokenUsage = response?.Value?.Usage;
@@ -163,7 +168,8 @@ public class ChatCompletionProvider : IChatCompletion
ToolCallId = toolCall?.Id,
FunctionName = toolCall?.FunctionName,
FunctionArgs = toolCall?.FunctionArguments?.ToString(),
- RenderedInstruction = string.Join("\r\n", renderedInstructions)
+ RenderedInstruction = string.Join("\r\n", renderedInstructions),
+ ReasoningContent = ExtractReasoningContent(value)
};
// Somethings LLM will generate a function name with agent name.
@@ -183,6 +189,7 @@ public class ChatCompletionProvider : IChatCompletion
CurrentAgentId = agent.Id,
MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty,
RenderedInstruction = string.Join("\r\n", renderedInstructions),
+ ReasoningContent = ExtractReasoningContent(value),
Annotations = value.Annotations?.Select(x => new ChatAnnotation
{
Title = x.WebResourceTitle,
@@ -228,6 +235,7 @@ public class ChatCompletionProvider : IChatCompletion
using var textStream = new RealtimeTextStream();
var toolCalls = new List();
ChatTokenUsage? tokenUsage = null;
+ var reasoningContentBuilder = new System.Text.StringBuilder();
var responseMessage = new RoleDialogModel(AgentRole.Assistant, string.Empty)
{
@@ -244,6 +252,14 @@ public class ChatCompletionProvider : IChatCompletion
toolCalls.AddRange(choice.ToolCallUpdates);
}
+ // DeepSeek thinking mode streams the chain-of-thought in `reasoning_content`
+ // (delta level). OpenAI SDK 2.12 keeps it as an unknown property in the JsonPatch.
+ var reasoningDelta = ReadStreamingReasoningContent(choice);
+ if (!string.IsNullOrEmpty(reasoningDelta))
+ {
+ reasoningContentBuilder.Append(reasoningDelta);
+ }
+
if (!choice.ContentUpdate.IsNullOrEmpty())
{
var text = choice.ContentUpdate[0]?.Text ?? string.Empty;
@@ -284,7 +300,8 @@ public class ChatCompletionProvider : IChatCompletion
MessageId = messageId,
ToolCallId = toolCallId,
FunctionName = functionName,
- FunctionArgs = functionArgument
+ FunctionArgs = functionArgument,
+ ReasoningContent = reasoningContentBuilder.Length > 0 ? reasoningContentBuilder.ToString() : null
};
}
else if (choice.FinishReason.HasValue)
@@ -296,7 +313,8 @@ public class ChatCompletionProvider : IChatCompletion
{
CurrentAgentId = agent.Id,
MessageId = messageId,
- IsStreaming = true
+ IsStreaming = true,
+ ReasoningContent = reasoningContentBuilder.Length > 0 ? reasoningContentBuilder.ToString() : null
};
}
}
@@ -332,6 +350,67 @@ public class ChatCompletionProvider : IChatCompletion
_model = model;
}
+ ///
+ /// Extracts reasoning_content (DeepSeek thinking mode) from a non-streaming
+ /// . The OpenAI SDK keeps the unknown property in the
+ /// message's JsonPatch, addressable at $.choices[0].message.reasoning_content.
+ ///
+ private static string? ExtractReasoningContent(ChatCompletion value)
+ {
+ if (value == null)
+ {
+ return null;
+ }
+
+ try
+ {
+ return DecodePatchString(value.Patch.GetJson("$.choices[0].message.reasoning_content"u8));
+ }
+ catch (Exception ex)
+ {
+ // Never fail the chat because of a reasoning_content extraction issue.
+ System.Diagnostics.Debug.WriteLine($"ExtractReasoningContent failed: {ex.Message}");
+ }
+
+ return null;
+ }
+
+ ///
+ /// Extracts one streaming chunk of reasoning_content from a
+ /// . Unknown delta properties land in the
+ /// update's JsonPatch under $.choices[0].delta.reasoning_content.
+ ///
+ private static string? ReadStreamingReasoningContent(StreamingChatCompletionUpdate choice)
+ {
+ if (choice == null)
+ {
+ return null;
+ }
+
+ try
+ {
+ return DecodePatchString(choice.Patch.GetJson("$.choices[0].delta.reasoning_content"u8));
+ }
+ catch (Exception ex)
+ {
+ // Never fail the chat because of a reasoning_content extraction issue.
+ System.Diagnostics.Debug.WriteLine($"ReadStreamingReasoningContent failed: {ex.Message}");
+ }
+
+ return null;
+ }
+
+ private static string? DecodePatchString(BinaryData data)
+ {
+ var bytes = data.ToArray();
+ if (bytes.Length >= 2 && bytes[0] == (byte)'"' && bytes[^1] == (byte)'"')
+ {
+ return System.Text.Encoding.UTF8.GetString(bytes, 1, bytes.Length - 2);
+ }
+
+ return System.Text.Encoding.UTF8.GetString(bytes);
+ }
+
protected (string, IEnumerable, ChatCompletionOptions) PrepareOptions(Agent agent, List conversations)
{
var agentService = _services.GetRequiredService();
@@ -394,10 +473,13 @@ public class ChatCompletionProvider : IChatCompletion
{
if (message.Role == AgentRole.Function)
{
- messages.Add(new AssistantChatMessage(new List
+ var assistantToolMsg = new AssistantChatMessage(new List
{
ChatToolCall.CreateFunctionToolCall(message.ToolCallId.IfNullOrEmptyAs(message.FunctionName), message.FunctionName, BinaryData.FromString(message.FunctionArgs ?? "{}"))
- }));
+ });
+ EchoReasoningContent(assistantToolMsg, message);
+
+ messages.Add(assistantToolMsg);
messages.Add(new ToolChatMessage(message.ToolCallId.IfNullOrEmptyAs(message.FunctionName), message.LlmContent));
}
@@ -423,7 +505,10 @@ public class ChatCompletionProvider : IChatCompletion
{
CollectMessageContentParts(contentParts, message.Files, imageDetailLevel);
}
- messages.Add(new AssistantChatMessage(contentParts));
+ var assistantMsg = new AssistantChatMessage(contentParts);
+ EchoReasoningContent(assistantMsg, message);
+
+ messages.Add(assistantMsg);
}
}
@@ -431,6 +516,24 @@ public class ChatCompletionProvider : IChatCompletion
return (prompt, messages, options);
}
+ ///
+ /// DeepSeek requires the reasoning_content of previous assistant messages to be
+ /// echoed back in every follow-up request that carries tools (otherwise 400
+ /// "The reasoning_content ... must be passed back"). The OpenAI SDK serializes it via
+ /// the message's JsonPatch (experimental SCME0001, suppressed by the file-level pragma).
+ ///
+ private static void EchoReasoningContent(AssistantChatMessage assistantMessage, RoleDialogModel dialog)
+ {
+ var reasoning = dialog.ReasoningContent;
+ if (string.IsNullOrWhiteSpace(reasoning) || assistantMessage == null)
+ {
+ return;
+ }
+
+ ref var patch = ref assistantMessage.Patch;
+ patch.Set("$.reasoning_content"u8, reasoning);
+ }
+
private void CollectMessageContentParts(List contentParts, List files, ChatImageDetailLevel imageDetailLevel)
{
foreach (var file in files)
diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Image/ImageCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Image/ImageCompletionProvider.cs
index 919054b1..acc8391f 100644
--- a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Image/ImageCompletionProvider.cs
+++ b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Image/ImageCompletionProvider.cs
@@ -105,10 +105,10 @@ public partial class ImageCompletionProvider : IImageCompletion
switch (value)
{
case "low":
- retQuality = GeneratedImageQuality.Low;
+ retQuality = GeneratedImageQuality.LowQuality;
break;
case "medium":
- retQuality = GeneratedImageQuality.Medium;
+ retQuality = GeneratedImageQuality.MediumQuality;
break;
case "high":
retQuality = GeneratedImageQuality.High;