From bbbcdddc910fb8db401da72cd474ae5de9ac341b Mon Sep 17 00:00:00 2001
From: Peer Hogeterp <20603780+peerschuett@users.noreply.github.com>
Date: Thu, 10 Sep 2026 15:37:43 +0200
Subject: [PATCH 1/3] Add per-answer thinking disclosure
---
.../Assistants/I18N/allTexts.lua | 9 ++
.../Chat/ContentBlockComponent.razor | 25 ++++
.../Chat/ContentBlockComponent.razor.cs | 14 +-
app/MindWork AI Studio/Chat/ContentText.cs | 140 +++++++++++++++++-
.../plugin.lua | 9 ++
.../plugin.lua | 9 ++
.../Provider/Anthropic/AnthropicResponse.cs | 9 +-
.../Anthropic/AnthropicToolCallingAdapter.cs | 3 +-
.../Provider/Anthropic/Delta.cs | 3 +-
.../Provider/Anthropic/ResponseStreamLine.cs | 11 +-
.../Provider/BaseProvider.cs | 8 +-
.../Provider/ContentStreamChunk.cs | 9 +-
.../Provider/Fireworks/Delta.cs | 12 +-
.../Provider/Fireworks/ResponseStreamLine.cs | 4 +-
.../OpenAI/AssistantToolCallMessage.cs | 6 +
.../Provider/OpenAI/ChatCompletionDelta.cs | 9 ++
.../OpenAI/ChatCompletionDeltaStreamLine.cs | 4 +-
.../OpenAI/ChatCompletionResponseMessage.cs | 6 +
.../ChatCompletionToolCallingAdapter.cs | 5 +-
.../OpenAI/ResponsesDeltaStreamLine.cs | 8 +-
.../Provider/OpenAI/ResponsesResponse.cs | 5 +
.../OpenAI/ResponsesToolCallingAdapter.cs | 3 +-
.../Provider/OpenAI/ThinkingContent.cs | 46 ++++++
.../Provider/Perplexity/Delta.cs | 12 +-
.../Provider/Perplexity/ResponseStreamLine.cs | 4 +-
.../Tools/AIJobs/AIJobService.cs | 10 +-
.../Harness/ToolCallingLoop.cs | 10 +-
.../Harness/ToolCallingRound.cs | 3 +-
.../wwwroot/changelog/v26.9.1.md | 1 +
29 files changed, 360 insertions(+), 37 deletions(-)
create mode 100644 app/MindWork AI Studio/Provider/OpenAI/ThinkingContent.cs
diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
index 82e9864fa..499994b75 100644
--- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
+++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
@@ -3220,6 +3220,12 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CHATROLEEXTENSIONS::T3768991250"] = "User"
-- AI
UI_TEXT_CONTENT["AISTUDIO::CHAT::CHATROLEEXTENSIONS::T601166687"] = "AI"
+-- Show thinking
+UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1121977572"] = "Show thinking"
+
+-- Thinking
+UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1182941917"] = "Thinking"
+
-- Edit Message
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1183581066"] = "Edit Message"
@@ -3301,6 +3307,9 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3816336467"] = "Blocked
-- Do you really want to regenerate this message?
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3878878761"] = "Do you really want to regenerate this message?"
+-- Hide thinking
+UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3979442719"] = "Hide thinking"
+
-- Remove Message
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4070211974"] = "Remove Message"
diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor
index edab5ac68..95b450b73 100644
--- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor
+++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor
@@ -31,6 +31,23 @@
}
+ @if (this.HasThinking)
+ {
+
+
+
+
+ @T("Thinking")
+
+
+
+
+ }
@@ -193,6 +210,14 @@
}
+ @if (this.HasThinking && this.showThinking)
+ {
+
+ @T("Thinking")
+ @textContent.Thinking
+
+ }
+
if (textContent.InitialRemoteWait)
{
diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs
index b2ec2babb..84fdce113 100644
--- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs
+++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs
@@ -127,6 +127,7 @@ public partial class ContentBlockComponent : MSGComponentBase
private bool hasActiveMathContainer;
private bool isDisposed;
private bool showToolTrace;
+ private bool showThinking;
private readonly HashSet expandedToolInvocations = [];
///
@@ -139,6 +140,10 @@ public partial class ContentBlockComponent : MSGComponentBase
///
private bool CanExport => this.Content is { InitialRemoteWait: false, IsStreaming: false } && this.Content.TryGetMarkdownText(out _);
+ private bool HasThinking => this.Role is ChatRole.AI &&
+ this.Content is ContentText { Thinking: var thinking } &&
+ !string.IsNullOrWhiteSpace(thinking);
+
///
/// The tables this block holds so that the export menu can offer each of them.
///
@@ -305,11 +310,14 @@ private int CreateRenderHash()
var textValue = text.Text;
hash.Add(textValue.Length);
hash.Add(textValue.GetHashCode(StringComparison.Ordinal));
+ hash.Add(text.Thinking.Length);
+ hash.Add(text.Thinking.GetHashCode(StringComparison.Ordinal));
hash.Add(text.Sources.Count);
hash.Add(text.ToolInvocations.Count);
hash.Add(text.ToolRuntimeStatus.IsRunning);
hash.Add(text.ToolRuntimeStatus.Message);
hash.Add(this.showToolTrace);
+ hash.Add(this.showThinking);
hash.Add(this.expandedToolInvocations.Count);
foreach (var expandedInvocation in this.expandedToolInvocations.Order())
hash.Add(expandedInvocation);
@@ -380,6 +388,10 @@ private string GetToolTraceTooltip()
private void ToggleToolTrace() => this.showToolTrace = !this.showToolTrace;
+ private void ToggleThinking() => this.showThinking = !this.showThinking;
+
+ private string GetThinkingTooltip() => this.showThinking ? this.T("Hide thinking") : this.T("Show thinking");
+
private bool IsToolInvocationExpanded(int order) => this.expandedToolInvocations.Contains(order);
private void ToggleToolInvocation(int order)
@@ -832,4 +844,4 @@ protected override async ValueTask DisposeResourcesAsync()
await this.DisposeMathContainerIfNeededAsync();
}
-}
\ No newline at end of file
+}
diff --git a/app/MindWork AI Studio/Chat/ContentText.cs b/app/MindWork AI Studio/Chat/ContentText.cs
index 0640b6582..c4f37315a 100644
--- a/app/MindWork AI Studio/Chat/ContentText.cs
+++ b/app/MindWork AI Studio/Chat/ContentText.cs
@@ -16,6 +16,9 @@ namespace AIStudio.Chat;
///
public sealed class ContentText : IContent
{
+ private const string OPEN_THINK_TAG = "";
+ private const string CLOSE_THINK_TAG = "";
+
private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger();
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ContentText).Namespace, nameof(ContentText));
@@ -26,6 +29,12 @@ public sealed class ContentText : IContent
///
private static readonly TimeSpan MIN_TIME = TimeSpan.FromSeconds(3);
+ [JsonIgnore]
+ private ThinkTagStreamState thinkTagStreamState;
+
+ [JsonIgnore]
+ private readonly StringBuilder thinkTagBuffer = new();
+
#region Implementation of IContent
///
@@ -122,11 +131,8 @@ await Task.Run(async () =>
this.InitialRemoteWait = false;
this.IsStreaming = true;
- // Add the response to the text:
- this.Text += contentStreamChunk;
-
- // Merge the sources:
- this.Sources.MergeSources(contentStreamChunk.Sources);
+ // Add the response to the content:
+ this.ApplyStreamChunk(contentStreamChunk);
// Notify the UI that the content has changed,
// depending on the energy saving mode:
@@ -160,7 +166,7 @@ await Task.Run(async () =>
}
finally
{
- this.Text = this.Text.RemoveThinkTags().Trim();
+ this.FinalizeStreamContent();
// Inform the UI that the streaming is done:
await this.StreamingDone();
@@ -253,6 +259,7 @@ private async Task CheckSelectedModelAvailability(IProvider provider, Mode
public IContent DeepClone() => new ContentText
{
Text = this.Text,
+ Thinking = this.Thinking,
InitialRemoteWait = this.InitialRemoteWait,
IsStreaming = this.IsStreaming,
Sources = [..this.Sources],
@@ -407,4 +414,123 @@ public async Task PrepareTextContentForAI()
/// The text content.
///
public string Text { get; set; } = string.Empty;
-}
\ No newline at end of file
+
+ ///
+ /// Human-readable thinking content exposed by the provider.
+ ///
+ public string Thinking { get; set; } = string.Empty;
+
+ ///
+ /// Applies one provider stream chunk to this content.
+ ///
+ public void ApplyStreamChunk(ContentStreamChunk chunk)
+ {
+ if (!string.IsNullOrEmpty(chunk.Thinking))
+ this.Thinking += chunk.Thinking;
+
+ if (!string.IsNullOrEmpty(chunk.Content))
+ this.ApplyAnswerChunk(chunk.Content);
+
+ this.Sources.MergeSources(chunk.Sources);
+ }
+
+ ///
+ /// Completes parsing of provider content and normalizes the displayed values.
+ ///
+ public void FinalizeStreamContent()
+ {
+ switch (this.thinkTagStreamState)
+ {
+ case ThinkTagStreamState.UNDECIDED:
+ this.Text += this.thinkTagBuffer;
+ break;
+
+ case ThinkTagStreamState.THINKING:
+ this.Thinking += this.thinkTagBuffer;
+ break;
+ }
+
+ this.thinkTagBuffer.Clear();
+ this.thinkTagStreamState = ThinkTagStreamState.ANSWER;
+ this.Text = this.Text.Trim();
+ this.Thinking = this.Thinking.Trim();
+ }
+
+ private void ApplyAnswerChunk(string content)
+ {
+ if (this.thinkTagStreamState is ThinkTagStreamState.UNDECIDED && this.Text.Length > 0)
+ this.thinkTagStreamState = ThinkTagStreamState.ANSWER;
+
+ switch (this.thinkTagStreamState)
+ {
+ case ThinkTagStreamState.UNDECIDED:
+ this.thinkTagBuffer.Append(content);
+ var undecidedContent = this.thinkTagBuffer.ToString();
+ if (undecidedContent.Length < OPEN_THINK_TAG.Length &&
+ OPEN_THINK_TAG.StartsWith(undecidedContent, StringComparison.Ordinal))
+ return;
+
+ if (!undecidedContent.StartsWith(OPEN_THINK_TAG, StringComparison.Ordinal))
+ {
+ this.Text += undecidedContent;
+ this.thinkTagBuffer.Clear();
+ this.thinkTagStreamState = ThinkTagStreamState.ANSWER;
+ return;
+ }
+
+ this.thinkTagBuffer.Clear();
+ this.thinkTagStreamState = ThinkTagStreamState.THINKING;
+ this.ApplyThinkingTagContent(undecidedContent[OPEN_THINK_TAG.Length..]);
+ return;
+
+ case ThinkTagStreamState.THINKING:
+ this.ApplyThinkingTagContent(content);
+ return;
+
+ case ThinkTagStreamState.ANSWER:
+ this.Text += content;
+ return;
+ }
+ }
+
+ private void ApplyThinkingTagContent(string content)
+ {
+ this.thinkTagBuffer.Append(content);
+ var thinkingContent = this.thinkTagBuffer.ToString();
+ var closeTagIndex = thinkingContent.IndexOf(CLOSE_THINK_TAG, StringComparison.Ordinal);
+ if (closeTagIndex >= 0)
+ {
+ this.Thinking += thinkingContent[..closeTagIndex];
+ this.Text += thinkingContent[(closeTagIndex + CLOSE_THINK_TAG.Length)..];
+ this.thinkTagBuffer.Clear();
+ this.thinkTagStreamState = ThinkTagStreamState.ANSWER;
+ return;
+ }
+
+ var pendingLength = GetMarkerPrefixSuffixLength(thinkingContent, CLOSE_THINK_TAG);
+ var completedLength = thinkingContent.Length - pendingLength;
+ if (completedLength <= 0)
+ return;
+
+ this.Thinking += thinkingContent[..completedLength];
+ this.thinkTagBuffer.Clear();
+ this.thinkTagBuffer.Append(thinkingContent.AsSpan(completedLength));
+ }
+
+ private static int GetMarkerPrefixSuffixLength(string content, string marker)
+ {
+ var maximumLength = Math.Min(content.Length, marker.Length - 1);
+ for (var length = maximumLength; length > 0; length--)
+ if (content.AsSpan(content.Length - length).SequenceEqual(marker.AsSpan(0, length)))
+ return length;
+
+ return 0;
+ }
+
+ private enum ThinkTagStreamState
+ {
+ UNDECIDED,
+ THINKING,
+ ANSWER,
+ }
+}
diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua
index 60ed776d8..04ec89821 100644
--- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua
+++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua
@@ -3222,6 +3222,12 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CHATROLEEXTENSIONS::T3768991250"] = "Benutzer"
-- AI
UI_TEXT_CONTENT["AISTUDIO::CHAT::CHATROLEEXTENSIONS::T601166687"] = "KI"
+-- Show thinking
+UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1121977572"] = "Denkprozess anzeigen"
+
+-- Thinking
+UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1182941917"] = "Denkprozess"
+
-- Edit Message
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1183581066"] = "Nachricht bearbeiten"
@@ -3303,6 +3309,9 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3816336467"] = "Blockie
-- Do you really want to regenerate this message?
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3878878761"] = "Möchten Sie diese Nachricht wirklich neu generieren?"
+-- Hide thinking
+UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3979442719"] = "Denkprozess ausblenden"
+
-- Remove Message
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4070211974"] = "Nachricht entfernen"
diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua
index 13998081a..b7297e90b 100644
--- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua
+++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua
@@ -3222,6 +3222,12 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CHATROLEEXTENSIONS::T3768991250"] = "User"
-- AI
UI_TEXT_CONTENT["AISTUDIO::CHAT::CHATROLEEXTENSIONS::T601166687"] = "AI"
+-- Show thinking
+UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1121977572"] = "Show thinking"
+
+-- Thinking
+UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1182941917"] = "Thinking"
+
-- Edit Message
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1183581066"] = "Edit Message"
@@ -3303,6 +3309,9 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3816336467"] = "Blocked
-- Do you really want to regenerate this message?
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3878878761"] = "Do you really want to regenerate this message?"
+-- Hide thinking
+UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3979442719"] = "Hide thinking"
+
-- Remove Message
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4070211974"] = "Remove Message"
diff --git a/app/MindWork AI Studio/Provider/Anthropic/AnthropicResponse.cs b/app/MindWork AI Studio/Provider/Anthropic/AnthropicResponse.cs
index b94d23cb7..c320c050c 100644
--- a/app/MindWork AI Studio/Provider/Anthropic/AnthropicResponse.cs
+++ b/app/MindWork AI Studio/Provider/Anthropic/AnthropicResponse.cs
@@ -36,6 +36,13 @@ public string GetTextOutput() => string.Concat(this.Content
.Where(x => ReadString(x, "type").Equals("text", StringComparison.Ordinal))
.Select(x => ReadString(x, "text")));
+ ///
+ /// The human-readable thinking the model returned, with redacted blocks omitted.
+ ///
+ public string GetThinkingOutput() => string.Concat(this.Content
+ .Where(x => ReadString(x, "type").Equals("thinking", StringComparison.Ordinal))
+ .Select(x => ReadString(x, "thinking")));
+
private static string ReadString(JsonElement item, string propertyName)
{
if (item.ValueKind is not JsonValueKind.Object ||
@@ -45,4 +52,4 @@ private static string ReadString(JsonElement item, string propertyName)
return property.GetString() ?? string.Empty;
}
-}
\ No newline at end of file
+}
diff --git a/app/MindWork AI Studio/Provider/Anthropic/AnthropicToolCallingAdapter.cs b/app/MindWork AI Studio/Provider/Anthropic/AnthropicToolCallingAdapter.cs
index 9843b3629..c0f6ce5b1 100644
--- a/app/MindWork AI Studio/Provider/Anthropic/AnthropicToolCallingAdapter.cs
+++ b/app/MindWork AI Studio/Provider/Anthropic/AnthropicToolCallingAdapter.cs
@@ -55,6 +55,7 @@ public sealed class AnthropicToolCallingAdapter(Model chatModel, IList new ToolCallingRequestedCall(
toolUse.Id,
@@ -85,4 +86,4 @@ public void RecordToolResult(string callId, string content, bool isError = false
Content = content,
IsError = isError,
});
-}
\ No newline at end of file
+}
diff --git a/app/MindWork AI Studio/Provider/Anthropic/Delta.cs b/app/MindWork AI Studio/Provider/Anthropic/Delta.cs
index 84fb06bea..05f02d7db 100644
--- a/app/MindWork AI Studio/Provider/Anthropic/Delta.cs
+++ b/app/MindWork AI Studio/Provider/Anthropic/Delta.cs
@@ -6,4 +6,5 @@ namespace AIStudio.Provider.Anthropic;
///
/// The type of the delta.
/// The text of the delta.
-public readonly record struct Delta(string Type, string Text);
\ No newline at end of file
+/// The human-readable thinking delta.
+public readonly record struct Delta(string Type, string Text, string Thinking);
diff --git a/app/MindWork AI Studio/Provider/Anthropic/ResponseStreamLine.cs b/app/MindWork AI Studio/Provider/Anthropic/ResponseStreamLine.cs
index 195f164c0..ef47ccbe5 100644
--- a/app/MindWork AI Studio/Provider/Anthropic/ResponseStreamLine.cs
+++ b/app/MindWork AI Studio/Provider/Anthropic/ResponseStreamLine.cs
@@ -10,10 +10,15 @@ namespace AIStudio.Provider.Anthropic;
public readonly record struct ResponseStreamLine(string Type, int Index, Delta Delta) : IResponseStreamLine
{
///
- public bool ContainsContent() => this != default && !string.IsNullOrWhiteSpace(this.Delta.Text);
+ public bool ContainsContent() => this != default &&
+ (!string.IsNullOrEmpty(this.Delta.Text) || !string.IsNullOrEmpty(this.Delta.Thinking));
///
- public ContentStreamChunk GetContent() => new(this.Delta.Text, []);
+ public ContentStreamChunk GetContent() => this.Delta.Type switch
+ {
+ "thinking_delta" => new(string.Empty, this.Delta.Thinking, []),
+ _ => new(this.Delta.Text, string.Empty, []),
+ };
#region Implementation of IAnnotationStreamLine
@@ -29,4 +34,4 @@ public readonly record struct ResponseStreamLine(string Type, int Index, Delta D
public IList GetSources() => [];
#endregion
-}
\ No newline at end of file
+}
diff --git a/app/MindWork AI Studio/Provider/BaseProvider.cs b/app/MindWork AI Studio/Provider/BaseProvider.cs
index 3ff4de26f..445228a44 100644
--- a/app/MindWork AI Studio/Provider/BaseProvider.cs
+++ b/app/MindWork AI Studio/Provider/BaseProvider.cs
@@ -1048,6 +1048,12 @@ protected async IAsyncEnumerable StreamResponsesInternal parameters, stri
return true;
}
-}
\ No newline at end of file
+}
diff --git a/app/MindWork AI Studio/Provider/ContentStreamChunk.cs b/app/MindWork AI Studio/Provider/ContentStreamChunk.cs
index c6b2e205c..a6c7347e6 100644
--- a/app/MindWork AI Studio/Provider/ContentStreamChunk.cs
+++ b/app/MindWork AI Studio/Provider/ContentStreamChunk.cs
@@ -4,13 +4,18 @@ namespace AIStudio.Provider;
/// A chunk of content from a content stream, along with its associated sources.
///
/// The text content of the chunk.
+/// The provider-exposed thinking content of the chunk.
/// The list of sources associated with the chunk.
-public sealed record ContentStreamChunk(string Content, IList Sources)
+public sealed record ContentStreamChunk(string Content, string Thinking, IList Sources)
{
+ public ContentStreamChunk(string content, IList sources) : this(content, string.Empty, sources)
+ {
+ }
+
///
/// Implicit conversion to string.
///
/// The content stream chunk.
/// The text content of the chunk.
public static implicit operator string(ContentStreamChunk chunk) => chunk.Content;
-}
\ No newline at end of file
+}
diff --git a/app/MindWork AI Studio/Provider/Fireworks/Delta.cs b/app/MindWork AI Studio/Provider/Fireworks/Delta.cs
index 3c53684bc..1b4d0741d 100644
--- a/app/MindWork AI Studio/Provider/Fireworks/Delta.cs
+++ b/app/MindWork AI Studio/Provider/Fireworks/Delta.cs
@@ -1,7 +1,17 @@
+using System.Text.Json;
+
+using AIStudio.Provider.OpenAI;
+
namespace AIStudio.Provider.Fireworks;
///
/// The delta text of a choice.
///
/// The content of the delta text.
-public readonly record struct Delta(string Content);
\ No newline at end of file
+/// OpenAI-compatible reasoning content.
+/// OpenRouter-compatible reasoning content.
+/// Structured reasoning details.
+public readonly record struct Delta(string Content, string? ReasoningContent, string? Reasoning, IList? ReasoningDetails)
+{
+ public string Thinking => ThinkingContent.Get(this.ReasoningContent, this.Reasoning, this.ReasoningDetails);
+}
diff --git a/app/MindWork AI Studio/Provider/Fireworks/ResponseStreamLine.cs b/app/MindWork AI Studio/Provider/Fireworks/ResponseStreamLine.cs
index 25e35f829..a0869f3ee 100644
--- a/app/MindWork AI Studio/Provider/Fireworks/ResponseStreamLine.cs
+++ b/app/MindWork AI Studio/Provider/Fireworks/ResponseStreamLine.cs
@@ -14,7 +14,7 @@ public readonly record struct ResponseStreamLine(string Id, string Object, uint
public bool ContainsContent() => this != default && this.Choices.Count > 0;
///
- public ContentStreamChunk GetContent() => new(this.Choices[0].Delta.Content, []);
+ public ContentStreamChunk GetContent() => new(this.Choices[0].Delta.Content, this.Choices[0].Delta.Thinking, []);
#region Implementation of IAnnotationStreamLine
@@ -29,4 +29,4 @@ public readonly record struct ResponseStreamLine(string Id, string Object, uint
public IList GetSources() => [];
#endregion
-}
\ No newline at end of file
+}
diff --git a/app/MindWork AI Studio/Provider/OpenAI/AssistantToolCallMessage.cs b/app/MindWork AI Studio/Provider/OpenAI/AssistantToolCallMessage.cs
index cd4048789..45bbae2a2 100644
--- a/app/MindWork AI Studio/Provider/OpenAI/AssistantToolCallMessage.cs
+++ b/app/MindWork AI Studio/Provider/OpenAI/AssistantToolCallMessage.cs
@@ -12,5 +12,11 @@ public sealed record AssistantToolCallMessage : IMessageBase
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? ReasoningContent { get; init; }
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? Reasoning { get; init; }
+
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public IList? ReasoningDetails { get; init; }
+
public IList ToolCalls { get; init; } = [];
}
diff --git a/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionDelta.cs b/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionDelta.cs
index 9f6f6cfcc..517b81191 100644
--- a/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionDelta.cs
+++ b/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionDelta.cs
@@ -13,4 +13,13 @@ public sealed record ChatCompletionDelta
[JsonIgnore]
public string Content => ChatCompletionContent.GetText(this.RawContent) ?? string.Empty;
+
+ public string? ReasoningContent { get; init; }
+
+ public string? Reasoning { get; init; }
+
+ public IList? ReasoningDetails { get; init; }
+
+ [JsonIgnore]
+ public string Thinking => ThinkingContent.Get(this.ReasoningContent, this.Reasoning, this.ReasoningDetails);
}
diff --git a/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionDeltaStreamLine.cs b/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionDeltaStreamLine.cs
index 1db13ba9d..7b686ac3a 100644
--- a/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionDeltaStreamLine.cs
+++ b/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionDeltaStreamLine.cs
@@ -19,7 +19,7 @@ public ChatCompletionDeltaStreamLine() : this(string.Empty, string.Empty, 0, str
public bool ContainsContent() => this.Choices.Count > 0;
///
- public ContentStreamChunk GetContent() => new(this.Choices[0].Delta.Content, []);
+ public ContentStreamChunk GetContent() => new(this.Choices[0].Delta.Content, this.Choices[0].Delta.Thinking, []);
#region Implementation of IAnnotationStreamLine
@@ -42,4 +42,4 @@ public ChatCompletionDeltaStreamLine() : this(string.Empty, string.Empty, 0, str
public IList GetSources() => [];
#endregion
-}
\ No newline at end of file
+}
diff --git a/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionResponseMessage.cs b/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionResponseMessage.cs
index 40b20e788..4ec59e9e3 100644
--- a/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionResponseMessage.cs
+++ b/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionResponseMessage.cs
@@ -15,5 +15,11 @@ public sealed record ChatCompletionResponseMessage
public string? ReasoningContent { get; init; }
+ public string? Reasoning { get; init; }
+
+ public IList? ReasoningDetails { get; init; }
+
public IList? ToolCalls { get; init; }
+
+ public string GetThinkingOutput() => ThinkingContent.Get(this.ReasoningContent, this.Reasoning, this.ReasoningDetails);
}
diff --git a/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionToolCallingAdapter.cs b/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionToolCallingAdapter.cs
index 594efc43f..c525eff26 100644
--- a/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionToolCallingAdapter.cs
+++ b/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionToolCallingAdapter.cs
@@ -72,6 +72,7 @@ public sealed class ChatCompletionToolCallingAdapter(
return new ToolCallingRound(
responseChoice.Message.Content ?? string.Empty,
+ responseChoice.Message.GetThinkingOutput(),
preparedCalls
.Select(x => new ToolCallingRequestedCall(x.ToolCall.Id!, x.ToolCall.Function!.Name!, x.ToolCall.Function!.Arguments!, x.IsValid))
.ToList(),
@@ -83,6 +84,8 @@ public void RecordAssistantTurn() => this.internalMessages.Add(new AssistantTool
{
Content = this.lastResponseMessage?.RawContent,
ReasoningContent = this.lastResponseMessage?.ReasoningContent,
+ Reasoning = this.lastResponseMessage?.Reasoning,
+ ReasoningDetails = this.lastResponseMessage?.ReasoningDetails,
ToolCalls = this.lastToolCalls,
});
@@ -168,4 +171,4 @@ private List PrepareToolCalls(IEnumerable this.Delta is not null;
///
- public ContentStreamChunk GetContent() => new(this.Delta ?? string.Empty, this.GetSources());
+ public ContentStreamChunk GetContent() => this.Type switch
+ {
+ "response.reasoning_summary_text.delta" or "response.reasoning_text.delta" => new(string.Empty, this.Delta ?? string.Empty, this.GetSources()),
+ _ => new(this.Delta ?? string.Empty, string.Empty, this.GetSources()),
+ };
//
// Please note that there are multiple options where LLM providers might stream sources:
@@ -36,4 +40,4 @@ public record ResponsesDeltaStreamLine(
public IList GetSources() => [];
#endregion
-}
\ No newline at end of file
+}
diff --git a/app/MindWork AI Studio/Provider/OpenAI/ResponsesResponse.cs b/app/MindWork AI Studio/Provider/OpenAI/ResponsesResponse.cs
index 285bca00e..f0266ea2c 100644
--- a/app/MindWork AI Studio/Provider/OpenAI/ResponsesResponse.cs
+++ b/app/MindWork AI Studio/Provider/OpenAI/ResponsesResponse.cs
@@ -42,6 +42,11 @@ public string GetTextOutput()
}));
}
+ public string GetThinkingOutput() => string.Concat(this.Output
+ .Where(x => ReadString(x, "type").Equals("reasoning", StringComparison.Ordinal))
+ .SelectMany(x => ReadArrayItems(x, "summary").Concat(ReadArrayItems(x, "content")))
+ .Select(x => ReadString(x, "text")));
+
public IReadOnlyList GetSources() => this.Output
.Where(x => ReadString(x, "type").Equals("message", StringComparison.Ordinal))
.SelectMany(ReadContentItems)
diff --git a/app/MindWork AI Studio/Provider/OpenAI/ResponsesToolCallingAdapter.cs b/app/MindWork AI Studio/Provider/OpenAI/ResponsesToolCallingAdapter.cs
index 014865579..5d30a6234 100644
--- a/app/MindWork AI Studio/Provider/OpenAI/ResponsesToolCallingAdapter.cs
+++ b/app/MindWork AI Studio/Provider/OpenAI/ResponsesToolCallingAdapter.cs
@@ -57,6 +57,7 @@ public sealed class ResponsesToolCallingAdapter(Model chatModel, IList