diff --git a/TelegramSearchBot.Common/Model/AI/LlmContinuationSnapshot.cs b/TelegramSearchBot.Common/Model/AI/LlmContinuationSnapshot.cs
index ddec625f..5c678fd3 100644
--- a/TelegramSearchBot.Common/Model/AI/LlmContinuationSnapshot.cs
+++ b/TelegramSearchBot.Common/Model/AI/LlmContinuationSnapshot.cs
@@ -16,6 +16,12 @@ public class SerializedChatMessage {
/// The text content of the message
///
public string Content { get; set; } = null!;
+
+ ///
+ /// The reasoning content for thinking mode models (e.g., Kimi-thinking-preview, QwQ).
+ /// This field must be passed back to the API in subsequent requests to avoid HTTP 400 errors.
+ ///
+ public string? ReasoningContent { get; set; }
}
///
diff --git a/TelegramSearchBot.LLM/Service/AI/LLM/OpenAIService.cs b/TelegramSearchBot.LLM/Service/AI/LLM/OpenAIService.cs
index 8044cda4..ab34380a 100644
--- a/TelegramSearchBot.LLM/Service/AI/LLM/OpenAIService.cs
+++ b/TelegramSearchBot.LLM/Service/AI/LLM/OpenAIService.cs
@@ -967,6 +967,7 @@ private async IAsyncEnumerable ExecWithNativeToolCallingAsync(
if (cancellationToken.IsCancellationRequested) throw new TaskCanceledException();
var contentBuilder = new StringBuilder();
+ var reasoningContentBuilder = new StringBuilder();
var toolCallAccumulators = new Dictionary();
ChatFinishReason? finishReason = null;
@@ -985,6 +986,12 @@ private async IAsyncEnumerable ExecWithNativeToolCallingAsync(
}
}
+ // Accumulate reasoning content for thinking mode models (e.g., Kimi-thinking-preview)
+ var reasoningUpdate = GetStreamingReasoningContent(update);
+ if (!string.IsNullOrEmpty(reasoningUpdate)) {
+ reasoningContentBuilder.Append(reasoningUpdate);
+ }
+
// Accumulate tool call updates
foreach (var toolCallUpdate in update.ToolCallUpdates ?? Enumerable.Empty()) {
int index = toolCallUpdate.Index;
@@ -1006,6 +1013,7 @@ private async IAsyncEnumerable ExecWithNativeToolCallingAsync(
}
string responseText = contentBuilder.ToString().Trim();
+ string reasoningContent = reasoningContentBuilder.ToString().Trim();
// Check if this is a tool call response
if (finishReason == ChatFinishReason.ToolCalls && toolCallAccumulators.Any()) {
@@ -1025,6 +1033,10 @@ private async IAsyncEnumerable ExecWithNativeToolCallingAsync(
if (!string.IsNullOrWhiteSpace(responseText)) {
assistantMessage = new AssistantChatMessage(chatToolCalls) { Content = { ChatMessageContentPart.CreateTextPart(responseText) } };
}
+ // Set reasoning content for thinking mode models
+ if (!string.IsNullOrEmpty(reasoningContent)) {
+ SetAssistantReasoningContent(assistantMessage, reasoningContent);
+ }
providerHistory.Add(assistantMessage);
var toolIndicators = new StringBuilder();
@@ -1066,7 +1078,11 @@ private async IAsyncEnumerable ExecWithNativeToolCallingAsync(
} else {
// Not a tool call - regular text response
if (!string.IsNullOrWhiteSpace(responseText)) {
- providerHistory.Add(new AssistantChatMessage(responseText));
+ var assistantMsg = new AssistantChatMessage(responseText);
+ if (!string.IsNullOrEmpty(reasoningContent)) {
+ SetAssistantReasoningContent(assistantMsg, reasoningContent);
+ }
+ providerHistory.Add(assistantMsg);
}
yield break;
}
@@ -1328,6 +1344,7 @@ public static List SerializeProviderHistory(List SerializeProviderHistory(List p.Text) ?? Enumerable.Empty());
+ // Try to get reasoning content from the assistant message
+ // OpenAI SDK stores reasoning content in a separate property
+ reasoningContent = GetAssistantReasoningContent(assistantMsg);
} else if (msg is UserChatMessage userMsg) {
role = "user";
content = string.Join("", userMsg.Content?.Select(p => p.Text) ?? Enumerable.Empty());
@@ -1343,11 +1363,61 @@ public static List SerializeProviderHistory(List
+ /// Extract reasoning_content from AssistantChatMessage if available.
+ /// For thinking mode models, the reasoning process is returned separately.
+ ///
+ private static string? GetAssistantReasoningContent(AssistantChatMessage assistantMsg) {
+ // Try to access reasoning content - OpenAI Chat SDK may store it in various ways
+ // The reasoning_content is typically available via reflection or specific properties
+ try {
+ // Check for Reasoning property via reflection
+ var reasoningProp = assistantMsg.GetType().GetProperty("Reasoning");
+ if (reasoningProp != null) {
+ var value = reasoningProp.GetValue(assistantMsg);
+ if (value is string reasoning && !string.IsNullOrEmpty(reasoning)) {
+ return reasoning;
+ }
+ }
+ } catch {
+ // Reflection failed, return null
+ }
+ return null;
+ }
+
+ ///
+ /// Extract reasoning_content from streaming update for thinking mode models.
+ /// Uses reflection to access SDK internals.
+ ///
+ private static string? GetStreamingReasoningContent(StreamingChatCompletionUpdate update) {
+ try {
+ // Try ReasoningContentUpdate property (OpenAI SDK for thinking models)
+ var reasoningProp = update.GetType().GetProperty("ReasoningContentUpdate");
+ if (reasoningProp != null) {
+ var value = reasoningProp.GetValue(update);
+ if (value is string reasoning && !string.IsNullOrEmpty(reasoning)) {
+ return reasoning;
+ }
+ }
+ // Fallback: try Reasoning property
+ var fallbackProp = update.GetType().GetProperty("Reasoning");
+ if (fallbackProp != null) {
+ var value = fallbackProp.GetValue(update);
+ if (value is string fallback && !string.IsNullOrEmpty(fallback)) {
+ return fallback;
+ }
+ }
+ } catch {
+ // Reflection failed
+ }
+ return null;
+ }
+
///
/// Deserialize portable format back to OpenAI ChatMessage list.
///
@@ -1361,7 +1431,12 @@ public static List DeserializeProviderHistory(List DeserializeProviderHistory(List
+ /// Set reasoning_content on AssistantChatMessage for thinking mode models.
+ /// Uses reflection since OpenAI SDK doesn't have a public setter.
+ ///
+ private static void SetAssistantReasoningContent(AssistantChatMessage msg, string reasoningContent) {
+ try {
+ var prop = msg.GetType().GetProperty("Reasoning");
+ if (prop != null && prop.CanWrite) {
+ prop.SetValue(msg, reasoningContent);
+ }
+ } catch {
+ // Reflection failed, ignore
+ }
+ }
+
public async Task GenerateEmbeddingsAsync(string text, string modelName, LLMChannel channel) {