mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-09-27 03:53:36 +00:00
Polish per-answer thinking disclosure
This commit is contained in:
parent
b28f2bcf6c
commit
fc30b33daa
@ -226,8 +226,9 @@
|
|||||||
}
|
}
|
||||||
else if (this.Content.IsStreaming)
|
else if (this.Content.IsStreaming)
|
||||||
{
|
{
|
||||||
|
@* The think tags never reach the text: ContentText splits them off while streaming. *@
|
||||||
<MudText Typo="Typo.body1" Style="white-space: pre-wrap;">
|
<MudText Typo="Typo.body1" Style="white-space: pre-wrap;">
|
||||||
@textContent.Text.RemoveThinkTags()
|
@textContent.Text
|
||||||
</MudText>
|
</MudText>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@ -127,13 +127,19 @@ public sealed class ContentText : IContent
|
|||||||
if (token.IsCancellationRequested)
|
if (token.IsCancellationRequested)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Stop the waiting animation:
|
|
||||||
this.InitialRemoteWait = false;
|
|
||||||
this.IsStreaming = true;
|
this.IsStreaming = true;
|
||||||
|
|
||||||
// Add the response to the content:
|
// Add the response to the content:
|
||||||
this.ApplyStreamChunk(contentStreamChunk);
|
this.ApplyStreamChunk(contentStreamChunk);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Stop the waiting animation once the answer itself starts. A model which
|
||||||
|
// is still reasoning has not written anything to read yet, and an empty
|
||||||
|
// bubble would look like a finished, empty answer. The thinking section
|
||||||
|
// is available next to the animation the whole time.
|
||||||
|
//
|
||||||
|
this.InitialRemoteWait = this.Text.Length is 0;
|
||||||
|
|
||||||
// Notify the UI that the content has changed,
|
// Notify the UI that the content has changed,
|
||||||
// depending on the energy saving mode:
|
// depending on the energy saving mode:
|
||||||
var now = DateTimeOffset.Now;
|
var now = DateTimeOffset.Now;
|
||||||
|
|||||||
@ -39,9 +39,16 @@ public sealed record AnthropicResponse
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The human-readable thinking the model returned, with redacted blocks omitted.
|
/// The human-readable thinking the model returned, with redacted blocks omitted.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string GetThinkingOutput() => string.Concat(this.Content
|
/// <remarks>
|
||||||
|
/// Each thinking block reads as its own paragraph, so they are joined as paragraphs
|
||||||
|
/// rather than run into one another.
|
||||||
|
/// </remarks>
|
||||||
|
public string GetThinkingOutput() => string.Join(
|
||||||
|
$"{Environment.NewLine}{Environment.NewLine}",
|
||||||
|
this.Content
|
||||||
.Where(x => ReadString(x, "type").Equals("thinking", StringComparison.Ordinal))
|
.Where(x => ReadString(x, "type").Equals("thinking", StringComparison.Ordinal))
|
||||||
.Select(x => ReadString(x, "thinking")));
|
.Select(x => ReadString(x, "thinking"))
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x)));
|
||||||
|
|
||||||
private static string ReadString(JsonElement item, string propertyName)
|
private static string ReadString(JsonElement item, string propertyName)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -16,8 +16,8 @@ public readonly record struct ResponseStreamLine(string Type, int Index, Delta D
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ContentStreamChunk GetContent() => this.Delta.Type switch
|
public ContentStreamChunk GetContent() => this.Delta.Type switch
|
||||||
{
|
{
|
||||||
"thinking_delta" => new(string.Empty, this.Delta.Thinking, []),
|
"thinking_delta" => new(string.Empty, [], this.Delta.Thinking),
|
||||||
_ => new(this.Delta.Text, string.Empty, []),
|
_ => new(this.Delta.Text, []),
|
||||||
};
|
};
|
||||||
|
|
||||||
#region Implementation of IAnnotationStreamLine
|
#region Implementation of IAnnotationStreamLine
|
||||||
|
|||||||
@ -1054,6 +1054,15 @@ public abstract class BaseProvider : IProvider, ISecretId
|
|||||||
""", StringComparison.InvariantCulture) ||
|
""", StringComparison.InvariantCulture) ||
|
||||||
jsonData.StartsWith("""
|
jsonData.StartsWith("""
|
||||||
{"type":"response.reasoning_text.delta"
|
{"type":"response.reasoning_text.delta"
|
||||||
|
""", StringComparison.InvariantCulture) ||
|
||||||
|
|
||||||
|
//
|
||||||
|
// Not a delta of its own: it announces the next part of a reasoning summary.
|
||||||
|
// The stream carries no separator between those parts, so the stream line
|
||||||
|
// turns this event into the paragraph break between them.
|
||||||
|
//
|
||||||
|
jsonData.StartsWith("""
|
||||||
|
{"type":"response.reasoning_summary_part.added"
|
||||||
""", StringComparison.InvariantCulture))
|
""", StringComparison.InvariantCulture))
|
||||||
{
|
{
|
||||||
TDelta? providerResponse;
|
TDelta? providerResponse;
|
||||||
|
|||||||
@ -4,14 +4,10 @@ namespace AIStudio.Provider;
|
|||||||
/// A chunk of content from a content stream, along with its associated sources.
|
/// A chunk of content from a content stream, along with its associated sources.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="Content">The text content of the chunk.</param>
|
/// <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>
|
/// <param name="Sources">The list of sources associated with the chunk.</param>
|
||||||
public sealed record ContentStreamChunk(string Content, string Thinking, IList<ISource> Sources)
|
/// <param name="Thinking">The provider-exposed thinking content of the chunk.</param>
|
||||||
|
public sealed record ContentStreamChunk(string Content, IList<ISource> Sources, string Thinking = "")
|
||||||
{
|
{
|
||||||
public ContentStreamChunk(string content, IList<ISource> sources) : this(content, string.Empty, sources)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Implicit conversion to string.
|
/// Implicit conversion to string.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -14,7 +14,7 @@ public readonly record struct ResponseStreamLine(string Id, string Object, uint
|
|||||||
public bool ContainsContent() => this != default && this.Choices.Count > 0;
|
public bool ContainsContent() => this != default && this.Choices.Count > 0;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ContentStreamChunk GetContent() => new(this.Choices[0].Delta.Content, this.Choices[0].Delta.Thinking, []);
|
public ContentStreamChunk GetContent() => new(this.Choices[0].Delta.Content, [], this.Choices[0].Delta.Thinking);
|
||||||
|
|
||||||
#region Implementation of IAnnotationStreamLine
|
#region Implementation of IAnnotationStreamLine
|
||||||
|
|
||||||
|
|||||||
@ -25,6 +25,13 @@ public readonly record struct Model(string Id, string? DisplayName)
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The provider-reported default reasoning behavior, when the model catalog supplies it.
|
/// The provider-reported default reasoning behavior, when the model catalog supplies it.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This travels with the selected model into the user's settings, so it is a copy of what the
|
||||||
|
/// catalog said when the user picked the model, not a live value. It is refreshed when the user
|
||||||
|
/// picks a model again. That is deliberate: the capability lookup works from the stored model
|
||||||
|
/// and must not depend on the provider being reachable. <see cref="ModelReasoningBehavior.UNKNOWN"/>
|
||||||
|
/// falls back to the model-name heuristics, which is also what every older settings file yields.
|
||||||
|
/// </remarks>
|
||||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||||
public ModelReasoningBehavior ReasoningBehavior { get; init; }
|
public ModelReasoningBehavior ReasoningBehavior { get; init; }
|
||||||
|
|
||||||
@ -80,14 +87,3 @@ public readonly record struct Model(string Id, string? DisplayName)
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Describes the default reasoning behavior reported by a provider's model catalog.
|
|
||||||
/// </summary>
|
|
||||||
public enum ModelReasoningBehavior
|
|
||||||
{
|
|
||||||
UNKNOWN,
|
|
||||||
OPTIONAL,
|
|
||||||
DEFAULT_ON,
|
|
||||||
ALWAYS_ON,
|
|
||||||
}
|
|
||||||
|
|||||||
32
app/MindWork AI Studio/Provider/ModelReasoningBehavior.cs
Normal file
32
app/MindWork AI Studio/Provider/ModelReasoningBehavior.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
namespace AIStudio.Provider;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Describes the default reasoning behavior reported by a provider's model catalog.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// A catalog which reports this is more reliable than our model-name heuristics, so
|
||||||
|
/// <see cref="Model.ReasoningBehavior"/> takes precedence over them. Not every provider
|
||||||
|
/// reports it, which is what <see cref="UNKNOWN"/> stands for.
|
||||||
|
/// </remarks>
|
||||||
|
public enum ModelReasoningBehavior
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The catalog said nothing about reasoning. The model-name heuristics decide.
|
||||||
|
/// </summary>
|
||||||
|
UNKNOWN,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The model can reason, but does not unless the request asks for it.
|
||||||
|
/// </summary>
|
||||||
|
OPTIONAL,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The model reasons unless the request switches it off.
|
||||||
|
/// </summary>
|
||||||
|
DEFAULT_ON,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The model always reasons and it cannot be switched off.
|
||||||
|
/// </summary>
|
||||||
|
ALWAYS_ON,
|
||||||
|
}
|
||||||
@ -19,7 +19,7 @@ public record ChatCompletionDeltaStreamLine(string Id, string Object, uint Creat
|
|||||||
public bool ContainsContent() => this.Choices.Count > 0;
|
public bool ContainsContent() => this.Choices.Count > 0;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ContentStreamChunk GetContent() => new(this.Choices[0].Delta.Content, this.Choices[0].Delta.Thinking, []);
|
public ContentStreamChunk GetContent() => new(this.Choices[0].Delta.Content, [], this.Choices[0].Delta.Thinking);
|
||||||
|
|
||||||
#region Implementation of IAnnotationStreamLine
|
#region Implementation of IAnnotationStreamLine
|
||||||
|
|
||||||
|
|||||||
@ -97,6 +97,11 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
|
|||||||
// Check if we are using the Responses API or the Chat Completion API:
|
// Check if we are using the Responses API or the Chat Completion API:
|
||||||
var usingResponsesAPI = modelCapabilities.Contains(Capability.RESPONSES_API);
|
var usingResponsesAPI = modelCapabilities.Contains(Capability.RESPONSES_API);
|
||||||
|
|
||||||
|
// Whether the model reasons at all, no matter whether it may be switched off:
|
||||||
|
var isReasoningModel = modelCapabilities.Contains(Capability.ALWAYS_REASONING) ||
|
||||||
|
modelCapabilities.Contains(Capability.REASONING_BY_DEFAULT) ||
|
||||||
|
modelCapabilities.Contains(Capability.OPTIONAL_REASONING);
|
||||||
|
|
||||||
// Prepare the request path based on the API we are using:
|
// Prepare the request path based on the API we are using:
|
||||||
var requestPath = usingResponsesAPI ? "responses" : "chat/completions";
|
var requestPath = usingResponsesAPI ? "responses" : "chat/completions";
|
||||||
|
|
||||||
@ -225,6 +230,7 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
|
|||||||
{
|
{
|
||||||
var adapter = new ResponsesToolCallingAdapter(
|
var adapter = new ResponsesToolCallingAdapter(
|
||||||
chatModel,
|
chatModel,
|
||||||
|
isReasoningModel,
|
||||||
baseInput,
|
baseInput,
|
||||||
additionalApiParameters,
|
additionalApiParameters,
|
||||||
providerTools,
|
providerTools,
|
||||||
|
|||||||
@ -5,20 +5,37 @@ namespace AIStudio.Provider.OpenAI;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="Type">The type of the response.</param>
|
/// <param name="Type">The type of the response.</param>
|
||||||
/// <param name="Delta">The delta content of the response.</param>
|
/// <param name="Delta">The delta content of the response.</param>
|
||||||
|
/// <param name="SummaryIndex">The reasoning summary part this line belongs to.</param>
|
||||||
public record ResponsesDeltaStreamLine(
|
public record ResponsesDeltaStreamLine(
|
||||||
string Type,
|
string Type,
|
||||||
string? Delta) : IResponseStreamLine
|
string? Delta,
|
||||||
|
int SummaryIndex) : IResponseStreamLine
|
||||||
{
|
{
|
||||||
|
private const string SUMMARY_PART_ADDED = "response.reasoning_summary_part.added";
|
||||||
|
private const string SUMMARY_TEXT_DELTA = "response.reasoning_summary_text.delta";
|
||||||
|
private const string REASONING_TEXT_DELTA = "response.reasoning_text.delta";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this line starts a reasoning summary part which follows an earlier one.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// A reasoning summary arrives as several parts, and each one reads as its own paragraph.
|
||||||
|
/// The stream carries no separator between them, so the boundary has to come from the
|
||||||
|
/// event which announces the next part.
|
||||||
|
/// </remarks>
|
||||||
|
private bool IsFollowUpSummaryPart => this.Type is SUMMARY_PART_ADDED && this.SummaryIndex > 0;
|
||||||
|
|
||||||
#region Implementation of IResponseStreamLine
|
#region Implementation of IResponseStreamLine
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool ContainsContent() => this.Delta is not null;
|
public bool ContainsContent() => this.Delta is not null || this.IsFollowUpSummaryPart;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ContentStreamChunk GetContent() => this.Type switch
|
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()),
|
SUMMARY_PART_ADDED => new(string.Empty, this.GetSources(), this.IsFollowUpSummaryPart ? $"{Environment.NewLine}{Environment.NewLine}" : string.Empty),
|
||||||
_ => new(this.Delta ?? string.Empty, string.Empty, this.GetSources()),
|
SUMMARY_TEXT_DELTA or REASONING_TEXT_DELTA => new(string.Empty, this.GetSources(), this.Delta ?? string.Empty),
|
||||||
|
_ => new(this.Delta ?? string.Empty, this.GetSources()),
|
||||||
};
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|||||||
@ -42,11 +42,23 @@ public sealed record ResponsesResponse
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetThinkingOutput() => string.Concat(this.Output
|
/// <summary>
|
||||||
|
/// The human-readable thinking the model returned.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// A reasoning item carries its summary and its raw text as arrays of parts, and each part
|
||||||
|
/// reads as its own paragraph. The API states no separator between them, so we join them
|
||||||
|
/// as paragraphs instead of running them into one another.
|
||||||
|
/// </remarks>
|
||||||
|
public string GetThinkingOutput() => JoinParagraphs(this.Output
|
||||||
.Where(x => ReadString(x, "type").Equals("reasoning", StringComparison.Ordinal))
|
.Where(x => ReadString(x, "type").Equals("reasoning", StringComparison.Ordinal))
|
||||||
.SelectMany(x => ReadArrayItems(x, "summary").Concat(ReadArrayItems(x, "content")))
|
.SelectMany(x => ReadArrayItems(x, "summary").Concat(ReadArrayItems(x, "content")))
|
||||||
.Select(x => ReadString(x, "text")));
|
.Select(x => ReadString(x, "text")));
|
||||||
|
|
||||||
|
private static string JoinParagraphs(IEnumerable<string> parts) => string.Join(
|
||||||
|
$"{Environment.NewLine}{Environment.NewLine}",
|
||||||
|
parts.Where(part => !string.IsNullOrWhiteSpace(part)));
|
||||||
|
|
||||||
public IReadOnlyList<Source> GetSources() => this.Output
|
public IReadOnlyList<Source> GetSources() => this.Output
|
||||||
.Where(x => ReadString(x, "type").Equals("message", StringComparison.Ordinal))
|
.Where(x => ReadString(x, "type").Equals("message", StringComparison.Ordinal))
|
||||||
.SelectMany(ReadContentItems)
|
.SelectMany(ReadContentItems)
|
||||||
|
|||||||
@ -11,7 +11,7 @@ namespace AIStudio.Provider.OpenAI;
|
|||||||
/// correlated by call ID. Unlike Chat Completions, the whole output of a round has to be sent
|
/// correlated by call ID. Unlike Chat Completions, the whole output of a round has to be sent
|
||||||
/// back for the next one, reasoning items included, or the API refuses to continue.
|
/// back for the next one, reasoning items included, or the API refuses to continue.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public sealed class ResponsesToolCallingAdapter(Model chatModel, IList<object> baseInput, IDictionary<string, object> apiParameters, IList<object> providerTools,
|
public sealed class ResponsesToolCallingAdapter(Model chatModel, bool isReasoningModel, IList<object> baseInput, IDictionary<string, object> apiParameters, IList<object> providerTools,
|
||||||
IReadOnlyList<(ToolDefinition Definition, IToolImplementation Implementation)> runnableTools,
|
IReadOnlyList<(ToolDefinition Definition, IToolImplementation Implementation)> runnableTools,
|
||||||
Func<ResponsesAPIRequest, CancellationToken, Task<ResponsesResponse?>> executeRequestAsync) : IToolCallingProviderAdapter
|
Func<ResponsesAPIRequest, CancellationToken, Task<ResponsesResponse?>> executeRequestAsync) : IToolCallingProviderAdapter
|
||||||
{
|
{
|
||||||
@ -50,7 +50,7 @@ public sealed class ResponsesToolCallingAdapter(Model chatModel, IList<object> b
|
|||||||
Stream = false,
|
Stream = false,
|
||||||
Store = false,
|
Store = false,
|
||||||
Tools = includeTools ? this.effectiveProviderTools : [],
|
Tools = includeTools ? this.effectiveProviderTools : [],
|
||||||
AdditionalApiParameters = IncludeEncryptedReasoning(apiParameters),
|
AdditionalApiParameters = isReasoningModel ? IncludeEncryptedReasoning(apiParameters) : apiParameters,
|
||||||
}, token);
|
}, token);
|
||||||
|
|
||||||
if (response is null)
|
if (response is null)
|
||||||
@ -99,7 +99,8 @@ public sealed class ResponsesToolCallingAdapter(Model chatModel, IList<object> b
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Tool rounds use stateless Responses requests. OpenAI requires encrypted reasoning items in that mode
|
/// Tool rounds use stateless Responses requests. OpenAI requires encrypted reasoning items in that mode
|
||||||
/// so that the complete output can be passed back with the tool result on the next round.
|
/// so that the complete output can be passed back with the tool result on the next round.<br/><br/>
|
||||||
|
/// Only a reasoning model gets this, because the include is meaningless for every other one.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
private static IDictionary<string, object> IncludeEncryptedReasoning(IDictionary<string, object> apiParameters)
|
private static IDictionary<string, object> IncludeEncryptedReasoning(IDictionary<string, object> apiParameters)
|
||||||
{
|
{
|
||||||
@ -111,11 +112,15 @@ public sealed class ResponsesToolCallingAdapter(Model chatModel, IList<object> b
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Whatever the user put there stays. A value we cannot read as a list is kept as the
|
||||||
|
// single entry it appears to be: the API rejecting the user's own value is the honest
|
||||||
|
// outcome, while dropping it here would hide the mistake behind a request that works.
|
||||||
|
//
|
||||||
var includedOutput = result[includeKey] switch
|
var includedOutput = result[includeKey] switch
|
||||||
{
|
{
|
||||||
IEnumerable<object> values => values.ToList(),
|
IEnumerable<object> values => values.ToList(),
|
||||||
string value => new List<object> { value },
|
var value => [value],
|
||||||
_ => [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!includedOutput.Any(value => string.Equals(value as string, ENCRYPTED_REASONING_INCLUDE, StringComparison.Ordinal)))
|
if (!includedOutput.Any(value => string.Equals(value as string, ENCRYPTED_REASONING_INCLUDE, StringComparison.Ordinal)))
|
||||||
|
|||||||
@ -8,6 +8,9 @@ namespace AIStudio.Provider.OpenRouter;
|
|||||||
/// <param name="Reasoning">The model's provider-reported reasoning behavior.</param>
|
/// <param name="Reasoning">The model's provider-reported reasoning behavior.</param>
|
||||||
public readonly record struct OpenRouterModel(string Id, string? Name, OpenRouterReasoning? Reasoning)
|
public readonly record struct OpenRouterModel(string Id, string? Name, OpenRouterReasoning? Reasoning)
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Converts the catalog entry into the model the rest of the app works with.
|
||||||
|
/// </summary>
|
||||||
public Model ToModel() => new(this.Id, this.Name)
|
public Model ToModel() => new(this.Id, this.Name)
|
||||||
{
|
{
|
||||||
ReasoningBehavior = this.Reasoning switch
|
ReasoningBehavior = this.Reasoning switch
|
||||||
@ -19,10 +22,3 @@ public readonly record struct OpenRouterModel(string Id, string? Name, OpenRoute
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The reasoning defaults returned for a model by OpenRouter's model catalog.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="DefaultEnabled">Whether reasoning is enabled when the request does not configure it.</param>
|
|
||||||
/// <param name="Mandatory">Whether reasoning cannot be disabled for the model.</param>
|
|
||||||
public readonly record struct OpenRouterReasoning(bool DefaultEnabled, bool Mandatory);
|
|
||||||
|
|||||||
@ -0,0 +1,12 @@
|
|||||||
|
namespace AIStudio.Provider.OpenRouter;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The reasoning defaults returned for a model by OpenRouter's model catalog.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The catalog reports more than this, such as the supported efforts and the default one.
|
||||||
|
/// We read only what decides the model's reasoning capability today.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="DefaultEnabled">Whether reasoning is enabled when the request does not configure it.</param>
|
||||||
|
/// <param name="Mandatory">Whether reasoning cannot be disabled for the model.</param>
|
||||||
|
public readonly record struct OpenRouterReasoning(bool DefaultEnabled, bool Mandatory);
|
||||||
@ -15,7 +15,7 @@ public readonly record struct ResponseStreamLine(string Id, string Object, uint
|
|||||||
public bool ContainsContent() => this != default && this.Choices.Count > 0;
|
public bool ContainsContent() => this != default && this.Choices.Count > 0;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ContentStreamChunk GetContent() => new(this.Choices[0].Delta.Content, this.Choices[0].Delta.Thinking, this.GetSources());
|
public ContentStreamChunk GetContent() => new(this.Choices[0].Delta.Content, this.GetSources(), this.Choices[0].Delta.Thinking);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool ContainsSources() => this != default && this.SearchResults.Count > 0;
|
public bool ContainsSources() => this != default && this.SearchResults.Count > 0;
|
||||||
|
|||||||
@ -73,9 +73,7 @@ public static partial class ProviderExtensions
|
|||||||
|
|
||||||
return provider.UsedLLMProvider switch
|
return provider.UsedLLMProvider switch
|
||||||
{
|
{
|
||||||
LLMProviders.OPEN_AI => MergeReasoningStates(
|
LLMProviders.OPEN_AI => GetOpenAICompatibleReasoningState(parameters),
|
||||||
GetOpenAICompatibleReasoningState(parameters),
|
|
||||||
GetReasoningEffortState(parameters)),
|
|
||||||
|
|
||||||
LLMProviders.ANTHROPIC => GetAnthropicReasoningState(parameters),
|
LLMProviders.ANTHROPIC => GetAnthropicReasoningState(parameters),
|
||||||
|
|
||||||
@ -101,7 +99,6 @@ public static partial class ProviderExtensions
|
|||||||
LLMProviders.HELMHOLTZ or
|
LLMProviders.HELMHOLTZ or
|
||||||
LLMProviders.GWDG => MergeReasoningStates(
|
LLMProviders.GWDG => MergeReasoningStates(
|
||||||
GetOpenAICompatibleReasoningState(parameters),
|
GetOpenAICompatibleReasoningState(parameters),
|
||||||
GetReasoningEffortState(parameters),
|
|
||||||
GetQwenReasoningState(parameters),
|
GetQwenReasoningState(parameters),
|
||||||
GetGoogleReasoningState(parameters)),
|
GetGoogleReasoningState(parameters)),
|
||||||
|
|
||||||
@ -119,14 +116,12 @@ public static partial class ProviderExtensions
|
|||||||
|
|
||||||
Host.VLLM => MergeReasoningStates(
|
Host.VLLM => MergeReasoningStates(
|
||||||
GetOpenAICompatibleReasoningState(parameters),
|
GetOpenAICompatibleReasoningState(parameters),
|
||||||
GetReasoningEffortState(parameters),
|
|
||||||
GetVllmReasoningState(parameters),
|
GetVllmReasoningState(parameters),
|
||||||
GetQwenReasoningState(parameters),
|
GetQwenReasoningState(parameters),
|
||||||
GetGoogleReasoningState(parameters)),
|
GetGoogleReasoningState(parameters)),
|
||||||
|
|
||||||
_ => MergeReasoningStates(
|
_ => MergeReasoningStates(
|
||||||
GetOpenAICompatibleReasoningState(parameters),
|
GetOpenAICompatibleReasoningState(parameters),
|
||||||
GetReasoningEffortState(parameters),
|
|
||||||
GetQwenReasoningState(parameters),
|
GetQwenReasoningState(parameters),
|
||||||
GetGoogleReasoningState(parameters)),
|
GetGoogleReasoningState(parameters)),
|
||||||
},
|
},
|
||||||
@ -142,7 +137,8 @@ public static partial class ProviderExtensions
|
|||||||
/// <returns>The detected reasoning configuration state.</returns>
|
/// <returns>The detected reasoning configuration state.</returns>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// OpenAI-compatible providers commonly use a nested <c>reasoning</c> object and/or
|
/// OpenAI-compatible providers commonly use a nested <c>reasoning</c> object and/or
|
||||||
/// a top-level <c>reasoning_effort</c> parameter.
|
/// a top-level <c>reasoning_effort</c> parameter. Both are covered here, so a caller
|
||||||
|
/// does not merge <see cref="GetReasoningEffortState"/> on top of this one again.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
private static ReasoningConfigurationState GetOpenAICompatibleReasoningState(IDictionary<string, object> parameters)
|
private static ReasoningConfigurationState GetOpenAICompatibleReasoningState(IDictionary<string, object> parameters)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -396,10 +396,12 @@ public sealed class AIJobService(SettingsManager settingsManager, MessageBus mes
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
var aiText = state.ChatGenerationRequest.AIText;
|
var aiText = state.ChatGenerationRequest.AIText;
|
||||||
aiText.InitialRemoteWait = false;
|
|
||||||
aiText.IsStreaming = true;
|
aiText.IsStreaming = true;
|
||||||
aiText.ApplyStreamChunk(contentStreamChunk);
|
aiText.ApplyStreamChunk(contentStreamChunk);
|
||||||
|
|
||||||
|
// The waiting animation stays until the answer itself starts, cf. ContentText:
|
||||||
|
aiText.InitialRemoteWait = aiText.Text.Length is 0;
|
||||||
|
|
||||||
if (state.Snapshot.Status is not AIJobStatus.RUNNING)
|
if (state.Snapshot.Status is not AIJobStatus.RUNNING)
|
||||||
{
|
{
|
||||||
state.Snapshot = state.Snapshot with
|
state.Snapshot = state.Snapshot with
|
||||||
|
|||||||
@ -51,7 +51,7 @@ public sealed class ToolCallingLoop(ILogger<ToolCallingLoop> logger) : IToolCall
|
|||||||
if (!string.IsNullOrWhiteSpace(round.ThinkingOutput))
|
if (!string.IsNullOrWhiteSpace(round.ThinkingOutput))
|
||||||
{
|
{
|
||||||
var separator = hasThinkingOutput ? $"{Environment.NewLine}{Environment.NewLine}" : string.Empty;
|
var separator = hasThinkingOutput ? $"{Environment.NewLine}{Environment.NewLine}" : string.Empty;
|
||||||
yield return new ContentStreamChunk(string.Empty, $"{separator}{round.ThinkingOutput}", []);
|
yield return new ContentStreamChunk(string.Empty, [], $"{separator}{round.ThinkingOutput}");
|
||||||
hasThinkingOutput = true;
|
hasThinkingOutput = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user