Removed provider access rule suppressions outside the settings manager (#915)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions

This commit is contained in:
Thorsten Sommer 2026-08-12 20:44:48 +02:00 committed by GitHub
parent 688fea73cb
commit ede45103b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 254 additions and 122 deletions

View File

@ -1,5 +1,4 @@
using System.Text; using System.Text;
using System.Diagnostics.CodeAnalysis;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Dialogs; using AIStudio.Dialogs;
@ -371,11 +370,10 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
await this.SettingsManager.StoreSettings(); await this.SettingsManager.StoreSettings();
} }
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
private void UpdateProviders() private void UpdateProviders()
{ {
this.availableLLMProviders.Clear(); this.availableLLMProviders.Clear();
foreach (var provider in this.SettingsManager.ConfigurationData.Providers) foreach (var provider in this.SettingsManager.GetAllProviders())
this.availableLLMProviders.Add(new ConfigurationSelectData<string>(provider.InstanceName, provider.Id)); this.availableLLMProviders.Add(new ConfigurationSelectData<string>(provider.InstanceName, provider.Id));
} }
@ -459,7 +457,6 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
await this.AutoSave(true); await this.AutoSave(true);
} }
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed", Justification = "Policy-specific preselection needs to probe providers by id before falling back to SettingsManager APIs.")]
private void ApplyPolicyPreselection(bool preferPolicyPreselection = false) private void ApplyPolicyPreselection(bool preferPolicyPreselection = false)
{ {
if (this.selectedPolicy is null) if (this.selectedPolicy is null)
@ -480,8 +477,8 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
} }
// Try to apply the policy preselection: // Try to apply the policy preselection:
var policyProvider = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.selectedPolicy.PreselectedProvider); var policyProvider = this.SettingsManager.GetProviderById(this.selectedPolicy.PreselectedProvider);
if (policyProvider is not null && policyProvider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level >= minimumLevel) if (policyProvider != Settings.Provider.NONE && policyProvider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level >= minimumLevel)
{ {
this.ProviderSettings = policyProvider; this.ProviderSettings = policyProvider;
this.CurrentProfile = this.ResolveProfileSelection(); this.CurrentProfile = this.ResolveProfileSelection();

View File

@ -1,9 +1,8 @@
using System.Diagnostics.CodeAnalysis;
using AIStudio.Assistants.SlideBuilder; using AIStudio.Assistants.SlideBuilder;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Settings; using AIStudio.Settings;
using ComponentKind = AIStudio.Tools.Components;
using ProviderSettings = AIStudio.Settings.Provider; using ProviderSettings = AIStudio.Settings.Provider;
namespace AIStudio.Assistants.VisualBriefing; namespace AIStudio.Assistants.VisualBriefing;
@ -77,7 +76,6 @@ public sealed class VisualBriefingEditorState
/// <param name="briefing">The manifest to read.</param> /// <param name="briefing">The manifest to read.</param>
/// <param name="settingsManager">The settings used to resolve the stored provider and profile.</param> /// <param name="settingsManager">The settings used to resolve the stored provider and profile.</param>
/// <returns>The editor state for the briefing.</returns> /// <returns>The editor state for the briefing.</returns>
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed", Justification = "A stored briefing references one specific provider and model by id, so it must be looked up directly instead of using the preselection APIs.")]
public static VisualBriefingEditorState FromManifest(VisualBriefingManifest briefing, SettingsManager settingsManager) => new() public static VisualBriefingEditorState FromManifest(VisualBriefingManifest briefing, SettingsManager settingsManager) => new()
{ {
Name = briefing.Name, Name = briefing.Name,
@ -94,8 +92,8 @@ public sealed class VisualBriefingEditorState
ProtectionLevel = briefing.Settings.ProtectionLevel, ProtectionLevel = briefing.Settings.ProtectionLevel,
CustomProtectionLevel = briefing.Settings.CustomProtectionLevel, CustomProtectionLevel = briefing.Settings.CustomProtectionLevel,
Provider = settingsManager.ConfigurationData.Providers.FirstOrDefault(candidate => candidate.Id == briefing.Settings.ProviderId && candidate.Model.Id == briefing.Settings.ModelId) ?? ProviderSettings.NONE, Provider = ResolveProvider(briefing, settingsManager),
Profile = settingsManager.ConfigurationData.Profiles.FirstOrDefault(candidate => candidate.Id == briefing.Settings.ProfileId) ?? Profile.NO_PROFILE, Profile = settingsManager.GetProfileById(briefing.Settings.ProfileId),
SourceMaterial = SourceMaterial =
[ [
@ -112,6 +110,43 @@ public sealed class VisualBriefingEditorState
], ],
}; };
/// <summary>
/// Resolves the provider a stored briefing refers to.
/// </summary>
/// <remarks>
/// <para>
/// A briefing stores its provider and model as two separate ids, and both must still match: when
/// the user changed the model of that provider, the stored combination no longer exists and the
/// editor starts without a provider.
/// </para>
/// <para>
/// The resolved provider is additionally checked against the minimum confidence level of the
/// visual briefing assistant. This matters because the confidence settings may have become
/// stricter since the briefing was stored: the user may have lowered the confidence of that
/// provider, or may now enforce a global minimum. Without this check, opening an old briefing
/// would silently restore a provider the user no longer trusts, bypassing the filtering that
/// the provider dropdown applies. Note that the component minimum already covers the enforced
/// global minimum as well.
/// </para>
/// </remarks>
/// <param name="briefing">The manifest to read.</param>
/// <param name="settingsManager">The settings used to resolve the provider.</param>
/// <returns>The stored provider, or <see cref="ProviderSettings.NONE"/> when it is unavailable or no longer trusted.</returns>
private static ProviderSettings ResolveProvider(VisualBriefingManifest briefing, SettingsManager settingsManager)
{
var storedProvider = settingsManager.GetProviderById(briefing.Settings.ProviderId);
if (storedProvider == ProviderSettings.NONE)
return ProviderSettings.NONE;
if (storedProvider.Model.Id != briefing.Settings.ModelId)
return ProviderSettings.NONE;
if (!settingsManager.IsProviderConfident(storedProvider, ComponentKind.VISUAL_BRIEFING_ASSISTANT))
return ProviderSettings.NONE;
return storedProvider;
}
/// <summary> /// <summary>
/// Creates the persisted settings for this editor state. /// Creates the persisted settings for this editor state.
/// </summary> /// </summary>

View File

@ -1,5 +1,3 @@
using System.Diagnostics.CodeAnalysis;
using AIStudio.Provider; using AIStudio.Provider;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Tools.PluginSystem; using AIStudio.Tools.PluginSystem;
@ -35,27 +33,20 @@ public partial class ConfigurationProviderSelection : MSGComponentBase
[Parameter] [Parameter]
public Func<bool> IsLocked { get; set; } = () => false; public Func<bool> IsLocked { get; set; } = () => false;
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
private IEnumerable<ConfigurationSelectData<string>> FilteredData() private IEnumerable<ConfigurationSelectData<string>> FilteredData()
{ {
if(this.Component is not Tools.Components.NONE and not Tools.Components.APP_SETTINGS) if(this.Component is not Tools.Components.NONE and not Tools.Components.APP_SETTINGS)
yield return new(T("Use app default"), string.Empty); yield return new(T("Use app default"), string.Empty);
// Get the minimum confidence level for this component, and/or the enforced global minimum confidence level: //
var minimumLevel = this.SettingsManager.GetMinimumConfidenceLevel(this.Component); // Filter the providers based on the minimum confidence level of this component, the enforced
// global minimum, and the explicit minimum level when it is higher. Providers which no longer
// Apply the explicit minimum confidence level if set and higher than the current minimum level: // exist resolve to `Provider.NONE` and are dropped by the confidence check as well:
if (this.ExplicitMinimumConfidence is not ConfidenceLevel.UNKNOWN && this.ExplicitMinimumConfidence > minimumLevel) //
minimumLevel = this.ExplicitMinimumConfidence;
// Filter the providers based on the minimum confidence level:
foreach (var providerId in this.Data) foreach (var providerId in this.Data)
{ {
var provider = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == providerId.Value); var provider = this.SettingsManager.GetProviderById(providerId.Value);
if (provider is null) if (this.SettingsManager.IsProviderConfident(provider, this.Component, this.ExplicitMinimumConfidence))
continue;
if (provider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level >= minimumLevel)
yield return providerId; yield return providerId;
} }
} }

View File

@ -1,5 +1,3 @@
using System.Diagnostics.CodeAnalysis;
using AIStudio.Provider; using AIStudio.Provider;
using AIStudio.Settings; using AIStudio.Settings;
@ -83,7 +81,6 @@ public partial class ProviderSelection : MSGComponentBase
_ => this.T("Uses reasoning (thinking)"), _ => this.T("Uses reasoning (thinking)"),
}; };
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
private IEnumerable<AIStudio.Settings.Provider> GetAvailableProviders() private IEnumerable<AIStudio.Settings.Provider> GetAvailableProviders()
{ {
switch (this.Component) switch (this.Component)
@ -98,18 +95,10 @@ public partial class ProviderSelection : MSGComponentBase
case { } component: case { } component:
// Get the minimum confidence level for this component, and/or the global minimum if enforced: // Filter providers based on the minimum confidence level of this component, the
var minimumLevel = this.SettingsManager.GetMinimumConfidenceLevel(component); // enforced global minimum, and the explicit minimum level when it is higher:
foreach (var provider in this.SettingsManager.GetConfidentProviders(component, this.ExplicitMinimumConfidence))
// Override with the explicit minimum level if set and higher: yield return provider;
if (this.ExplicitMinimumConfidence is not ConfidenceLevel.UNKNOWN && this.ExplicitMinimumConfidence > minimumLevel)
minimumLevel = this.ExplicitMinimumConfidence;
// Filter providers based on the minimum confidence level:
foreach (var provider in this.SettingsManager.ConfigurationData.Providers)
if (provider.UsedLLMProvider != LLMProviders.NONE)
if (provider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level >= minimumLevel)
yield return provider;
break; break;
} }
} }

