diff --git a/app/MindWork AI Studio/Provider/Model.cs b/app/MindWork AI Studio/Provider/Model.cs
index 97ca3bbf..5961a2a6 100644
--- a/app/MindWork AI Studio/Provider/Model.cs
+++ b/app/MindWork AI Studio/Provider/Model.cs
@@ -1,3 +1,5 @@
+using System.Text.Json.Serialization;
+
using AIStudio.Tools.PluginSystem;
namespace AIStudio.Provider;
@@ -20,6 +22,12 @@ public readonly record struct Model(string Id, string? DisplayName)
///
public static readonly Model SYSTEM_MODEL = new(SYSTEM_MODEL_ID, null);
+ ///
+ /// The provider-reported default reasoning behavior, when the model catalog supplies it.
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
+ public ModelReasoningBehavior ReasoningBehavior { get; init; }
+
///
/// Checks if this model is the system-configured placeholder.
///
@@ -71,4 +79,15 @@ public readonly record struct Model(string Id, string? DisplayName)
public override int GetHashCode() => this.Id?.GetHashCode(StringComparison.Ordinal) ?? 0;
#endregion
-}
\ No newline at end of file
+}
+
+///
+/// Describes the default reasoning behavior reported by a provider's model catalog.
+///
+public enum ModelReasoningBehavior
+{
+ UNKNOWN,
+ OPTIONAL,
+ DEFAULT_ON,
+ ALWAYS_ON,
+}
diff --git a/app/MindWork AI Studio/Provider/OpenAI/ResponsesToolCallingAdapter.cs b/app/MindWork AI Studio/Provider/OpenAI/ResponsesToolCallingAdapter.cs
index 5d30a623..f039a14b 100644
--- a/app/MindWork AI Studio/Provider/OpenAI/ResponsesToolCallingAdapter.cs
+++ b/app/MindWork AI Studio/Provider/OpenAI/ResponsesToolCallingAdapter.cs
@@ -15,6 +15,8 @@ public sealed class ResponsesToolCallingAdapter(Model chatModel, IList b
IReadOnlyList<(ToolDefinition Definition, IToolImplementation Implementation)> runnableTools,
Func> executeRequestAsync) : IToolCallingProviderAdapter
{
+ private const string ENCRYPTED_REASONING_INCLUDE = "reasoning.encrypted_content";
+
private readonly List internalItems = [];
private ResponsesResponse? lastResponse;
@@ -48,7 +50,7 @@ public sealed class ResponsesToolCallingAdapter(Model chatModel, IList b
Stream = false,
Store = false,
Tools = includeTools ? this.effectiveProviderTools : [],
- AdditionalApiParameters = apiParameters,
+ AdditionalApiParameters = IncludeEncryptedReasoning(apiParameters),
}, token);
if (response is null)
@@ -92,6 +94,37 @@ public sealed class ResponsesToolCallingAdapter(Model chatModel, IList b
Output = content,
});
+ ///
+ /// Request encrypted reasoning content without replacing any additional output data selected by the user.
+ ///
+ ///
+ /// 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.
+ ///
+ private static IDictionary IncludeEncryptedReasoning(IDictionary apiParameters)
+ {
+ var result = new Dictionary(apiParameters);
+ var includeKey = result.Keys.FirstOrDefault(key => key.Equals("include", StringComparison.OrdinalIgnoreCase));
+ if (includeKey is null)
+ {
+ result["include"] = new List { ENCRYPTED_REASONING_INCLUDE };
+ return result;
+ }
+
+ var includedOutput = result[includeKey] switch
+ {
+ IEnumerable values => values.ToList(),
+ string value => new List { value },
+ _ => [],
+ };
+
+ if (!includedOutput.Any(value => string.Equals(value as string, ENCRYPTED_REASONING_INCLUDE, StringComparison.Ordinal)))
+ includedOutput.Add(ENCRYPTED_REASONING_INCLUDE);
+
+ result[includeKey] = includedOutput;
+ return result;
+ }
+
private static IList BuildEffectiveProviderTools(IList providerTools, IReadOnlyList<(ToolDefinition Definition, IToolImplementation Implementation)> runnableTools)
{
var localFunctionNames = runnableTools
diff --git a/app/MindWork AI Studio/Provider/OpenAI/ThinkingContent.cs b/app/MindWork AI Studio/Provider/OpenAI/ThinkingContent.cs
index 114258cd..ab40f996 100644
--- a/app/MindWork AI Studio/Provider/OpenAI/ThinkingContent.cs
+++ b/app/MindWork AI Studio/Provider/OpenAI/ThinkingContent.cs
@@ -9,10 +9,10 @@ internal static class ThinkingContent
{
public static string Get(string? reasoningContent, string? reasoning, IEnumerable? reasoningDetails)
{
- if (!string.IsNullOrWhiteSpace(reasoningContent))
+ if (!string.IsNullOrEmpty(reasoningContent))
return reasoningContent;
- if (!string.IsNullOrWhiteSpace(reasoning))
+ if (!string.IsNullOrEmpty(reasoning))
return reasoning;
if (reasoningDetails is null)
diff --git a/app/MindWork AI Studio/Provider/OpenRouter/OpenRouterModel.cs b/app/MindWork AI Studio/Provider/OpenRouter/OpenRouterModel.cs
index 7cd47a59..c05d2475 100644
--- a/app/MindWork AI Studio/Provider/OpenRouter/OpenRouterModel.cs
+++ b/app/MindWork AI Studio/Provider/OpenRouter/OpenRouterModel.cs
@@ -5,4 +5,24 @@ namespace AIStudio.Provider.OpenRouter;
///
/// The model's ID.
/// The model's human-readable display name.
-public readonly record struct OpenRouterModel(string Id, string? Name);
+/// The model's provider-reported reasoning behavior.
+public readonly record struct OpenRouterModel(string Id, string? Name, OpenRouterReasoning? Reasoning)
+{
+ public Model ToModel() => new(this.Id, this.Name)
+ {
+ ReasoningBehavior = this.Reasoning switch
+ {
+ { Mandatory: true } => ModelReasoningBehavior.ALWAYS_ON,
+ { DefaultEnabled: true } => ModelReasoningBehavior.DEFAULT_ON,
+ not null => ModelReasoningBehavior.OPTIONAL,
+ _ => ModelReasoningBehavior.UNKNOWN,
+ },
+ };
+}
+
+///
+/// The reasoning defaults returned for a model by OpenRouter's model catalog.
+///
+/// Whether reasoning is enabled when the request does not configure it.
+/// Whether reasoning cannot be disabled for the model.
+public readonly record struct OpenRouterReasoning(bool DefaultEnabled, bool Mandatory);
diff --git a/app/MindWork AI Studio/Provider/OpenRouter/ProviderOpenRouter.cs b/app/MindWork AI Studio/Provider/OpenRouter/ProviderOpenRouter.cs
index 842b9fc6..995d3463 100644
--- a/app/MindWork AI Studio/Provider/OpenRouter/ProviderOpenRouter.cs
+++ b/app/MindWork AI Studio/Provider/OpenRouter/ProviderOpenRouter.cs
@@ -116,7 +116,7 @@ public sealed class ProviderOpenRouter() : BaseProvider(LLMProviders.OPEN_ROUTER
storeType,
"models",
modelResponse => modelResponse.Data
- .Select(n => new Model(n.Id, n.Name))
+ .Select(n => n.ToModel())
.Where(model => model.IsChatModel()),
apiKeyProvisional,
requestConfigurator: (request, secretKey) =>
@@ -133,7 +133,7 @@ public sealed class ProviderOpenRouter() : BaseProvider(LLMProviders.OPEN_ROUTER
return this.LoadModelsResponse(
SecretStoreType.EMBEDDING_PROVIDER,
"embeddings/models",
- modelResponse => modelResponse.Data.Select(n => new Model(n.Id, n.Name)),
+ modelResponse => modelResponse.Data.Select(n => n.ToModel()),
apiKeyProvisional,
requestConfigurator: (request, secretKey) =>
{
diff --git a/app/MindWork AI Studio/Settings/ProviderExtensions.Gateway.cs b/app/MindWork AI Studio/Settings/ProviderExtensions.Gateway.cs
index c1123381..ac7898ea 100644
--- a/app/MindWork AI Studio/Settings/ProviderExtensions.Gateway.cs
+++ b/app/MindWork AI Studio/Settings/ProviderExtensions.Gateway.cs
@@ -57,7 +57,29 @@ public static partial class ProviderExtensions
_ => GetModelCapabilitiesOpenSource(bareModel),
};
- return NormalizeForGateway(capabilities);
+ return ApplyGatewayReasoningBehavior(NormalizeForGateway(capabilities), model.ReasoningBehavior);
+ }
+
+ ///
+ /// Applies provider-reported reasoning behavior in preference to model-name heuristics.
+ ///
+ private static List ApplyGatewayReasoningBehavior(List capabilities, ModelReasoningBehavior reasoningBehavior)
+ {
+ if (reasoningBehavior is ModelReasoningBehavior.UNKNOWN)
+ return capabilities;
+
+ capabilities.Remove(Capability.OPTIONAL_REASONING);
+ capabilities.Remove(Capability.REASONING_BY_DEFAULT);
+ capabilities.Remove(Capability.ALWAYS_REASONING);
+ capabilities.Add(reasoningBehavior switch
+ {
+ ModelReasoningBehavior.OPTIONAL => Capability.OPTIONAL_REASONING,
+ ModelReasoningBehavior.DEFAULT_ON => Capability.REASONING_BY_DEFAULT,
+ ModelReasoningBehavior.ALWAYS_ON => Capability.ALWAYS_REASONING,
+ _ => throw new ArgumentOutOfRangeException(nameof(reasoningBehavior), reasoningBehavior, null),
+ });
+
+ return capabilities;
}
///
@@ -78,4 +100,4 @@ public static partial class ProviderExtensions
return capabilities;
}
-}
\ No newline at end of file
+}
diff --git a/app/MindWork AI Studio/Settings/ProviderExtensions.Reasoning.cs b/app/MindWork AI Studio/Settings/ProviderExtensions.Reasoning.cs
index 25d687b4..c239f257 100644
--- a/app/MindWork AI Studio/Settings/ProviderExtensions.Reasoning.cs
+++ b/app/MindWork AI Studio/Settings/ProviderExtensions.Reasoning.cs
@@ -146,21 +146,55 @@ public static partial class ProviderExtensions
///
private static ReasoningConfigurationState GetOpenAICompatibleReasoningState(IDictionary parameters)
{
- var reasoningState = ReasoningConfigurationState.NOT_CONFIGURED;
+ var states = new List();
if (TryGetParameter(parameters, "reasoning", out var reasoning))
{
- reasoningState = reasoning switch
+ if (reasoning is IDictionary reasoningObject)
{
- IDictionary reasoningObject when TryGetParameter(reasoningObject, "effort", out var effort) => GetLevelState(effort),
- IDictionary reasoningObject when TryGetParameter(reasoningObject, "summary", out var summary) => GetLevelState(summary),
- IDictionary => ReasoningConfigurationState.NOT_CONFIGURED,
- _ => GetLevelState(reasoning),
- };
+ if (TryGetParameter(reasoningObject, "effort", out var effort))
+ states.Add(GetLevelState(effort));
+
+ if (TryGetParameter(reasoningObject, "enabled", out var enabled))
+ states.Add(GetLevelState(enabled));
+
+ if (TryGetParameter(reasoningObject, "max_tokens", out var maxTokens))
+ states.Add(GetBudgetState(maxTokens));
+
+ if (TryGetParameter(reasoningObject, "summary", out var summary))
+ states.Add(GetReasoningSummaryState(summary));
+
+ if (TryGetParameter(reasoningObject, "generate_summary", out var generateSummary))
+ states.Add(GetReasoningSummaryState(generateSummary));
+ }
+ else
+ states.Add(GetLevelState(reasoning));
}
- return MergeReasoningStates(reasoningState, GetReasoningEffortState(parameters));
+ if (TryGetParameter(parameters, "include_reasoning", out var includeReasoning) &&
+ GetLevelState(includeReasoning) is ReasoningConfigurationState.EXPLICITLY_ENABLED)
+ states.Add(ReasoningConfigurationState.EXPLICITLY_ENABLED);
+
+ states.Add(GetReasoningEffortState(parameters));
+ return MergeReasoningStates(states);
}
+ ///
+ /// Detect summary settings that imply reasoning is enabled.
+ ///
+ ///
+ /// Turning a summary off controls visibility only and does not disable the model's reasoning.
+ ///
+ private static ReasoningConfigurationState GetReasoningSummaryState(object? value) => value switch
+ {
+ string text when text.Equals("auto", StringComparison.OrdinalIgnoreCase) ||
+ text.Equals("concise", StringComparison.OrdinalIgnoreCase) ||
+ text.Equals("detailed", StringComparison.OrdinalIgnoreCase)
+ => ReasoningConfigurationState.EXPLICITLY_ENABLED,
+
+ true => ReasoningConfigurationState.EXPLICITLY_ENABLED,
+ _ => ReasoningConfigurationState.NOT_CONFIGURED,
+ };
+
///
/// Detect a top-level reasoning_effort parameter.
///
@@ -444,6 +478,7 @@ public static partial class ProviderExtensions
text.Equals("minimal", StringComparison.OrdinalIgnoreCase) ||
text.Equals("medium", StringComparison.OrdinalIgnoreCase) ||
text.Equals("high", StringComparison.OrdinalIgnoreCase) ||
+ text.Equals("xhigh", StringComparison.OrdinalIgnoreCase) ||
text.Equals("max", StringComparison.OrdinalIgnoreCase);
}
@@ -516,4 +551,4 @@ public static partial class ProviderExtensions
value = parameters[foundKey];
return true;
}
-}
\ No newline at end of file
+}