Merge branch 'main' into chunk-data

This commit is contained in:
PaulKoudelka committed 2026-08-04 18:01:25 +02:00
commit 9932b5870b
484 files changed
+45549 -2467

No files matched your search

@@ -25,8 +25,10 @@ public enum ConfigurableAssistant
ERI_ASSISTANT,
DOCUMENT_ANALYSIS_ASSISTANT,
SLIDE_BUILDER_ASSISTANT,
LOG_VIEWER_ASSISTANT,
VISUAL_BRIEFING_ASSISTANT,
// ReSharper disable InconsistentNaming
I18N_ASSISTANT,
// ReSharper restore InconsistentNaming
}
}
@@ -74,6 +74,15 @@ public static class ConfigurationSelectDataFactory
yield return new (TB("Check every week"), UpdateInterval.WEEKLY);
}
public static IEnumerable<ConfigurationSelectData<UpdateInterval>> GetManagedUpdateIntervalData(UpdateInterval currentValue)
{
if (currentValue is UpdateInterval.DISABLE_UPDATES)
yield return new(TB("Updates disabled by your organization"), UpdateInterval.DISABLE_UPDATES);
foreach (var option in GetUpdateIntervalData())
yield return option;
}
public static IEnumerable<ConfigurationSelectData<UpdateInstallation>> GetUpdateBehaviourData()
{
yield return new(TB("Install updates manually"), UpdateInstallation.MANUAL);
@@ -125,9 +125,9 @@ public sealed class Data
public DataTextContentCleaner TextContentCleaner { get; init; } = new();
public DataAgentDataSourceSelection AgentDataSourceSelection { get; init; } = new();
public DataAgentDataSourceSelection AgentDataSourceSelection { get; init; } = new(x => x.AgentDataSourceSelection);
public DataAgentRetrievalContextValidation AgentRetrievalContextValidation { get; init; } = new();
public DataAgentRetrievalContextValidation AgentRetrievalContextValidation { get; init; } = new(x => x.AgentRetrievalContextValidation);
public DataDataSourceIndexing DataSourceIndexing { get; init; } = new();
@@ -144,6 +144,11 @@ public sealed class Data
public DataEMail EMail { get; init; } = new();
public DataSlideBuilder SlideBuilder { get; init; } = new();
/// <summary>
/// Gets the managed Visual Briefing Assistant defaults.
/// </summary>
public DataVisualBriefing VisualBriefing { get; init; } = new(x => x.VisualBriefing);
public DataLegalCheck LegalCheck { get; init; } = new();
@@ -1,14 +1,23 @@
using System.Linq.Expressions;
namespace AIStudio.Settings.DataModel;
public sealed class DataAgentDataSourceSelection
public sealed class DataAgentDataSourceSelection(Expression<Func<Data, DataAgentDataSourceSelection>>? configSelection = null)
{
/// <summary>
/// The default constructor for the JSON deserializer.
/// </summary>
public DataAgentDataSourceSelection() : this(null)
{
}
/// <summary>
/// Preselect any data source selection options?
/// </summary>
public bool PreselectAgentOptions { get; set; }
public bool PreselectAgentOptions { get; set; } = ManagedConfiguration.Register(configSelection, n => n.PreselectAgentOptions, false);
/// <summary>
/// Preselect a data source selection provider?
/// </summary>
public string PreselectedAgentProvider { get; set; } = string.Empty;
public string PreselectedAgentProvider { get; set; } = ManagedConfiguration.Register(configSelection, n => n.PreselectedAgentProvider, string.Empty);
}
@@ -1,24 +1,33 @@
using System.Linq.Expressions;
namespace AIStudio.Settings.DataModel;
public sealed class DataAgentRetrievalContextValidation
public sealed class DataAgentRetrievalContextValidation(Expression<Func<Data, DataAgentRetrievalContextValidation>>? configSelection = null)
{
/// <summary>
/// The default constructor for the JSON deserializer.
/// </summary>
public DataAgentRetrievalContextValidation() : this(null)
{
}
/// <summary>
/// Enable the retrieval context validation agent?
/// </summary>
public bool EnableRetrievalContextValidation { get; set; }
public bool EnableRetrievalContextValidation { get; set; } = ManagedConfiguration.Register(configSelection, n => n.EnableRetrievalContextValidation, false);
/// <summary>
/// Preselect any retrieval context validation options?
/// </summary>
public bool PreselectAgentOptions { get; set; }
public bool PreselectAgentOptions { get; set; } = ManagedConfiguration.Register(configSelection, n => n.PreselectAgentOptions, false);
/// <summary>
/// Preselect a retrieval context validation provider?
/// </summary>
public string PreselectedAgentProvider { get; set; } = string.Empty;
public string PreselectedAgentProvider { get; set; } = ManagedConfiguration.Register(configSelection, n => n.PreselectedAgentProvider, string.Empty);
/// <summary>
/// Configure how many parallel validations to run.
/// </summary>
public int NumParallelValidations { get; set; } = 3;
public int NumParallelValidations { get; set; } = ManagedConfiguration.Register(configSelection, n => n.NumParallelValidations, 3);
}
@@ -67,6 +67,16 @@ public sealed class DataApp(Expression<Func<Data, DataApp>>? configSelection = n
/// </summary>
public bool ShowQuickStartGuide { get; set; } = ManagedConfiguration.Register(configSelection, n => n.ShowQuickStartGuide, true);
/// <summary>
/// Should the last changelog be visible on the home page?
/// </summary>
public bool ShowLastChangelog { get; set; } = ManagedConfiguration.Register(configSelection, n => n.ShowLastChangelog, true);
/// <summary>
/// Should the vision panel be visible on the home page?
/// </summary>
public bool ShowVision { get; set; } = ManagedConfiguration.Register(configSelection, n => n.ShowVision, true);
/// <summary>
/// The visibility setting for previews features.
/// </summary>
@@ -40,4 +40,9 @@ public sealed class DataAssistantPluginAudit(Expression<Func<Data, DataAssistant
/// If true, the security audit will be hidden from the user and done in the background
/// </summary>
public bool AutomaticallyAuditAssistants { get; set; } = ManagedConfiguration.Register(configSelection, n => n.AutomaticallyAuditAssistants, false);
/// <summary>
/// Enterprise-managed assistant plugin hashes that are approved without requiring a user audit.
/// </summary>
public IList<DataAssistantPluginEnterpriseApproval> EnterpriseApprovedPlugins { get; set; } = ManagedConfiguration.Register(configSelection, n => n.EnterpriseApprovedPlugins, []);
}
@@ -0,0 +1,13 @@
namespace AIStudio.Settings.DataModel;
/// <summary>
/// Enterprise-managed approval entry for an assistant plugin hash.
/// </summary>
public sealed class DataAssistantPluginEnterpriseApproval
{
public string PluginHash { get; init; } = string.Empty;
public string DisplayName { get; init; } = string.Empty;
public string Comment { get; init; } = string.Empty;
public string ApprovedBy { get; init; } = string.Empty;
public DateTimeOffset? ApprovedAtUtc { get; init; }
}
@@ -29,7 +29,7 @@ public sealed class DataChat(Expression<Func<Data, DataChat>>? configSelection =
/// <summary>
/// Defines the data source behavior when sending assistant results to a chat.
/// </summary>
public SendToChatDataSourceBehavior SendToChatDataSourceBehavior { get; set; } = SendToChatDataSourceBehavior.NO_DATA_SOURCES;
public SendToChatDataSourceBehavior SendToChatDataSourceBehavior { get; set; } = ManagedConfiguration.Register(configSelection, n => n.SendToChatDataSourceBehavior, SendToChatDataSourceBehavior.NO_DATA_SOURCES);
/// <summary>
/// Preselect any chat options?
@@ -51,10 +51,47 @@ public sealed class DataChat(Expression<Func<Data, DataChat>>? configSelection =
/// </summary>
public string PreselectedChatTemplate { get; set; } = ManagedConfiguration.Register(configSelection, n => n.PreselectedChatTemplate, string.Empty);
/// <summary>
/// Whether data sources are disabled by default for new chats.
/// </summary>
public bool PreselectedDataSourcesDisabled { get; set; } = ManagedConfiguration.Register(configSelection, n => n.PreselectedDataSourcesDisabled, true);
/// <summary>
/// Whether data sources should be selected automatically by default for new chats.
/// </summary>
public bool PreselectedDataSourcesAutomaticSelection { get; set; } = ManagedConfiguration.Register(configSelection, n => n.PreselectedDataSourcesAutomaticSelection, false);
/// <summary>
/// Whether retrieved data should be validated automatically by default for new chats.
/// </summary>
public bool PreselectedDataSourcesAutomaticValidation { get; set; } = ManagedConfiguration.Register(configSelection, n => n.PreselectedDataSourcesAutomaticValidation, false);
/// <summary>
/// The data source IDs that should be preselected by default for new chats.
/// </summary>
public List<string> PreselectedDataSourceIds { get; set; } = ManagedConfiguration.Register(configSelection, n => n.PreselectedDataSourceIds, []);
/// <summary>
/// Should we preselect data sources options for a created chat?
/// </summary>
public DataSourceOptions PreselectedDataSourceOptions { get; set; } = new();
// Compatibility shim: legacy settings used this nested object. See documentation/compatibility-shims/2026-07-chat-data-source-options.md; remove after 2027-01-05.
public DataSourceOptions PreselectedDataSourceOptions
{
get => new()
{
DisableDataSources = this.PreselectedDataSourcesDisabled,
AutomaticDataSourceSelection = this.PreselectedDataSourcesAutomaticSelection,
AutomaticValidation = this.PreselectedDataSourcesAutomaticValidation,
PreselectedDataSourceIds = [..this.PreselectedDataSourceIds],
};
set
{
this.PreselectedDataSourcesDisabled = value.DisableDataSources;
this.PreselectedDataSourcesAutomaticSelection = value.AutomaticDataSourceSelection;
this.PreselectedDataSourcesAutomaticValidation = value.AutomaticValidation;
this.PreselectedDataSourceIds = [..value.PreselectedDataSourceIds];
}
}
/// <summary>
/// Should we show the latest message after loading? When false, we show the first (aka oldest) message.
@@ -0,0 +1,75 @@
using System.Linq.Expressions;
using AIStudio.Assistants.SlideBuilder;
using AIStudio.Provider;
namespace AIStudio.Settings.DataModel;
/// <summary>
/// Stores managed default settings for the Visual Briefing Assistant.
/// </summary>
/// <param name="configSelection">The managed-configuration selector.</param>
public sealed class DataVisualBriefing(Expression<Func<Data, DataVisualBriefing>>? configSelection = null)
{
/// <summary>
/// Initializes an unmanaged Visual Briefing settings instance.
/// </summary>
public DataVisualBriefing() : this(null)
{
}
/// <summary>
/// Gets or sets the preselected profile identifier.
/// </summary>
public string PreselectedProfile { get; set; } = ManagedConfiguration.Register(configSelection, value => value.PreselectedProfile, string.Empty);
/// <summary>
/// Gets or sets the preselected provider identifier.
/// </summary>
public string PreselectedProvider { get; set; } = ManagedConfiguration.Register(configSelection, value => value.PreselectedProvider, string.Empty);
/// <summary>
/// Gets or sets the default target language.
/// </summary>
public CommonLanguages PreselectedTargetLanguage { get; set; } = ManagedConfiguration.Register(configSelection, value => value.PreselectedTargetLanguage, CommonLanguages.EN_US);
/// <summary>
/// Gets or sets the default free-form target language.
/// </summary>
public string PreselectedOtherLanguage { get; set; } = ManagedConfiguration.Register(configSelection, value => value.PreselectedOtherLanguage, string.Empty);
/// <summary>
/// Gets or sets the default audience profile.
/// </summary>
public AudienceProfile PreselectedAudienceProfile { get; set; } = ManagedConfiguration.Register(configSelection, value => value.PreselectedAudienceProfile, AudienceProfile.UNSPECIFIED);
/// <summary>
/// Gets or sets the default audience age group.
/// </summary>
public AudienceAgeGroup PreselectedAudienceAgeGroup { get; set; } = ManagedConfiguration.Register(configSelection, value => value.PreselectedAudienceAgeGroup, AudienceAgeGroup.UNSPECIFIED);
/// <summary>
/// Gets or sets the default audience organizational level.
/// </summary>
public AudienceOrganizationalLevel PreselectedAudienceOrganizationalLevel { get; set; } = ManagedConfiguration.Register(configSelection, value => value.PreselectedAudienceOrganizationalLevel, AudienceOrganizationalLevel.UNSPECIFIED);
/// <summary>
/// Gets or sets the default audience expertise.
/// </summary>
public AudienceExpertise PreselectedAudienceExpertise { get; set; } = ManagedConfiguration.Register(configSelection, value => value.PreselectedAudienceExpertise, AudienceExpertise.UNSPECIFIED);
/// <summary>
/// Gets or sets whether generated briefings show source references by default.
/// </summary>
public bool ShowSourceReferences { get; set; } = ManagedConfiguration.Register(configSelection, value => value.ShowSourceReferences, true);
/// <summary>
/// Gets or sets whether visual assets are optimized by default.
/// </summary>
public bool OptimizeImages { get; set; } = ManagedConfiguration.Register(configSelection, value => value.OptimizeImages, true);
/// <summary>
/// Gets or sets the minimum confidence accepted for the selected provider.
/// </summary>
public ConfidenceLevel MinimumProviderConfidence { get; set; } = ManagedConfiguration.Register(configSelection, value => value.MinimumProviderConfidence, ConfidenceLevel.NONE);
}
@@ -15,4 +15,6 @@ public enum PreviewFeatures
PRE_READ_PDF_2025,
PRE_DOCUMENT_ANALYSIS_2025,
PRE_SPEECH_TO_TEXT_2026,
PRE_META_ASSISTANT_V1,
PRE_VISUAL_BRIEFING_ASSISTANT_2026,
}
@@ -15,6 +15,8 @@ public static class PreviewFeaturesExtensions
PreviewFeatures.PRE_READ_PDF_2025 => TB("Read PDF: Preview of our PDF reading system where you can read and extract text from PDF files"),
PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025 => TB("Document Analysis: Preview of our document analysis system where you can analyze and extract information from documents"),
PreviewFeatures.PRE_SPEECH_TO_TEXT_2026 => TB("Transcription: Convert recordings and audio files into text"),
PreviewFeatures.PRE_META_ASSISTANT_V1 => TB("Assistant Builder: Generate and install assistant plugins"),
PreviewFeatures.PRE_VISUAL_BRIEFING_ASSISTANT_2026 => TB("Visual Briefing Assistant: Turn source material into an interactive briefing"),
_ => TB("Unknown preview feature")
};
@@ -12,6 +12,7 @@ public static class PreviewVisibilityExtensions
if (visibility >= PreviewVisibility.BETA)
{
features.Add(PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025);
features.Add(PreviewFeatures.PRE_META_ASSISTANT_V1);
}
if (visibility >= PreviewVisibility.ALPHA)
@@ -21,6 +22,7 @@ public static class PreviewVisibilityExtensions
if (visibility >= PreviewVisibility.PROTOTYPE)
{
features.Add(PreviewFeatures.PRE_RAG_2024);
features.Add(PreviewFeatures.PRE_VISUAL_BRIEFING_ASSISTANT_2026);
}
if (visibility >= PreviewVisibility.EXPERIMENTAL)
@@ -3,6 +3,7 @@ namespace AIStudio.Settings.DataModel;
public enum UpdateInterval
{
NO_CHECK,
DISABLE_UPDATES,
ONCE_STARTUP,
HOURLY,
DAILY,
@@ -435,7 +435,9 @@ public static partial class ManagedConfiguration
if(dryRun)
return successful;
return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue);
var settingName = SettingName(propertyExpression);
var managedMode = ReadManagedConfigurationMode(propertyExpression, settings);
return HandleParsedScalarValue(configPluginId, dryRun, successful, configMeta, configuredValue, managedMode, settingName);
}
/// <summary>
@@ -1026,6 +1028,10 @@ public static partial class ManagedConfiguration
.Cast<object>()
.OrderBy(key => key.ToString(), StringComparer.Ordinal)
.Select(key => $"{key}:{dictionary[key]}")),
System.Collections.IEnumerable enumerable => string.Join(";", enumerable
.Cast<object>()
.Select(item => item.ToString() ?? string.Empty)
.Order(StringComparer.Ordinal)),
IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture),
_ => value.ToString() ?? string.Empty,
@@ -358,17 +358,18 @@ public static partial class ManagedConfiguration
if (!TryGet(configSelection, propertyExpression, out var configMeta))
return false;
if (configMeta.ManagedMode is ManagedConfigurationMode.EDITABLE_DEFAULT)
return CleanupEditableDefaultState(configMeta, SettingName(propertyExpression), availablePlugins.ToList());
if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
return false;
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId);
if (plugin is null)
{
configMeta.ResetLockedConfiguration();
return true;
}
if (plugin is not null)
return false;
return false;
configMeta.ResetLockedConfiguration();
return true;
}
public static bool IsConfigurationLeftOver<TClass, TValue>(
@@ -35,6 +35,7 @@ public sealed record Provider(
HFInferenceProvider HFInferenceProvider = HFInferenceProvider.NONE,
string AdditionalJsonApiParameters = "",
string TokenizerPath = "") : ConfigurationBaseObject, ISecretId
ProviderCapabilityOverrides? CapabilityOverrides = null) : ConfigurationBaseObject, ISecretId
{
private static readonly ILogger<Provider> LOGGER = Program.LOGGER_FACTORY.CreateLogger<Provider>();
@@ -159,6 +160,7 @@ public sealed record Provider(
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid tokenizer path. (Plugin ID: {configPluginId})");
tokenizerPath = string.Empty;
}
var capabilityOverrides = ProviderCapabilityOverrides.TryParseFromLuaTable(idx, table, configPluginId, LOGGER);
provider = new Provider
{
@@ -175,6 +177,7 @@ public sealed record Provider(
HFInferenceProvider = hfInferenceProvider,
AdditionalJsonApiParameters = additionalJsonApiParameters,
TokenizerPath = tokenizerPath,
CapabilityOverrides = capabilityOverrides,
};
// Handle encrypted API key if present:
@@ -250,6 +253,8 @@ public sealed record Provider(
""";
}
var capabilityOverridesLine = this.CapabilityOverrides?.ExportAsLuaTable(" ") ?? string.Empty;
return $$"""
CONFIG["LLM_PROVIDERS"][#CONFIG["LLM_PROVIDERS"]+1] = {
["Id"] = "{{Guid.NewGuid().ToString()}}",
@@ -263,6 +268,7 @@ public sealed record Provider(
{{hfInferenceProviderLine}}
{{apiKeyLine}}
["AdditionalJsonApiParameters"] = "{{LuaTools.EscapeLuaString(this.AdditionalJsonApiParameters)}}",
{{capabilityOverridesLine}}
["Model"] = {
["Id"] = "{{LuaTools.EscapeLuaString(this.Model.Id)}}",
["DisplayName"] = "{{LuaTools.EscapeLuaString(this.Model.DisplayName ?? this.Model.Id)}}",
@@ -0,0 +1,207 @@
using System.Text;
using System.Text.Json.Serialization;
using AIStudio.Provider;
using Lua;
using LuaTable = Lua.LuaTable;
namespace AIStudio.Settings;
/// <summary>
/// Optional expert capability overrides for a configured LLM provider.
/// Missing values keep the automatic capability detection result.
/// </summary>
public sealed record ProviderCapabilityOverrides
{
private static readonly IReadOnlyList<Capability> SUPPORTED_CAPABILITIES =
[
Capability.AUDIO_INPUT,
Capability.MULTIPLE_IMAGE_INPUT,
Capability.SPEECH_INPUT,
Capability.VIDEO_INPUT,
Capability.OPTIONAL_REASONING,
Capability.ALWAYS_REASONING,
Capability.REASONING_BY_DEFAULT
];
[JsonPropertyName("AUDIO_INPUT")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? AudioInput { get; init; }
[JsonPropertyName("MULTIPLE_IMAGE_INPUT")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? MultipleImageInput { get; init; }
[JsonPropertyName("SPEECH_INPUT")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? SpeechInput { get; init; }
[JsonPropertyName("VIDEO_INPUT")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? VideoInput { get; init; }
[JsonPropertyName("OPTIONAL_REASONING")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? OptionalReasoning { get; init; }
[JsonPropertyName("ALWAYS_REASONING")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? AlwaysReasoning { get; init; }
[JsonPropertyName("REASONING_BY_DEFAULT")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? ReasoningByDefault { get; init; }
[JsonIgnore]
public bool HasOverrides =>
this.AudioInput is not null ||
this.MultipleImageInput is not null ||
this.SpeechInput is not null ||
this.VideoInput is not null ||
this.OptionalReasoning is not null ||
this.AlwaysReasoning is not null ||
this.ReasoningByDefault is not null;
public bool? GetOverride(Capability capability) => capability switch
{
Capability.AUDIO_INPUT => this.AudioInput,
Capability.MULTIPLE_IMAGE_INPUT => this.MultipleImageInput,
Capability.SPEECH_INPUT => this.SpeechInput,
Capability.VIDEO_INPUT => this.VideoInput,
Capability.OPTIONAL_REASONING => this.OptionalReasoning,
Capability.ALWAYS_REASONING => this.AlwaysReasoning,
Capability.REASONING_BY_DEFAULT => this.ReasoningByDefault,
_ => null
};
public ProviderCapabilityOverrides SetOverride(Capability capability, bool? value) => capability switch
{
Capability.AUDIO_INPUT => this with { AudioInput = value },
Capability.MULTIPLE_IMAGE_INPUT => this with { MultipleImageInput = value },
Capability.SPEECH_INPUT => this with { SpeechInput = value },
Capability.VIDEO_INPUT => this with { VideoInput = value },
Capability.OPTIONAL_REASONING => this with { OptionalReasoning = value },
Capability.ALWAYS_REASONING => this with { AlwaysReasoning = value },
Capability.REASONING_BY_DEFAULT => this with { ReasoningByDefault = value },
_ => this
};
public List<Capability> ApplyTo(IEnumerable<Capability> automaticCapabilities)
{
var mergedCapabilities = automaticCapabilities.Distinct().ToList();
foreach (var capability in SUPPORTED_CAPABILITIES)
{
var overrideValue = this.GetOverride(capability);
if (overrideValue == true && !mergedCapabilities.Contains(capability))
mergedCapabilities.Add(capability);
else if (overrideValue == false)
mergedCapabilities.Remove(capability);
}
this.NormalizeReasoningCapabilities(mergedCapabilities);
return mergedCapabilities;
}
private void NormalizeReasoningCapabilities(List<Capability> capabilities)
{
if (this.AlwaysReasoning == true ||
this.AlwaysReasoning is not false &&
this.OptionalReasoning is not true &&
this.ReasoningByDefault is not true &&
capabilities.Contains(Capability.ALWAYS_REASONING))
{
capabilities.Remove(Capability.OPTIONAL_REASONING);
capabilities.Remove(Capability.REASONING_BY_DEFAULT);
return;
}
if (this.AlwaysReasoning == false ||
this.OptionalReasoning == true ||
this.ReasoningByDefault == true)
capabilities.Remove(Capability.ALWAYS_REASONING);
if (this.OptionalReasoning == false)
{
capabilities.Remove(Capability.REASONING_BY_DEFAULT);
return;
}
if (this.ReasoningByDefault == true && !capabilities.Contains(Capability.OPTIONAL_REASONING))
capabilities.Add(Capability.OPTIONAL_REASONING);
if (!capabilities.Contains(Capability.OPTIONAL_REASONING))
capabilities.Remove(Capability.REASONING_BY_DEFAULT);
}
public string ExportAsLuaTable(string indentation)
{
if (!this.HasOverrides)
return string.Empty;
var builder = new StringBuilder();
builder.AppendLine($@"{indentation}[""CapabilityOverrides""] = {{");
foreach (var capability in SUPPORTED_CAPABILITIES)
{
var overrideValue = this.GetOverride(capability);
if (overrideValue is null)
continue;
builder.AppendLine($@"{indentation} [""{capability}""] = {overrideValue.Value.ToString().ToLowerInvariant()},");
}
builder.Append($@"{indentation}}},");
return builder.ToString();
}
public static ProviderCapabilityOverrides? TryParseFromLuaTable(int idx, LuaTable providerTable, Guid configPluginId, ILogger logger)
{
if (!providerTable.TryGetValue("CapabilityOverrides", out var capabilityOverridesValue))
return null;
if (capabilityOverridesValue.Type is not LuaValueType.Table || !capabilityOverridesValue.TryRead<LuaTable>(out var capabilityOverridesTable))
{
logger.LogWarning("The configured provider {ProviderIndex} contains an invalid CapabilityOverrides table. Automatic capability detection will be used instead. (Plugin ID: {PluginId})", idx, configPluginId);
return null;
}
var result = new ProviderCapabilityOverrides();
var previousKey = LuaValue.Nil;
while (capabilityOverridesTable.TryGetNext(previousKey, out var pair))
{
previousKey = pair.Key;
if (!pair.Key.TryRead<string>(out var keyText))
{
logger.LogWarning("The configured provider {ProviderIndex} contains a CapabilityOverrides entry with a non-string key. The entry will be ignored. (Plugin ID: {PluginId})", idx, configPluginId);
continue;
}
if (!TryParseSupportedCapability(keyText, out var capability))
{
logger.LogWarning("The configured provider {ProviderIndex} contains an unsupported capability override '{CapabilityKey}'. The entry will be ignored. (Plugin ID: {PluginId})", idx, keyText, configPluginId);
continue;
}
if (!pair.Value.TryRead<bool>(out var overrideValue))
{
logger.LogWarning("The configured provider {ProviderIndex} contains a non-boolean capability override for '{CapabilityKey}'. Automatic capability detection will be used for that capability. (Plugin ID: {PluginId})", idx, keyText, configPluginId);
continue;
}
result = result.SetOverride(capability, overrideValue);
}
return result.HasOverrides ? result : null;
}
private static bool TryParseSupportedCapability(string capabilityKey, out Capability capability)
{
capability = Capability.NONE;
if (!Enum.TryParse(capabilityKey, true, out capability))
return false;
return SUPPORTED_CAPABILITIES.Contains(capability);
}
}
@@ -7,6 +7,16 @@ public static partial class ProviderExtensions
private static List<Capability> GetModelCapabilitiesAnthropic(Model model)
{
var modelName = model.Id.ToLowerInvariant().AsSpan();
// Claude Fable 5 and Mythos 5 always use adaptive thinking:
if(modelName.StartsWith("claude-fable-5") || modelName.StartsWith("claude-mythos-5"))
return [
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
// Claude 4.x models:
if(modelName.StartsWith("claude-opus-4") || modelName.StartsWith("claude-sonnet-4"))
@@ -10,6 +10,38 @@ public static partial class ProviderExtensions
if (modelName.IndexOf("gemini-") is not -1)
{
// Chat-compatible Gemini 3.x reasoning models:
if (modelName is "gemini-3.5-flash" ||
modelName is "gemini-flash-latest" ||
modelName is "gemini-3.1-flash-lite" ||
modelName is "gemini-3-flash-preview" ||
modelName is "gemini-pro-latest" ||
modelName is "gemini-3.1-pro-preview" ||
modelName is "gemini-3.1-pro-preview-customtools")
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.AUDIO_INPUT,
Capability.SPEECH_INPUT, Capability.VIDEO_INPUT,
Capability.TEXT_OUTPUT,
Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
// Gemini 2.5 Flash Lite supports thinking, but the default is off:
if (modelName.IndexOf("gemini-2.5-flash-lite") is not -1)
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.AUDIO_INPUT,
Capability.SPEECH_INPUT, Capability.VIDEO_INPUT,
Capability.TEXT_OUTPUT,
Capability.OPTIONAL_REASONING, Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
// Reasoning models:
if (modelName.IndexOf("gemini-2.5") is not -1)
return
@@ -175,6 +175,28 @@ public static partial class ProviderExtensions
Capability.WEB_SEARCH,
Capability.RESPONSES_API, Capability.CHAT_COMPLETION_API,
];
if(modelName is "gpt-5.5" || modelName.StartsWith("gpt-5.5-"))
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT, Capability.IMAGE_OUTPUT,
Capability.FUNCTION_CALLING, Capability.OPTIONAL_REASONING, Capability.REASONING_BY_DEFAULT,
Capability.WEB_SEARCH,
Capability.RESPONSES_API, Capability.CHAT_COMPLETION_API,
];
if(modelName is "gpt-5.6" || modelName.StartsWith("gpt-5.6-"))
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.FUNCTION_CALLING, Capability.OPTIONAL_REASONING, Capability.REASONING_BY_DEFAULT,
Capability.WEB_SEARCH,
Capability.RESPONSES_API, Capability.CHAT_COMPLETION_API,
];
return
[
@@ -109,7 +109,7 @@ public static partial class ProviderExtensions
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING,
Capability.OPTIONAL_REASONING, Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
@@ -121,7 +121,7 @@ public static partial class ProviderExtensions
Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING,
Capability.OPTIONAL_REASONING, Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
@@ -158,6 +158,22 @@ public static partial class ProviderExtensions
Capability.CHAT_COMPLETION_API,
];
// Mistral medium 3.5:
if (modelName.IndexOf("mistral-medium-3.5") is not -1)
return
[
Capability.TEXT_INPUT,
Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.OPTIONAL_REASONING,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
if (modelName.IndexOf("mistral-3") is not -1 ||
modelName.IndexOf("mistral-large-3") is not -1)
return
@@ -357,4 +373,4 @@ public static partial class ProviderExtensions
Capability.CHAT_COMPLETION_API,
];
}
}
}
@@ -0,0 +1,516 @@
using AIStudio.Provider;
using Host = AIStudio.Provider.SelfHosted.Host;
namespace AIStudio.Settings;
public static partial class ProviderExtensions
{
/// <summary>
/// The reasoning-related intent found in the configured additional API parameters.
/// </summary>
private enum ReasoningConfigurationState
{
/// <summary>
/// No recognized reasoning parameter was found.
/// </summary>
NOT_CONFIGURED,
/// <summary>
/// A recognized reasoning parameter explicitly enables reasoning.
/// </summary>
EXPLICITLY_ENABLED,
/// <summary>
/// A recognized reasoning parameter explicitly disables reasoning.
/// </summary>
EXPLICITLY_DISABLED,
}
/// <summary>
/// Get the effective reasoning indicator state for the configured provider instance.
/// </summary>
/// <param name="provider">The configured provider.</param>
/// <returns>The effective reasoning indicator state.</returns>
/// <remarks>
/// This combines static model capabilities with per-provider additional API parameters.
/// For default-on models, an explicit disabling parameter hides the icon; for optional
/// models, an explicit enabling parameter is required before the icon is shown.
/// </remarks>
public static ReasoningIndicatorState GetReasoningIndicatorState(this Provider provider)
{
var capabilities = provider.GetModelCapabilities();
if (capabilities.Contains(Capability.ALWAYS_REASONING))
return ReasoningIndicatorState.ALWAYS_ON;
var reasoningConfigurationState = GetReasoningConfigurationState(provider);
if (capabilities.Contains(Capability.REASONING_BY_DEFAULT))
{
return reasoningConfigurationState switch
{
ReasoningConfigurationState.EXPLICITLY_DISABLED => ReasoningIndicatorState.NONE,
ReasoningConfigurationState.EXPLICITLY_ENABLED => ReasoningIndicatorState.CONFIGURED,
_ => ReasoningIndicatorState.DEFAULT_ON,
};
}
if (capabilities.Contains(Capability.OPTIONAL_REASONING) &&
reasoningConfigurationState is ReasoningConfigurationState.EXPLICITLY_ENABLED)
return ReasoningIndicatorState.CONFIGURED;
return ReasoningIndicatorState.NONE;
}
/// <summary>
/// Parse additional API parameters and dispatch them to provider-specific reasoning detectors.
/// </summary>
/// <param name="provider">The configured provider whose additional API parameters should be inspected.</param>
/// <returns>The explicit reasoning configuration state, or <see cref="ReasoningConfigurationState.NOT_CONFIGURED"/> if nothing known was found.</returns>
private static ReasoningConfigurationState GetReasoningConfigurationState(Provider provider)
{
if (!AdditionalApiParametersParser.TryParse(provider.AdditionalJsonApiParameters, out var parameters, out _))
return ReasoningConfigurationState.NOT_CONFIGURED;
return provider.UsedLLMProvider switch
{
LLMProviders.OPEN_AI => MergeReasoningStates(
GetOpenAICompatibleReasoningState(parameters),
GetReasoningEffortState(parameters)),
LLMProviders.ANTHROPIC => GetAnthropicReasoningState(parameters),
LLMProviders.MISTRAL or LLMProviders.PERPLEXITY => GetReasoningEffortState(parameters),
LLMProviders.GOOGLE => MergeReasoningStates(
GetOpenAICompatibleReasoningState(parameters),
GetGoogleReasoningState(parameters)),
LLMProviders.ALIBABA_CLOUD => MergeReasoningStates(
GetOpenAICompatibleReasoningState(parameters),
GetQwenReasoningState(parameters)),
LLMProviders.OPEN_ROUTER or
LLMProviders.X or
LLMProviders.DEEP_SEEK or
LLMProviders.GROQ or
LLMProviders.FIREWORKS or
LLMProviders.HUGGINGFACE or
LLMProviders.HELMHOLTZ or
LLMProviders.GWDG => MergeReasoningStates(
GetOpenAICompatibleReasoningState(parameters),
GetReasoningEffortState(parameters),
GetQwenReasoningState(parameters),
GetGoogleReasoningState(parameters)),
LLMProviders.SELF_HOSTED => provider.Host switch
{
Host.OLLAMA => MergeReasoningStates(
GetOpenAICompatibleReasoningState(parameters),
GetOllamaReasoningState(parameters),
GetQwenReasoningState(parameters)),
Host.LLAMA_CPP => MergeReasoningStates(
GetOpenAICompatibleReasoningState(parameters),
GetLlamaCppReasoningState(parameters),
GetQwenReasoningState(parameters)),
Host.VLLM => MergeReasoningStates(
GetOpenAICompatibleReasoningState(parameters),
GetReasoningEffortState(parameters),
GetVllmReasoningState(parameters),
GetQwenReasoningState(parameters),
GetGoogleReasoningState(parameters)),
_ => MergeReasoningStates(
GetOpenAICompatibleReasoningState(parameters),
GetReasoningEffortState(parameters),
GetQwenReasoningState(parameters),
GetGoogleReasoningState(parameters)),
},
_ => ReasoningConfigurationState.NOT_CONFIGURED,
};
}
/// <summary>
/// Detect OpenAI-compatible reasoning parameters.
/// </summary>
/// <param name="parameters">The parsed additional API parameters.</param>
/// <returns>The detected reasoning configuration state.</returns>
/// <remarks>
/// OpenAI-compatible providers commonly use a nested <c>reasoning</c> object and/or
/// a top-level <c>reasoning_effort</c> parameter.
/// </remarks>
private static ReasoningConfigurationState GetOpenAICompatibleReasoningState(IDictionary<string, object> parameters)
{
var reasoningState = ReasoningConfigurationState.NOT_CONFIGURED;
if (TryGetParameter(parameters, "reasoning", out var reasoning))
{
reasoningState = reasoning switch
{
IDictionary<string, object> reasoningObject when TryGetParameter(reasoningObject, "effort", out var effort) => GetLevelState(effort),
IDictionary<string, object> reasoningObject when TryGetParameter(reasoningObject, "summary", out var summary) => GetLevelState(summary),
IDictionary<string, object> => ReasoningConfigurationState.NOT_CONFIGURED,
_ => GetLevelState(reasoning),
};
}
return MergeReasoningStates(reasoningState, GetReasoningEffortState(parameters));
}
/// <summary>
/// Detect a top-level <c>reasoning_effort</c> parameter.
/// </summary>
/// <param name="parameters">The parsed additional API parameters.</param>
/// <returns>The detected reasoning configuration state.</returns>
private static ReasoningConfigurationState GetReasoningEffortState(IDictionary<string, object> parameters)
{
return TryGetParameter(parameters, "reasoning_effort", out var reasoningEffort)
? GetLevelState(reasoningEffort)
: ReasoningConfigurationState.NOT_CONFIGURED;
}
/// <summary>
/// Detect Anthropic extended-thinking parameters.
/// </summary>
/// <param name="parameters">The parsed additional API parameters.</param>
/// <returns>The detected reasoning configuration state.</returns>
private static ReasoningConfigurationState GetAnthropicReasoningState(IDictionary<string, object> parameters)
{
if (!TryGetParameter(parameters, "thinking", out var thinking))
return ReasoningConfigurationState.NOT_CONFIGURED;
return thinking switch
{
IDictionary<string, object> thinkingObject when TryGetParameter(thinkingObject, "type", out var type) => GetAnthropicThinkingTypeState(type),
_ => GetLevelState(thinking),
};
}
/// <summary>
/// Detect Google Gemini thinking parameters across OpenAI-compatible additional parameters.
/// </summary>
/// <param name="parameters">The parsed additional API parameters.</param>
/// <returns>The detected reasoning configuration state.</returns>
/// <remarks>
/// Google can expose thinking options through <c>thinking_config</c>,
/// <c>generation_config.thinking_config</c>, <c>thinking_level</c>, and summary settings.
/// Summary settings only prove that thinking is enabled when they request summaries;
/// disabling summaries does not necessarily disable reasoning.
/// </remarks>
private static ReasoningConfigurationState GetGoogleReasoningState(IDictionary<string, object> parameters)
{
var states = new List<ReasoningConfigurationState>();
if (TryGetParameter(parameters, "thinking_config", out var thinkingConfig) &&
thinkingConfig is IDictionary<string, object> thinkingConfigObject)
states.Add(GetGoogleThinkingConfigState(thinkingConfigObject));
if (TryGetParameter(parameters, "generation_config", out var generationConfig) &&
generationConfig is IDictionary<string, object> generationConfigObject)
{
if (TryGetParameter(generationConfigObject, "thinking_config", out var nestedThinkingConfig) &&
nestedThinkingConfig is IDictionary<string, object> nestedThinkingConfigObject)
states.Add(GetGoogleThinkingConfigState(nestedThinkingConfigObject));
if (TryGetParameter(generationConfigObject, "thinking_summaries", out var thinkingSummaries))
states.Add(GetThinkingSummariesState(thinkingSummaries));
if (TryGetParameter(generationConfigObject, "thinking_level", out var thinkingLevel))
states.Add(GetLevelState(thinkingLevel));
}
if (TryGetParameter(parameters, "thinking_summaries", out var topLevelThinkingSummaries))
states.Add(GetThinkingSummariesState(topLevelThinkingSummaries));
if (TryGetParameter(parameters, "thinking_level", out var topLevelThinkingLevel))
states.Add(GetLevelState(topLevelThinkingLevel));
return MergeReasoningStates(states);
}
/// <summary>
/// Detect Google Gemini thinking-budget and include-thoughts settings.
/// </summary>
/// <param name="thinkingConfig">The parsed <c>thinking_config</c> object.</param>
/// <returns>The detected reasoning configuration state.</returns>
private static ReasoningConfigurationState GetGoogleThinkingConfigState(IDictionary<string, object> thinkingConfig)
{
var states = new List<ReasoningConfigurationState>();
if (TryGetParameter(thinkingConfig, "thinking_budget", out var thinkingBudget) ||
TryGetParameter(thinkingConfig, "thinkingBudget", out thinkingBudget))
states.Add(GetBudgetState(thinkingBudget));
if (TryGetParameter(thinkingConfig, "include_thoughts", out var includeThoughts) ||
TryGetParameter(thinkingConfig, "includeThoughts", out includeThoughts))
states.Add(GetLevelState(includeThoughts));
return MergeReasoningStates(states);
}
/// <summary>
/// Detect Google Gemini thinking-summary values that imply reasoning is active.
/// </summary>
/// <param name="value">The configured thinking-summary value.</param>
/// <returns>The detected reasoning configuration state.</returns>
/// <remarks>
/// A disabled or missing summary does not prove that thinking is disabled, so only
/// known enabling values are treated as explicit reasoning configuration.
/// </remarks>
private static ReasoningConfigurationState GetThinkingSummariesState(object? value) => value switch
{
string text when text.Equals("auto", StringComparison.OrdinalIgnoreCase) ||
text.Equals("on", StringComparison.OrdinalIgnoreCase) ||
text.Equals("summarized", StringComparison.OrdinalIgnoreCase)
=> ReasoningConfigurationState.EXPLICITLY_ENABLED,
true => ReasoningConfigurationState.EXPLICITLY_ENABLED,
_ => ReasoningConfigurationState.NOT_CONFIGURED,
};
/// <summary>
/// Detect Ollama's <c>think</c> parameter.
/// </summary>
/// <param name="parameters">The parsed additional API parameters.</param>
/// <returns>The detected reasoning configuration state.</returns>
private static ReasoningConfigurationState GetOllamaReasoningState(IDictionary<string, object> parameters)
{
return TryGetParameter(parameters, "think", out var think)
? GetLevelState(think)
: ReasoningConfigurationState.NOT_CONFIGURED;
}
/// <summary>
/// Detect llama.cpp server reasoning parameters.
/// </summary>
/// <param name="parameters">The parsed additional API parameters.</param>
/// <returns>The detected reasoning configuration state.</returns>
/// <remarks>
/// llama.cpp exposes runtime reasoning control through parameters such as
/// <c>reasoning</c>, <c>reasoning_budget</c>, and template-specific kwargs.
/// </remarks>
private static ReasoningConfigurationState GetLlamaCppReasoningState(IDictionary<string, object> parameters)
{
var states = new List<ReasoningConfigurationState>();
if (TryGetParameter(parameters, "reasoning", out var reasoning))
states.Add(GetLlamaCppReasoningModeState(reasoning));
if (TryGetParameter(parameters, "reasoning_budget", out var reasoningBudget))
states.Add(GetBudgetState(reasoningBudget));
if (TryGetParameter(parameters, "chat_template_kwargs", out var chatTemplateKwargs) &&
chatTemplateKwargs is IDictionary<string, object> chatTemplateKwargsObject)
states.Add(GetQwenReasoningState(chatTemplateKwargsObject));
return MergeReasoningStates(states);
}
/// <summary>
/// Detect vLLM reasoning parameters.
/// </summary>
/// <param name="parameters">The parsed additional API parameters.</param>
/// <returns>The detected reasoning configuration state.</returns>
/// <remarks>
/// vLLM supports both top-level reasoning fields and chat-template kwargs, depending
/// on model family and reasoning parser configuration.
/// </remarks>
private static ReasoningConfigurationState GetVllmReasoningState(IDictionary<string, object> parameters)
{
var states = new List<ReasoningConfigurationState>();
if (TryGetParameter(parameters, "thinking_token_budget", out var thinkingTokenBudget))
states.Add(GetBudgetState(thinkingTokenBudget));
if (TryGetParameter(parameters, "chat_template_kwargs", out var chatTemplateKwargs) &&
chatTemplateKwargs is IDictionary<string, object> chatTemplateKwargsObject)
{
states.Add(GetQwenReasoningState(chatTemplateKwargsObject));
if (TryGetParameter(chatTemplateKwargsObject, "thinking", out var thinking))
states.Add(GetLevelState(thinking));
}
return MergeReasoningStates(states);
}
/// <summary>
/// Detect Qwen-style <c>enable_thinking</c> parameters.
/// </summary>
/// <param name="parameters">The parsed additional API parameters.</param>
/// <returns>The detected reasoning configuration state.</returns>
/// <remarks>
/// Some OpenAI-compatible servers accept <c>enable_thinking</c> either at the
/// top level or under <c>chat_template_kwargs</c>.
/// </remarks>
private static ReasoningConfigurationState GetQwenReasoningState(IDictionary<string, object> parameters)
{
var states = new List<ReasoningConfigurationState>();
if (TryGetParameter(parameters, "enable_thinking", out var enableThinking))
states.Add(GetLevelState(enableThinking));
if (TryGetParameter(parameters, "chat_template_kwargs", out var chatTemplateKwargs) &&
chatTemplateKwargs is IDictionary<string, object> chatTemplateKwargsObject &&
TryGetParameter(chatTemplateKwargsObject, "enable_thinking", out var nestedEnableThinking))
states.Add(GetLevelState(nestedEnableThinking));
return MergeReasoningStates(states);
}
/// <summary>
/// Interpret Anthropic's <c>thinking.type</c> value.
/// </summary>
/// <param name="value">The configured Anthropic thinking type.</param>
/// <returns>The detected reasoning configuration state.</returns>
private static ReasoningConfigurationState GetAnthropicThinkingTypeState(object? value) => value switch
{
string text when text.Equals("enabled", StringComparison.OrdinalIgnoreCase) ||
text.Equals("adaptive", StringComparison.OrdinalIgnoreCase)
=> ReasoningConfigurationState.EXPLICITLY_ENABLED,
string text when IsDisabledText(text) => ReasoningConfigurationState.EXPLICITLY_DISABLED,
_ => GetLevelState(value),
};
/// <summary>
/// Interpret llama.cpp's <c>reasoning</c> mode value.
/// </summary>
/// <param name="value">The configured llama.cpp reasoning mode.</param>
/// <returns>The detected reasoning configuration state.</returns>
/// <remarks>
/// <c>auto</c> means the server decides from the model/template, so it is treated as
/// not configured by the user rather than as explicitly enabled.
/// </remarks>
private static ReasoningConfigurationState GetLlamaCppReasoningModeState(object? value) => value switch
{
string text when text.Equals("on", StringComparison.OrdinalIgnoreCase) => ReasoningConfigurationState.EXPLICITLY_ENABLED,
string text when text.Equals("off", StringComparison.OrdinalIgnoreCase) => ReasoningConfigurationState.EXPLICITLY_DISABLED,
string text when text.Equals("auto", StringComparison.OrdinalIgnoreCase) => ReasoningConfigurationState.NOT_CONFIGURED,
_ => GetLevelState(value),
};
/// <summary>
/// Interpret token-budget style values used by several providers.
/// </summary>
/// <param name="value">The configured budget value.</param>
/// <returns>The detected reasoning configuration state.</returns>
/// <remarks>
/// A zero budget disables reasoning; non-zero values, including unrestricted negative
/// budgets, indicate that reasoning is available for the request.
/// </remarks>
private static ReasoningConfigurationState GetBudgetState(object? value) => value switch
{
int i => i is 0 ? ReasoningConfigurationState.EXPLICITLY_DISABLED : ReasoningConfigurationState.EXPLICITLY_ENABLED,
long l => l is 0 ? ReasoningConfigurationState.EXPLICITLY_DISABLED : ReasoningConfigurationState.EXPLICITLY_ENABLED,
double d => Math.Abs(d) < double.Epsilon ? ReasoningConfigurationState.EXPLICITLY_DISABLED : ReasoningConfigurationState.EXPLICITLY_ENABLED,
decimal m => m is 0 ? ReasoningConfigurationState.EXPLICITLY_DISABLED : ReasoningConfigurationState.EXPLICITLY_ENABLED,
_ => GetLevelState(value),
};
/// <summary>
/// Interpret common boolean, numeric, and level-style reasoning values.
/// </summary>
/// <param name="value">The raw parsed parameter value.</param>
/// <returns>The detected reasoning configuration state.</returns>
private static ReasoningConfigurationState GetLevelState(object? value) => value switch
{
bool booleanValue => booleanValue ? ReasoningConfigurationState.EXPLICITLY_ENABLED : ReasoningConfigurationState.EXPLICITLY_DISABLED,
int i => i is 0 ? ReasoningConfigurationState.EXPLICITLY_DISABLED : ReasoningConfigurationState.EXPLICITLY_ENABLED,
long l => l is 0 ? ReasoningConfigurationState.EXPLICITLY_DISABLED : ReasoningConfigurationState.EXPLICITLY_ENABLED,
double d => Math.Abs(d) < double.Epsilon ? ReasoningConfigurationState.EXPLICITLY_DISABLED : ReasoningConfigurationState.EXPLICITLY_ENABLED,
decimal m => m is 0 ? ReasoningConfigurationState.EXPLICITLY_DISABLED : ReasoningConfigurationState.EXPLICITLY_ENABLED,
string text when IsDisabledText(text) => ReasoningConfigurationState.EXPLICITLY_DISABLED,
string text when IsEnabledText(text) => ReasoningConfigurationState.EXPLICITLY_ENABLED,
_ => ReasoningConfigurationState.NOT_CONFIGURED,
};
/// <summary>
/// Determine whether a string value is a known reasoning-enabling value.
/// </summary>
/// <param name="text">The string value to inspect.</param>
/// <returns><see langword="true"/> if the value should be treated as enabling reasoning.</returns>
private static bool IsEnabledText(string text)
{
return text.Equals("true", StringComparison.OrdinalIgnoreCase) ||
text.Equals("yes", StringComparison.OrdinalIgnoreCase) ||
text.Equals("on", StringComparison.OrdinalIgnoreCase) ||
text.Equals("enabled", StringComparison.OrdinalIgnoreCase) ||
text.Equals("low", StringComparison.OrdinalIgnoreCase) ||
text.Equals("minimal", StringComparison.OrdinalIgnoreCase) ||
text.Equals("medium", StringComparison.OrdinalIgnoreCase) ||
text.Equals("high", StringComparison.OrdinalIgnoreCase) ||
text.Equals("max", StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Determine whether a string value is a known reasoning-disabling value.
/// </summary>
/// <param name="text">The string value to inspect.</param>
/// <returns><see langword="true"/> if the value should be treated as disabling reasoning.</returns>
private static bool IsDisabledText(string text)
{
return string.IsNullOrWhiteSpace(text) ||
text.Equals("false", StringComparison.OrdinalIgnoreCase) ||
text.Equals("no", StringComparison.OrdinalIgnoreCase) ||
text.Equals("off", StringComparison.OrdinalIgnoreCase) ||
text.Equals("none", StringComparison.OrdinalIgnoreCase) ||
text.Equals("disabled", StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Merge multiple detected reasoning states into a single state.
/// </summary>
/// <param name="states">The detected states from provider-specific parameter checks.</param>
/// <returns>The merged state.</returns>
/// <remarks>
/// Explicit disabling wins over enabling because user-provided off switches should
/// suppress default-on reasoning indicators.
/// </remarks>
private static ReasoningConfigurationState MergeReasoningStates(IEnumerable<ReasoningConfigurationState> states)
{
var result = ReasoningConfigurationState.NOT_CONFIGURED;
foreach (var state in states)
{
if (state is ReasoningConfigurationState.EXPLICITLY_DISABLED)
return ReasoningConfigurationState.EXPLICITLY_DISABLED;
if (state is ReasoningConfigurationState.EXPLICITLY_ENABLED)
result = ReasoningConfigurationState.EXPLICITLY_ENABLED;
}
return result;
}
/// <summary>
/// Merge multiple detected reasoning states into a single state.
/// </summary>
/// <param name="states">The detected states from provider-specific parameter checks.</param>
/// <returns>The merged state.</returns>
private static ReasoningConfigurationState MergeReasoningStates(params ReasoningConfigurationState[] states)
{
return MergeReasoningStates(states.AsEnumerable());
}
/// <summary>
/// Try to read a parameter from a dictionary using case-insensitive key matching.
/// </summary>
/// <param name="parameters">The parsed parameter dictionary.</param>
/// <param name="key">The parameter name to find.</param>
/// <param name="value">The matched parameter value, if found.</param>
/// <returns><see langword="true"/> if a matching key was found; otherwise <see langword="false"/>.</returns>
private static bool TryGetParameter(IDictionary<string, object> parameters, string key, out object? value)
{
value = null;
if (parameters.Count is 0)
return false;
var foundKey = parameters.Keys.FirstOrDefault(k => string.Equals(k, key, StringComparison.OrdinalIgnoreCase));
if (foundKey is null)
return false;
value = parameters[foundKey];
return true;
}
}
@@ -9,35 +9,62 @@ public static partial class ProviderExtensions
/// </summary>
/// <param name="provider">The configured provider.</param>
/// <returns>The capabilities of the configured model.</returns>
public static List<Capability> GetModelCapabilities(this Provider provider) => provider.UsedLLMProvider.GetModelCapabilities(provider.Model);
public static List<Capability> GetModelCapabilities(this Provider provider)
{
var automaticCapabilities = provider.UsedLLMProvider.GetModelCapabilities(provider.Model);
return provider.CapabilityOverrides?.ApplyTo(automaticCapabilities) ?? automaticCapabilities;
}
/// <summary>
/// Get whether the model used by the configured provider accepts images as input.
/// </summary>
/// <remarks>
/// Two capabilities express image input, one for a single image and one for several. Anything that
/// wants to know whether an image may be sent has to accept both, which is why the question is asked
/// here instead of at each call site: attaching a file and validating an already attached file must
/// never disagree about it.
/// </remarks>
/// <param name="provider">The configured provider.</param>
/// <returns><c>true</c> when the model accepts image input.</returns>
public static bool SupportsImageInput(this Provider provider)
{
var capabilities = provider.GetModelCapabilities();
return capabilities.Contains(Capability.SINGLE_IMAGE_INPUT) || capabilities.Contains(Capability.MULTIPLE_IMAGE_INPUT);
}
/// <summary>
/// Get the capabilities of a model for a specific provider.
/// </summary>
/// <param name="provider">The LLM provider.</param>
/// <param name="model">The model to get the capabilities for.</param>
/// <returns>>The capabilities of the model.</returns>
public static List<Capability> GetModelCapabilities(this LLMProviders provider, Model model) => provider switch
public static List<Capability> GetModelCapabilities(this LLMProviders provider, Model model)
{
LLMProviders.OPEN_AI => GetModelCapabilitiesOpenAI(model),
LLMProviders.MISTRAL => GetModelCapabilitiesMistral(model),
LLMProviders.ANTHROPIC => GetModelCapabilitiesAnthropic(model),
LLMProviders.GOOGLE => GetModelCapabilitiesGoogle(model),
LLMProviders.X => GetModelCapabilitiesOpenSource(model),
LLMProviders.DEEP_SEEK => GetModelCapabilitiesDeepSeek(model),
LLMProviders.ALIBABA_CLOUD => GetModelCapabilitiesAlibaba(model),
LLMProviders.PERPLEXITY => GetModelCapabilitiesPerplexity(model),
LLMProviders.OPEN_ROUTER => GetModelCapabilitiesOpenRouter(model),
if (string.IsNullOrWhiteSpace(model.Id))
return [];
LLMProviders.GROQ => GetModelCapabilitiesOpenSource(model),
LLMProviders.FIREWORKS => GetModelCapabilitiesOpenSource(model),
LLMProviders.HUGGINGFACE => GetModelCapabilitiesOpenSource(model),
return provider switch
{
LLMProviders.OPEN_AI => GetModelCapabilitiesOpenAI(model),
LLMProviders.MISTRAL => GetModelCapabilitiesMistral(model),
LLMProviders.ANTHROPIC => GetModelCapabilitiesAnthropic(model),
LLMProviders.GOOGLE => GetModelCapabilitiesGoogle(model),
LLMProviders.X => GetModelCapabilitiesOpenSource(model),
LLMProviders.DEEP_SEEK => GetModelCapabilitiesDeepSeek(model),
LLMProviders.ALIBABA_CLOUD => GetModelCapabilitiesAlibaba(model),
LLMProviders.PERPLEXITY => GetModelCapabilitiesPerplexity(model),
LLMProviders.OPEN_ROUTER => GetModelCapabilitiesOpenRouter(model),
LLMProviders.GROQ => GetModelCapabilitiesOpenSource(model),
LLMProviders.FIREWORKS => GetModelCapabilitiesOpenSource(model),
LLMProviders.HUGGINGFACE => GetModelCapabilitiesOpenSource(model),
LLMProviders.HELMHOLTZ => GetModelCapabilitiesOpenSource(model),
LLMProviders.GWDG => GetModelCapabilitiesOpenSource(model),
LLMProviders.HELMHOLTZ => GetModelCapabilitiesOpenSource(model),
LLMProviders.GWDG => GetModelCapabilitiesOpenSource(model),
LLMProviders.SELF_HOSTED => GetModelCapabilitiesOpenSource(model),
LLMProviders.SELF_HOSTED => GetModelCapabilitiesOpenSource(model),
_ => []
};
_ => []
};
}
}