View File

@ -9,7 +9,7 @@
<MudJustifiedText Typo="Typo.body1" Class="mb-3"> <MudJustifiedText Typo="Typo.body1" Class="mb-3">
@T("What we call a provider is the combination of an LLM provider such as OpenAI and a model like GPT-4o. You can configure as many providers as you want. This way, you can use the appropriate model for each task. As an LLM provider, you can also choose local providers. However, to use this app, you must configure at least one provider.") @T("What we call a provider is the combination of an LLM provider such as OpenAI and a model like GPT-4o. You can configure as many providers as you want. This way, you can use the appropriate model for each task. As an LLM provider, you can also choose local providers. However, to use this app, you must configure at least one provider.")
</MudJustifiedText> </MudJustifiedText>
<MudTable Items="@this.SettingsManager.ConfigurationData.Providers" Hover="@true" Class="border-dashed border rounded-lg"> <MudTable Items="@this.SettingsManager.GetAllProviders()" Hover="@true" Class="border-dashed border rounded-lg">
<ColGroup> <ColGroup>
<col style="width: 3em;"/> <col style="width: 3em;"/>
<col style="width: 12em;"/> <col style="width: 12em;"/>
@ -66,7 +66,7 @@
</RowTemplate> </RowTemplate>
</MudTable> </MudTable>
@if(this.SettingsManager.ConfigurationData.Providers.Count == 0) @if(this.SettingsManager.GetAllProviders().Count == 0)
{ {
<MudText Typo="Typo.h6" Class="mt-3"> <MudText Typo="Typo.h6" Class="mt-3">
@T("No providers configured yet.") @T("No providers configured yet.")

View File

@ -27,7 +27,7 @@ public partial class SettingsPanelProviders : SettingsPanelProviderBase
#endregion #endregion
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")] [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed", Justification = "Managing the provider list is the purpose of this settings panel. Reading providers goes through the settings manager, but adding, editing, and removing them stays here on purpose.")]
private async Task AddLLMProvider() private async Task AddLLMProvider()
{ {
var dialogParameters = new DialogParameters<ProviderDialog> var dialogParameters = new DialogParameters<ProviderDialog>
@ -50,7 +50,7 @@ public partial class SettingsPanelProviders : SettingsPanelProviderBase
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED); await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
} }
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")] [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed", Justification = "Managing the provider list is the purpose of this settings panel. Reading providers goes through the settings manager, but adding, editing, and removing them stays here on purpose.")]
private async Task EditLLMProvider(AIStudio.Settings.Provider provider) private async Task EditLLMProvider(AIStudio.Settings.Provider provider)
{ {
if(provider == AIStudio.Settings.Provider.NONE) if(provider == AIStudio.Settings.Provider.NONE)
@ -94,7 +94,7 @@ public partial class SettingsPanelProviders : SettingsPanelProviderBase
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED); await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
} }
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")] [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed", Justification = "Managing the provider list is the purpose of this settings panel. Reading providers goes through the settings manager, but adding, editing, and removing them stays here on purpose.")]
private async Task DeleteLLMProvider(AIStudio.Settings.Provider provider) private async Task DeleteLLMProvider(AIStudio.Settings.Provider provider)
{ {
var dialogParameters = new DialogParameters<ConfirmDialog> var dialogParameters = new DialogParameters<ConfirmDialog>
@ -156,11 +156,10 @@ public partial class SettingsPanelProviders : SettingsPanelProviderBase
return modelName.Length > MAX_LENGTH ? "[...] " + modelName[^Math.Min(MAX_LENGTH, modelName.Length)..] : modelName; return modelName.Length > MAX_LENGTH ? "[...] " + modelName[^Math.Min(MAX_LENGTH, modelName.Length)..] : modelName;
} }
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
private async Task UpdateProviders() private async Task UpdateProviders()
{ {
this.AvailableLLMProviders.Clear(); this.AvailableLLMProviders.Clear();
foreach (var provider in this.SettingsManager.ConfigurationData.Providers) foreach (var provider in this.SettingsManager.GetAllProviders())
this.AvailableLLMProviders.Add(new (provider.InstanceName, provider.Id)); this.AvailableLLMProviders.Add(new (provider.InstanceName, provider.Id));
await this.AvailableLLMProvidersChanged.InvokeAsync(this.AvailableLLMProviders); await this.AvailableLLMProvidersChanged.InvokeAsync(this.AvailableLLMProviders);

View File

@ -201,9 +201,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES); this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES);
// Load the used instance names: // Load the used instance names:
#pragma warning disable MWAIS0001 this.UsedInstanceNames = this.SettingsManager.GetAllProviders().Select(x => x.InstanceName.ToLowerInvariant()).ToList();
this.UsedInstanceNames = this.SettingsManager.ConfigurationData.Providers.Select(x => x.InstanceName.ToLowerInvariant()).ToList();
#pragma warning restore MWAIS0001
this.capabilityOverrides = this.DataCapabilityOverrides ?? new(); this.capabilityOverrides = this.DataCapabilityOverrides ?? new();
this.showExpertSettings = !string.IsNullOrWhiteSpace(this.AdditionalJsonApiParameters) || this.capabilityOverrides.HasOverrides; this.showExpertSettings = !string.IsNullOrWhiteSpace(this.AdditionalJsonApiParameters) || this.capabilityOverrides.HasOverrides;

