mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-07 06:49:40 +00:00
Add per-answer thinking disclosure
This commit is contained in:
1 parent
c7b42bee96
commit
bbbcdddc91
29 files changed
+360
-37
No files matched your search
@@ -36,6 +36,13 @@ public sealed record AnthropicResponse
|
||||
.Where(x => ReadString(x, "type").Equals("text", StringComparison.Ordinal))
|
||||
.Select(x => ReadString(x, "text")));
|
||||
|
||||
/// <summary>
|
||||
/// The human-readable thinking the model returned, with redacted blocks omitted.
|
||||
/// </summary>
|
||||
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 @@ public sealed record AnthropicResponse
|
||||
|
||||
return property.GetString() ?? string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,7 @@ public sealed class AnthropicToolCallingAdapter(Model chatModel, IList<IMessageB
|
||||
this.lastResponse = response;
|
||||
return new ToolCallingRound(
|
||||
response.GetTextOutput(),
|
||||
response.GetThinkingOutput(),
|
||||
response.GetToolUses()
|
||||
.Select(toolUse => new ToolCallingRequestedCall(
|
||||
toolUse.Id,
|
||||
@@ -85,4 +86,4 @@ public sealed class AnthropicToolCallingAdapter(Model chatModel, IList<IMessageB
|
||||
Content = content,
|
||||
IsError = isError,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -6,4 +6,5 @@ namespace AIStudio.Provider.Anthropic;
|
||||
/// </summary>
|
||||
/// <param name="Type">The type of the delta.</param>
|
||||
/// <param name="Text">The text of the delta.</param>
|
||||
public readonly record struct Delta(string Type, string Text);
|
||||
/// <param name="Thinking">The human-readable thinking delta.</param>
|
||||
public readonly record struct Delta(string Type, string Text, string Thinking);
|
||||
@@ -10,10 +10,15 @@ namespace AIStudio.Provider.Anthropic;
|
||||
public readonly record struct ResponseStreamLine(string Type, int Index, Delta Delta) : IResponseStreamLine
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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));
|
||||
|
||||
/// <inheritdoc />
|
||||
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<ISource> GetSources() => [];
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1048,6 +1048,12 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
//
|
||||
if (jsonData.StartsWith("""
|
||||
{"type":"response.output_text.delta"
|
||||
""", StringComparison.InvariantCulture) ||
|
||||
jsonData.StartsWith("""
|
||||
{"type":"response.reasoning_summary_text.delta"
|
||||
""", StringComparison.InvariantCulture) ||
|
||||
jsonData.StartsWith("""
|
||||
{"type":"response.reasoning_text.delta"
|
||||
""", StringComparison.InvariantCulture))
|
||||
{
|
||||
TDelta? providerResponse;
|
||||
@@ -1606,4 +1612,4 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,18 @@ namespace AIStudio.Provider;
|
||||
/// A chunk of content from a content stream, along with its associated sources.
|
||||
/// </summary>
|
||||
/// <param name="Content">The text content of the chunk.</param>
|
||||
/// <param name="Thinking">The provider-exposed thinking content of the chunk.</param>
|
||||
/// <param name="Sources">The list of sources associated with the chunk.</param>
|
||||
public sealed record ContentStreamChunk(string Content, IList<ISource> Sources)
|
||||
public sealed record ContentStreamChunk(string Content, string Thinking, IList<ISource> Sources)
|
||||
{
|
||||
public ContentStreamChunk(string content, IList<ISource> sources) : this(content, string.Empty, sources)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implicit conversion to string.
|
||||
/// </summary>
|
||||
/// <param name="chunk">The content stream chunk.</param>
|
||||
/// <returns>The text content of the chunk.</returns>
|
||||
public static implicit operator string(ContentStreamChunk chunk) => chunk.Content;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,17 @@
|
||||
using System.Text.Json;
|
||||
|
||||
using AIStudio.Provider.OpenAI;
|
||||
|
||||
namespace AIStudio.Provider.Fireworks;
|
||||
|
||||
/// <summary>
|
||||
/// The delta text of a choice.
|
||||
/// </summary>
|
||||
/// <param name="Content">The content of the delta text.</param>
|
||||
public readonly record struct Delta(string Content);
|
||||
/// <param name="ReasoningContent">OpenAI-compatible reasoning content.</param>
|
||||
/// <param name="Reasoning">OpenRouter-compatible reasoning content.</param>
|
||||
/// <param name="ReasoningDetails">Structured reasoning details.</param>
|
||||
public readonly record struct Delta(string Content, string? ReasoningContent, string? Reasoning, IList<JsonElement>? ReasoningDetails)
|
||||
{
|
||||
public string Thinking => ThinkingContent.Get(this.ReasoningContent, this.Reasoning, this.ReasoningDetails);
|
||||
}
|
||||
@@ -14,7 +14,7 @@ public readonly record struct ResponseStreamLine(string Id, string Object, uint
|
||||
public bool ContainsContent() => this != default && this.Choices.Count > 0;
|
||||
|
||||
/// <inheritdoc />
|
||||
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<ISource> GetSources() => [];
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -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<JsonElement>? ReasoningDetails { get; init; }
|
||||
|
||||
public IList<ChatCompletionToolCall> ToolCalls { get; init; } = [];
|
||||
}
|
||||
@@ -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<JsonElement>? ReasoningDetails { get; init; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string Thinking => ThinkingContent.Get(this.ReasoningContent, this.Reasoning, this.ReasoningDetails);
|
||||
}
|
||||
@@ -19,7 +19,7 @@ public record ChatCompletionDeltaStreamLine(string Id, string Object, uint Creat
|
||||
public bool ContainsContent() => this.Choices.Count > 0;
|
||||
|
||||
/// <inheritdoc />
|
||||
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 record ChatCompletionDeltaStreamLine(string Id, string Object, uint Creat
|
||||
public IList<ISource> GetSources() => [];
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -15,5 +15,11 @@ public sealed record ChatCompletionResponseMessage
|
||||
|
||||
public string? ReasoningContent { get; init; }
|
||||
|
||||
public string? Reasoning { get; init; }
|
||||
|
||||
public IList<JsonElement>? ReasoningDetails { get; init; }
|
||||
|
||||
public IList<ChatCompletionToolCall?>? ToolCalls { get; init; }
|
||||
|
||||
public string GetThinkingOutput() => ThinkingContent.Get(this.ReasoningContent, this.Reasoning, this.ReasoningDetails);
|
||||
}
|
||||
@@ -72,6 +72,7 @@ public sealed class ChatCompletionToolCallingAdapter<TRequest>(
|
||||
|
||||
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 sealed class ChatCompletionToolCallingAdapter<TRequest>(
|
||||
{
|
||||
Content = this.lastResponseMessage?.RawContent,
|
||||
ReasoningContent = this.lastResponseMessage?.ReasoningContent,
|
||||
Reasoning = this.lastResponseMessage?.Reasoning,
|
||||
ReasoningDetails = this.lastResponseMessage?.ReasoningDetails,
|
||||
ToolCalls = this.lastToolCalls,
|
||||
});
|
||||
|
||||
@@ -168,4 +171,4 @@ public sealed class ChatCompletionToolCallingAdapter<TRequest>(
|
||||
}
|
||||
|
||||
private readonly record struct PreparedChatCompletionToolCall(ChatCompletionToolCall ToolCall, bool IsValid);
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,11 @@ public record ResponsesDeltaStreamLine(
|
||||
public bool ContainsContent() => this.Delta is not null;
|
||||
|
||||
/// <inheritdoc />
|
||||
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<ISource> GetSources() => [];
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,11 @@ public sealed record ResponsesResponse
|
||||
}));
|
||||
}
|
||||
|
||||
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<Source> GetSources() => this.Output
|
||||
.Where(x => ReadString(x, "type").Equals("message", StringComparison.Ordinal))
|
||||
.SelectMany(ReadContentItems)
|
||||
|
||||
@@ -57,6 +57,7 @@ public sealed class ResponsesToolCallingAdapter(Model chatModel, IList<object> b
|
||||
this.lastResponse = response;
|
||||
return new ToolCallingRound(
|
||||
response.GetTextOutput(),
|
||||
response.GetThinkingOutput(),
|
||||
response.GetFunctionCalls()
|
||||
.Select(call => new ToolCallingRequestedCall(
|
||||
call.CallId ?? string.Empty,
|
||||
@@ -102,4 +103,4 @@ public sealed class ResponsesToolCallingAdapter(Model chatModel, IList<object> b
|
||||
.Concat(runnableTools.Select(x => (object)ProviderToolAdapters.ToResponsesTool(x.Definition)))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// Reads human-readable thinking from OpenAI-compatible response fields.
|
||||
/// </summary>
|
||||
internal static class ThinkingContent
|
||||
{
|
||||
public static string Get(string? reasoningContent, string? reasoning, IEnumerable<JsonElement>? reasoningDetails)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(reasoningContent))
|
||||
return reasoningContent;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(reasoning))
|
||||
return reasoning;
|
||||
|
||||
if (reasoningDetails is null)
|
||||
return string.Empty;
|
||||
|
||||
return string.Concat(reasoningDetails.Select(GetVisibleDetailText));
|
||||
}
|
||||
|
||||
private static string GetVisibleDetailText(JsonElement detail)
|
||||
{
|
||||
if (detail.ValueKind is not JsonValueKind.Object ||
|
||||
!detail.TryGetProperty("type", out var typeProperty) ||
|
||||
typeProperty.ValueKind is not JsonValueKind.String)
|
||||
return string.Empty;
|
||||
|
||||
return typeProperty.GetString() switch
|
||||
{
|
||||
"reasoning.text" => ReadString(detail, "text"),
|
||||
"reasoning.summary" => ReadString(detail, "summary"),
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
private static string ReadString(JsonElement item, string propertyName)
|
||||
{
|
||||
if (!item.TryGetProperty(propertyName, out var property) || property.ValueKind is not JsonValueKind.String)
|
||||
return string.Empty;
|
||||
|
||||
return property.GetString() ?? string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,17 @@
|
||||
using System.Text.Json;
|
||||
|
||||
using AIStudio.Provider.OpenAI;
|
||||
|
||||
namespace AIStudio.Provider.Perplexity;
|
||||
|
||||
/// <summary>
|
||||
/// The delta text of a choice.
|
||||
/// </summary>
|
||||
/// <param name="Content">The content of the delta text.</param>
|
||||
public readonly record struct Delta(string Content);
|
||||
/// <param name="ReasoningContent">OpenAI-compatible reasoning content.</param>
|
||||
/// <param name="Reasoning">OpenRouter-compatible reasoning content.</param>
|
||||
/// <param name="ReasoningDetails">Structured reasoning details.</param>
|
||||
public readonly record struct Delta(string Content, string? ReasoningContent, string? Reasoning, IList<JsonElement>? ReasoningDetails)
|
||||
{
|
||||
public string Thinking => ThinkingContent.Get(this.ReasoningContent, this.Reasoning, this.ReasoningDetails);
|
||||
}
|
||||
@@ -15,11 +15,11 @@ public readonly record struct ResponseStreamLine(string Id, string Object, uint
|
||||
public bool ContainsContent() => this != default && this.Choices.Count > 0;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ContentStreamChunk GetContent() => new(this.Choices[0].Delta.Content, this.GetSources());
|
||||
public ContentStreamChunk GetContent() => new(this.Choices[0].Delta.Content, this.Choices[0].Delta.Thinking, this.GetSources());
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ContainsSources() => this != default && this.SearchResults.Count > 0;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IList<ISource> GetSources() => this.SearchResults.Cast<ISource>().ToList();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user