Add per-answer thinking disclosure

This commit is contained in:
Peer Hogeterp committed 2026-09-10 15:37:43 +02:00
1 parent c7b42bee96
commit bbbcdddc91
29 files changed
+360 -37

No files matched your search

@@ -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;
}
}