View File

@ -1,5 +1,3 @@
using System.Diagnostics.CodeAnalysis;
using AIStudio.Components; using AIStudio.Components;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Tools.Services; using AIStudio.Tools.Services;
@ -40,11 +38,10 @@ public abstract class SettingsDialogBase : MSGComponentBase
protected void Close() => this.MudDialog.Cancel(); protected void Close() => this.MudDialog.Cancel();
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
private void UpdateProviders() private void UpdateProviders()
{ {
this.AvailableLLMProviders.Clear(); this.AvailableLLMProviders.Clear();
foreach (var provider in this.SettingsManager.ConfigurationData.Providers) foreach (var provider in this.SettingsManager.GetAllProviders())
this.AvailableLLMProviders.Add(new (provider.InstanceName, provider.Id)); this.AvailableLLMProviders.Add(new (provider.InstanceName, provider.Id));
} }

View File

@ -1,4 +1,3 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Text.Json; using System.Text.Json;
@ -434,7 +433,6 @@ public sealed class SettingsManager
return localeTag[..separatorIndex]; return localeTag[..separatorIndex];
} }
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
public Provider GetPreselectedProvider(Tools.Components component, string? currentProviderId = null, bool usePreselectionBeforeCurrentProvider = false) public Provider GetPreselectedProvider(Tools.Components component, string? currentProviderId = null, bool usePreselectionBeforeCurrentProvider = false)
{ {
var minimumLevel = this.GetMinimumConfidenceLevel(component); var minimumLevel = this.GetMinimumConfidenceLevel(component);
@ -486,15 +484,27 @@ public sealed class SettingsManager
return this.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.ConfigurationData.App.PreselectedProvider && x.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel) ?? Provider.NONE; return this.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.ConfigurationData.App.PreselectedProvider && x.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel) ?? Provider.NONE;
} }
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
public Provider GetChatProviderForLoadedChat(string? chatProviderId = null) public Provider GetChatProviderForLoadedChat(string? chatProviderId = null)
{ {
var minimumLevel = this.GetMinimumConfidenceLevel(Tools.Components.CHAT); var minimumLevel = this.GetMinimumConfidenceLevel(Tools.Components.CHAT);
bool IsSelectableProvider(Provider provider) => var chatProvider = FindProviderById(chatProviderId);
provider != Provider.NONE if (chatProvider is not null)
&& provider.UsedLLMProvider != LLMProviders.NONE return chatProvider;
&& provider.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel;
var defaultChatProvider = this.ConfigurationData.Chat.PreselectOptions
? FindProviderById(this.ConfigurationData.Chat.PreselectedProvider)
: null;
if (defaultChatProvider is not null)
return defaultChatProvider;
var defaultAppProvider = FindProviderById(this.ConfigurationData.App.PreselectedProvider);
if (defaultAppProvider is not null)
return defaultAppProvider;
var selectableProviders = this.ConfigurationData.Providers.Where(IsSelectableProvider).ToList();
return selectableProviders.Count == 1 ? selectableProviders[0] : Provider.NONE;
Provider? FindProviderById(string? providerId) Provider? FindProviderById(string? providerId)
{ {
@ -505,22 +515,103 @@ public sealed class SettingsManager
return provider is not null && IsSelectableProvider(provider) ? provider : null; return provider is not null && IsSelectableProvider(provider) ? provider : null;
} }
var chatProvider = FindProviderById(chatProviderId); bool IsSelectableProvider(Provider provider) =>
if (chatProvider is not null) provider != Provider.NONE
return chatProvider; && provider.UsedLLMProvider != LLMProviders.NONE
&& provider.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel;
}
var defaultChatProvider = this.ConfigurationData.Chat.PreselectOptions /// <summary>
? FindProviderById(this.ConfigurationData.Chat.PreselectedProvider) /// Returns all configured providers without applying any confidence filtering.
: null; /// </summary>
if (defaultChatProvider is not null) /// <remarks>
return defaultChatProvider; /// <para>
/// This method applies neither the global minimum confidence level (see
/// <see cref="Data.Confidence"/> with <c>EnforceGlobalMinimumConfidence</c>) nor any
/// component-specific minimum. Even when the user enforces a global minimum of, say,
/// <see cref="ConfidenceLevel.HIGH"/>, this method still returns every configured provider.
/// That is intentional: this method serves the provider management UI, duplicate-name checks,
/// and the raw select data of provider dropdowns. The dropdowns are filtered afterward by
/// ConfigurationProviderSelection, which calls IsProviderConfident.
/// </para>
/// <para>
/// Whenever a provider is about to be used for an LLM request, do not use this method. Use
/// GetConfidentProviders, GetPreselectedProvider, or GetChatProviderForLoadedChat instead,
/// since they honor the confidence levels.
/// </para>
/// <para>
/// The returned list is the live provider list. It is read-only for callers: adding, editing,
/// or removing providers stays inside the settings UI.
/// </para>
/// </remarks>
/// <returns>All configured providers, unfiltered.</returns>
public IReadOnlyList<Provider> GetAllProviders() => this.ConfigurationData.Providers;
var defaultAppProvider = FindProviderById(this.ConfigurationData.App.PreselectedProvider); /// <summary>
if (defaultAppProvider is not null) /// Returns the provider with the given id, without applying any confidence filtering.
return defaultAppProvider; /// </summary>
/// <remarks>
/// This method resolves a stored provider reference by its id. It applies neither the global
/// minimum confidence level nor any component-specific minimum, so it returns the requested
/// provider even when the user enforces a higher global minimum. Callers that intend to use the
/// returned provider for an LLM request must check it themselves through
/// IsProviderConfident or fall back to GetPreselectedProvider.
/// </remarks>
/// <param name="providerId">The id of the provider to look up.</param>
/// <returns>The provider, or <see cref="Provider.NONE"/> when no provider with that id exists.</returns>
public Provider GetProviderById(string? providerId)
{
if (string.IsNullOrWhiteSpace(providerId))
return Provider.NONE;
var selectableProviders = this.ConfigurationData.Providers.Where(IsSelectableProvider).ToList(); if (string.Equals(providerId, Provider.NONE.Id, StringComparison.OrdinalIgnoreCase))
return selectableProviders.Count == 1 ? selectableProviders[0] : Provider.NONE; return Provider.NONE;
return this.ConfigurationData.Providers.FirstOrDefault(x => x.Id.Equals(providerId, StringComparison.OrdinalIgnoreCase)) ?? Provider.NONE;
}
/// <summary>
/// Determines the minimum confidence level a provider must have for the given component.
/// </summary>
/// <param name="component">The component for which the providers get filtered.</param>
/// <param name="explicitMinimum">An explicit minimum level, which is applied when it is higher than the component's minimum.</param>
/// <returns>The effective minimum confidence level.</returns>
public ConfidenceLevel GetEffectiveMinimumConfidenceLevel(Tools.Components component, ConfidenceLevel explicitMinimum = ConfidenceLevel.UNKNOWN)
{
var minimumLevel = this.GetMinimumConfidenceLevel(component);
if (explicitMinimum is not ConfidenceLevel.UNKNOWN && explicitMinimum > minimumLevel)
return explicitMinimum;
return minimumLevel;
}
/// <summary>
/// Checks whether the given provider satisfies the minimum confidence level of the given component.
/// </summary>
/// <param name="provider">The provider to check.</param>
/// <param name="component">The component for which the provider gets checked.</param>
/// <param name="explicitMinimum">An explicit minimum level, which is applied when it is higher than the component's minimum.</param>
/// <returns>True, when the provider may be used by the component, false otherwise.</returns>
public bool IsProviderConfident(Provider provider, Tools.Components component, ConfidenceLevel explicitMinimum = ConfidenceLevel.UNKNOWN)
{
if (provider.UsedLLMProvider is LLMProviders.NONE)
return false;
return provider.UsedLLMProvider.GetConfidence(this).Level >= this.GetEffectiveMinimumConfidenceLevel(component, explicitMinimum);
}
/// <summary>
/// Returns all providers that satisfy the minimum confidence level of the given component.
/// </summary>
/// <param name="component">The component for which the providers get filtered.</param>
/// <param name="explicitMinimum">An explicit minimum level, which is applied when it is higher than the component's minimum.</param>
/// <returns>All providers the component may use.</returns>
public IEnumerable<Provider> GetConfidentProviders(Tools.Components component, ConfidenceLevel explicitMinimum = ConfidenceLevel.UNKNOWN)
{
var minimumLevel = this.GetEffectiveMinimumConfidenceLevel(component, explicitMinimum);
foreach (var provider in this.ConfigurationData.Providers)
if (provider.UsedLLMProvider is not LLMProviders.NONE && provider.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel)
yield return provider;
} }
public Profile GetPreselectedProfile(Tools.Components component) public Profile GetPreselectedProfile(Tools.Components component)

View File

@ -1,4 +1,3 @@
using System.Diagnostics.CodeAnalysis;
using AIStudio.Provider; using AIStudio.Provider;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Settings.DataModel; using AIStudio.Settings.DataModel;
@ -164,48 +163,42 @@ public static class ComponentsExtensions
_ => default, _ => default,
}; };
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")] public static AIStudio.Settings.Provider PreselectedProvider(this Components component, SettingsManager settingsManager) => component switch
public static AIStudio.Settings.Provider PreselectedProvider(this Components component, SettingsManager settingsManager)
{ {
var preselectedProvider = component switch Components.GRAMMAR_SPELLING_ASSISTANT => settingsManager.ConfigurationData.GrammarSpelling.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.GrammarSpelling.PreselectedProvider) : Settings.Provider.NONE,
{ Components.ICON_FINDER_ASSISTANT => settingsManager.ConfigurationData.IconFinder.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.IconFinder.PreselectedProvider) : Settings.Provider.NONE,
Components.GRAMMAR_SPELLING_ASSISTANT => settingsManager.ConfigurationData.GrammarSpelling.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.GrammarSpelling.PreselectedProvider) : null, Components.REWRITE_ASSISTANT => settingsManager.ConfigurationData.RewriteImprove.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.RewriteImprove.PreselectedProvider) : Settings.Provider.NONE,
Components.ICON_FINDER_ASSISTANT => settingsManager.ConfigurationData.IconFinder.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.IconFinder.PreselectedProvider) : null, Components.PROMPT_OPTIMIZER_ASSISTANT => settingsManager.ConfigurationData.PromptOptimizer.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.PromptOptimizer.PreselectedProvider) : Settings.Provider.NONE,
Components.REWRITE_ASSISTANT => settingsManager.ConfigurationData.RewriteImprove.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.RewriteImprove.PreselectedProvider) : null, Components.TRANSLATION_ASSISTANT => settingsManager.ConfigurationData.Translation.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Translation.PreselectedProvider) : Settings.Provider.NONE,
Components.PROMPT_OPTIMIZER_ASSISTANT => settingsManager.ConfigurationData.PromptOptimizer.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.PromptOptimizer.PreselectedProvider) : null, Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Agenda.PreselectedProvider) : Settings.Provider.NONE,
Components.TRANSLATION_ASSISTANT => settingsManager.ConfigurationData.Translation.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Translation.PreselectedProvider) : null, Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Coding.PreselectedProvider) : Settings.Provider.NONE,
Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Agenda.PreselectedProvider) : null, Components.TEXT_SUMMARIZER_ASSISTANT => settingsManager.ConfigurationData.TextSummarizer.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.TextSummarizer.PreselectedProvider) : Settings.Provider.NONE,
Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Coding.PreselectedProvider) : null, Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.EMail.PreselectedProvider) : Settings.Provider.NONE,
Components.TEXT_SUMMARIZER_ASSISTANT => settingsManager.ConfigurationData.TextSummarizer.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.TextSummarizer.PreselectedProvider) : null, Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.LegalCheck.PreselectedProvider) : Settings.Provider.NONE,
Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.EMail.PreselectedProvider) : null, Components.SYNONYMS_ASSISTANT => settingsManager.ConfigurationData.Synonyms.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Synonyms.PreselectedProvider) : Settings.Provider.NONE,
Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.LegalCheck.PreselectedProvider) : null, Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.MyTasks.PreselectedProvider) : Settings.Provider.NONE,
Components.SYNONYMS_ASSISTANT => settingsManager.ConfigurationData.Synonyms.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Synonyms.PreselectedProvider) : null, Components.JOB_POSTING_ASSISTANT => settingsManager.ConfigurationData.JobPostings.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.JobPostings.PreselectedProvider) : Settings.Provider.NONE,
Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.MyTasks.PreselectedProvider) : null, Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider) : Settings.Provider.NONE,
Components.JOB_POSTING_ASSISTANT => settingsManager.ConfigurationData.JobPostings.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.JobPostings.PreselectedProvider) : null, Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.ERI.PreselectedProvider) : Settings.Provider.NONE,
Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider) : null, Components.I18N_ASSISTANT => settingsManager.ConfigurationData.I18N.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.I18N.PreselectedProvider) : Settings.Provider.NONE,
Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.ERI.PreselectedProvider) : null, Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.SlideBuilder.PreselectedProvider) : Settings.Provider.NONE,
Components.I18N_ASSISTANT => settingsManager.ConfigurationData.I18N.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.I18N.PreselectedProvider) : null, Components.VISUAL_BRIEFING_ASSISTANT => settingsManager.GetProviderById(settingsManager.ConfigurationData.VisualBriefing.PreselectedProvider),
Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.SlideBuilder.PreselectedProvider) : null,
Components.VISUAL_BRIEFING_ASSISTANT => settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.VisualBriefing.PreselectedProvider),
// The Document Analysis Assistant does not have a preselected provider at the component level. // The Document Analysis Assistant does not have a preselected provider at the component level.
// The provider is selected per policy instead. We do this inside the Document Analysis Assistant component. // The provider is selected per policy instead. We do this inside the Document Analysis Assistant component.
Components.DOCUMENT_ANALYSIS_ASSISTANT => Settings.Provider.NONE, Components.DOCUMENT_ANALYSIS_ASSISTANT => Settings.Provider.NONE,
Components.BATCH_PROCESSING_ASSISTANT => settingsManager.ConfigurationData.BatchProcessing.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.BatchProcessing.PreselectedProvider) : null, Components.BATCH_PROCESSING_ASSISTANT => settingsManager.ConfigurationData.BatchProcessing.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.BatchProcessing.PreselectedProvider) : Settings.Provider.NONE,
Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Chat.PreselectedProvider) : null, Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Chat.PreselectedProvider) : Settings.Provider.NONE,
Components.AGENT_TEXT_CONTENT_CLEANER => settingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.TextContentCleaner.PreselectedAgentProvider) : null, Components.AGENT_TEXT_CONTENT_CLEANER => settingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.TextContentCleaner.PreselectedAgentProvider) : Settings.Provider.NONE,
Components.AGENT_DATA_SOURCE_SELECTION => settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectedAgentProvider) : null, Components.AGENT_DATA_SOURCE_SELECTION => settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectedAgentProvider) : Settings.Provider.NONE,
Components.AGENT_RETRIEVAL_CONTEXT_VALIDATION => settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectAgentOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectedAgentProvider) : null, Components.AGENT_RETRIEVAL_CONTEXT_VALIDATION => settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectAgentOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectedAgentProvider) : Settings.Provider.NONE,
Components.AGENT_ASSISTANT_PLUGIN_AUDIT => settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.AssistantPluginAudit.PreselectedAgentProvider), Components.AGENT_ASSISTANT_PLUGIN_AUDIT => settingsManager.GetProviderById(settingsManager.ConfigurationData.AssistantPluginAudit.PreselectedAgentProvider),
_ => Settings.Provider.NONE, _ => Settings.Provider.NONE,
}; };
return preselectedProvider ?? Settings.Provider.NONE;
}
public static ProfilePreselection GetProfilePreselection(this Components component, SettingsManager settingsManager) public static ProfilePreselection GetProfilePreselection(this Components component, SettingsManager settingsManager)
{ {

View File

@ -17,19 +17,29 @@ public sealed class ProviderAccessAnalyzer : DiagnosticAnalyzer
private static readonly string TITLE = "Direct access to `Providers` is not allowed"; private static readonly string TITLE = "Direct access to `Providers` is not allowed";
private static readonly string MESSAGE_FORMAT = "Direct access to `SettingsManager.ConfigurationData.Providers` is not allowed. Instead, use APIs like `SettingsManager.GetPreselectedProvider`, etc."; private static readonly string MESSAGE_FORMAT = "Direct access to `SettingsManager.ConfigurationData.Providers` is not allowed. Instead, use APIs like `SettingsManager.GetAllProviders`, `GetProviderById`, `GetConfidentProviders`, `GetPreselectedProvider`, or `GetChatProviderForLoadedChat`.";
private static readonly string DESCRIPTION = MESSAGE_FORMAT; private static readonly string DESCRIPTION = MESSAGE_FORMAT;
private const string CATEGORY = "Usage"; private const string CATEGORY = "Usage";
/// <summary>
/// The one type which owns the provider list and is therefore allowed to access it directly.
/// </summary>
private const string OWNING_TYPE = "AIStudio.Settings.SettingsManager";
private static readonly DiagnosticDescriptor RULE = new(DIAGNOSTIC_ID, TITLE, MESSAGE_FORMAT, CATEGORY, DiagnosticSeverity.Error, isEnabledByDefault: true, description: DESCRIPTION); private static readonly DiagnosticDescriptor RULE = new(DIAGNOSTIC_ID, TITLE, MESSAGE_FORMAT, CATEGORY, DiagnosticSeverity.Error, isEnabledByDefault: true, description: DESCRIPTION);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [RULE]; public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [RULE];
public override void Initialize(AnalysisContext context) public override void Initialize(AnalysisContext context)
{ {
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); //
// We analyze generated code as well, because Razor markup ends up in generated files. Without
// this, any `ConfigurationData.Providers` access written directly in a `.razor` file would
// bypass this rule entirely. The Razor compiler maps the diagnostic back to the `.razor` line:
//
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.EnableConcurrentExecution(); context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(this.AnalyzeMemberAccess, SyntaxKind.SimpleMemberAccessExpression); context.RegisterSyntaxNodeAction(this.AnalyzeMemberAccess, SyntaxKind.SimpleMemberAccessExpression);
} }
@ -42,8 +52,17 @@ public sealed class ProviderAccessAnalyzer : DiagnosticAnalyzer
if (memberAccess.Name.Identifier.Text != "Providers") if (memberAccess.Name.Identifier.Text != "Providers")
return; return;
//
// The settings manager owns the provider list: it implements the very APIs which all other
// code is meant to use, so it must access `Providers` directly. Exempting it here keeps
// those implementations free of suppression attributes, which would otherwise read as if
// suppressing this rule was a normal thing to do:
//
if (IsOwningType(context.ContainingSymbol))
return;
// Get the full path of the member access: // Get the full path of the member access:
var fullPath = this.GetFullMemberAccessPath(memberAccess); var fullPath = GetFullMemberAccessPath(memberAccess);
// Check for the forbidden pattern: // Check for the forbidden pattern:
if (fullPath.EndsWith("ConfigurationData.Providers")) if (fullPath.EndsWith("ConfigurationData.Providers"))
@ -53,7 +72,30 @@ public sealed class ProviderAccessAnalyzer : DiagnosticAnalyzer
} }
} }
private string GetFullMemberAccessPath(ExpressionSyntax expression) /// <summary>
/// Checks whether the analyzed node sits inside the type which owns the provider list.
/// </summary>
/// <remarks>
/// The containing symbol is the member the node belongs to, e.g. a method or a property. We walk
/// the chain of containing types so that nested types of the owning type are covered as well.
/// </remarks>
/// <param name="containingSymbol">The symbol containing the analyzed node, which may be null.</param>
/// <returns>True, when the node belongs to the owning type.</returns>
private static bool IsOwningType(ISymbol? containingSymbol)
{
var containingType = containingSymbol as INamedTypeSymbol ?? containingSymbol?.ContainingType;
while (containingType != null)
{
if (containingType.ToDisplayString() == OWNING_TYPE)
return true;
containingType = containingType.ContainingType;
}
return false;
}
private static string GetFullMemberAccessPath(ExpressionSyntax expression)
{ {
var parts = new List<string>(); var parts = new List<string>();
while (expression is MemberAccessExpressionSyntax memberAccess) while (expression is MemberAccessExpressionSyntax memberAccess)