2025-06-27 20:52:34 +00:00
using System.Linq.Expressions ;
2024-04-19 19:19:13 +00:00
using System.Text.Json ;
2024-07-28 19:18:17 +00:00
2024-09-11 21:08:02 +00:00
using AIStudio.Provider ;
2024-07-28 09:20:00 +00:00
using AIStudio.Settings.DataModel ;
2025-03-29 17:40:17 +00:00
using AIStudio.Tools.PluginSystem ;
2025-04-12 19:13:33 +00:00
using AIStudio.Tools.Services ;
2024-04-19 19:19:13 +00:00
2024-04-20 15:06:50 +00:00
// ReSharper disable NotAccessedPositionalProperty.Local
2024-04-19 19:19:13 +00:00
namespace AIStudio.Settings ;
2024-05-04 08:55:00 +00:00
/// <summary>
/// The settings manager.
/// </summary>
2025-05-03 19:47:43 +00:00
public sealed class SettingsManager
2024-04-19 19:19:13 +00:00
{
private const string SETTINGS_FILENAME = "settings.json" ;
2026-06-21 16:46:21 +00:00
private const Version CURRENT_SETTINGS_VERSION = Version . V6 ;
private readonly record struct SettingsVersionReadResult ( Version Version , SettingsWriteBlockReason FailureReason ) ;
private readonly record struct CurrentSettingsReadResult ( Data ? SettingsData , SettingsWriteBlockReason FailureReason ) ;
2024-04-19 19:19:13 +00:00
2026-08-09 18:47:30 +00:00
internal static readonly JsonSerializerOptions JSON_OPTIONS = new ( )
2024-07-28 19:18:17 +00:00
{
WriteIndented = true ,
2025-04-12 08:08:36 +00:00
Converters = { new TolerantEnumConverter ( ) } ,
2024-07-28 19:18:17 +00:00
} ;
2024-09-01 18:10:03 +00:00
2025-05-03 19:47:43 +00:00
private readonly ILogger < SettingsManager > logger ;
private readonly RustService rustService ;
/// <summary>
/// The settings manager.
/// </summary>
public SettingsManager ( ILogger < SettingsManager > logger , RustService rustService )
{
this . logger = logger ;
this . rustService = rustService ;
this . logger . LogInformation ( "Settings manager created." ) ;
}
2024-05-04 08:55:00 +00:00
/// <summary>
/// The directory where the configuration files are stored.
/// </summary>
2024-04-19 19:19:13 +00:00
public static string? ConfigDirectory { get ; set ; }
2024-05-04 08:55:00 +00:00
/// <summary>
/// The directory where the data files are stored.
/// </summary>
2024-04-19 19:19:13 +00:00
public static string? DataDirectory { get ; set ; }
2024-09-15 10:30:07 +00:00
/// <summary>
/// Whether the app is in dark mode.
/// </summary>
public bool IsDarkMode { get ; set ; }
2026-03-21 17:05:06 +00:00
/// <summary>
/// Ensures that the startup start-page redirect is evaluated at most once per app session.
/// </summary>
public bool StartupStartPageRedirectHandled { get ; set ; }
/// <summary>
/// Indicates that the initial settings load attempt has completed.
/// </summary>
public bool HasCompletedInitialSettingsLoad { get ; private set ; }
2026-06-21 16:46:21 +00:00
/// <summary>
/// Indicates why settings writes are blocked for the current session.
/// </summary>
public SettingsWriteBlockReason SettingsWriteBlockReason { get ; private set ; } = SettingsWriteBlockReason . NONE ;
/// <summary>
/// Indicates that settings writes are blocked for the current session.
/// </summary>
public bool SettingsWriteBlocked = > this . SettingsWriteBlockReason is not SettingsWriteBlockReason . NONE ;
2024-05-04 08:55:00 +00:00
/// <summary>
/// The configuration data.
/// </summary>
2024-04-20 15:06:50 +00:00
public Data ConfigurationData { get ; private set ; } = new ( ) ;
private bool IsSetUp = > ! string . IsNullOrWhiteSpace ( ConfigDirectory ) & & ! string . IsNullOrWhiteSpace ( DataDirectory ) ;
2024-05-04 08:55:00 +00:00
/// <summary>
/// Loads the settings from the file system.
/// </summary>
2024-04-20 15:06:50 +00:00
public async Task LoadSettings ( )
2026-03-10 19:50:45 +00:00
{
var settingsSnapshot = await this . TryReadSettingsSnapshot ( ) ;
if ( settingsSnapshot is not null )
this . ConfigurationData = settingsSnapshot ;
2026-03-21 17:05:06 +00:00
this . HasCompletedInitialSettingsLoad = true ;
2026-03-10 19:50:45 +00:00
}
/// <summary>
/// Reads the settings from disk without mutating the current in-memory state.
/// </summary>
/// <returns>A (migrated) settings snapshot, or null if it could not be read.</returns>
public async Task < Data ? > TryReadSettingsSnapshot ( )
2024-04-19 19:19:13 +00:00
{
2026-06-21 16:46:21 +00:00
this . SettingsWriteBlockReason = SettingsWriteBlockReason . NONE ;
2024-04-19 19:19:13 +00:00
if ( ! this . IsSetUp )
2024-09-01 18:10:03 +00:00
{
this . logger . LogWarning ( "Cannot load settings, because the configuration is not set up yet." ) ;
2026-03-10 19:50:45 +00:00
return null ;
2024-09-01 18:10:03 +00:00
}
2024-04-19 19:19:13 +00:00
var settingsPath = Path . Combine ( ConfigDirectory ! , SETTINGS_FILENAME ) ;
if ( ! File . Exists ( settingsPath ) )
2024-09-01 18:10:03 +00:00
{
this . logger . LogWarning ( "Cannot load settings, because the settings file does not exist." ) ;
2026-03-10 19:50:45 +00:00
return null ;
2024-09-01 18:10:03 +00:00
}
2026-06-21 16:46:21 +00:00
var settingsVersion = await this . TryReadSettingsVersion ( settingsPath ) ;
if ( settingsVersion . FailureReason is not SettingsWriteBlockReason . NONE )
2024-08-05 19:12:52 +00:00
{
2026-06-21 16:46:21 +00:00
this . BlockSettingsWrites ( settingsVersion . FailureReason , "The settings file version could not be identified. Settings writes are blocked to avoid overwriting newer or unreadable settings." ) ;
return await this . TryReadCurrentVersionBackupSnapshotForBlockedSettings ( ) ;
}
2024-08-05 19:12:52 +00:00
2026-06-21 16:46:21 +00:00
if ( settingsVersion . Version > CURRENT_SETTINGS_VERSION )
{
this . BlockSettingsWrites ( SettingsWriteBlockReason . VERSION_NEWER_THAN_APP , $"The settings file uses the newer version '{settingsVersion.Version}'. Settings writes are blocked to avoid overwriting newer settings." ) ;
return await this . TryReadCurrentVersionBackupSnapshotForBlockedSettings ( ) ;
}
2026-03-10 19:50:45 +00:00
2026-06-21 16:46:21 +00:00
Data ? settingsData ;
if ( settingsVersion . Version < CURRENT_SETTINGS_VERSION )
{
settingsData = await this . TryReadCurrentVersionBackupSnapshot ( ) ;
if ( settingsData is not null )
2024-08-05 19:12:52 +00:00
{
2026-06-21 16:46:21 +00:00
this . PrepareLoadedSettings ( settingsData ) ;
await this . StoreSettingsSnapshot ( settingsData , settingsPath ) ;
await this . StoreCurrentVersionBackup ( settingsData ) ;
this . logger . LogInformation ( $"Restored settings from the '{GetBackupSettingsFilename(CURRENT_SETTINGS_VERSION)}' backup file." ) ;
return settingsData ;
2024-08-05 19:12:52 +00:00
}
2026-03-10 19:50:45 +00:00
2026-06-21 16:46:21 +00:00
this . logger . LogInformation ( "No valid current-version settings backup was found. Migrating the settings file." ) ;
settingsData = SettingsMigrations . Migrate ( this . logger , settingsVersion . Version , await File . ReadAllTextAsync ( settingsPath ) , JSON_OPTIONS ) ;
this . PrepareLoadedSettings ( settingsData ) ;
await this . StoreSettingsSnapshot ( settingsData , settingsPath ) ;
await this . StoreCurrentVersionBackup ( settingsData ) ;
2026-03-10 19:50:45 +00:00
return settingsData ;
2024-08-05 19:12:52 +00:00
}
2026-03-10 19:50:45 +00:00
2026-06-21 16:46:21 +00:00
var currentSettings = await this . TryDeserializeCurrentSettings ( settingsPath , "settings file" ) ;
if ( currentSettings . FailureReason is not SettingsWriteBlockReason . NONE )
{
this . BlockSettingsWrites ( currentSettings . FailureReason , "The current settings file could not be safely loaded. Settings writes are blocked to avoid overwriting recoverable settings." ) ;
return await this . TryReadCurrentVersionBackupSnapshotForBlockedSettings ( ) ;
}
settingsData = currentSettings . SettingsData ! ;
this . PrepareLoadedSettings ( settingsData ) ;
await this . StoreCurrentVersionBackup ( settingsData ) ;
return settingsData ;
}
private async Task < SettingsVersionReadResult > TryReadSettingsVersion ( string settingsPath )
{
try
{
await using var settingsStream = File . OpenRead ( settingsPath ) ;
using var settingsDocument = await JsonDocument . ParseAsync ( settingsStream ) ;
if ( ! settingsDocument . RootElement . TryGetProperty ( "Version" , out var versionElement ) )
{
this . logger . LogError ( $"Failed to read the version of the settings file '{settingsPath}'." ) ;
return new ( Version . UNKNOWN , SettingsWriteBlockReason . VERSION_MISSING ) ;
}
if ( versionElement . ValueKind is JsonValueKind . String & & versionElement . GetString ( ) is { } versionText )
{
if ( Enum . TryParse ( versionText , out Version stringVersion ) & & Enum . IsDefined ( stringVersion ) & & stringVersion is not Version . UNKNOWN )
return new ( stringVersion , SettingsWriteBlockReason . NONE ) ;
if ( versionText . StartsWith ( 'V' ) & & int . TryParse ( versionText [ 1. . ] , out var futureVersion ) & & futureVersion > ( int ) CURRENT_SETTINGS_VERSION )
return new ( ( Version ) futureVersion , SettingsWriteBlockReason . NONE ) ;
if ( int . TryParse ( versionText , out var numericStringVersion ) & & numericStringVersion > ( int ) CURRENT_SETTINGS_VERSION )
return new ( ( Version ) numericStringVersion , SettingsWriteBlockReason . NONE ) ;
}
if ( versionElement . ValueKind is JsonValueKind . Number & & versionElement . TryGetInt32 ( out var numericVersion ) & & numericVersion > ( int ) Version . UNKNOWN & & ( Enum . IsDefined ( typeof ( Version ) , numericVersion ) | | numericVersion > ( int ) CURRENT_SETTINGS_VERSION ) )
return new ( ( Version ) numericVersion , SettingsWriteBlockReason . NONE ) ;
}
catch ( Exception e )
{
this . logger . LogError ( e , $"Failed to read the version of the settings file '{settingsPath}'." ) ;
return new ( Version . UNKNOWN , SettingsWriteBlockReason . FILE_UNREADABLE ) ;
}
return new ( Version . UNKNOWN , SettingsWriteBlockReason . VERSION_UNKNOWN ) ;
}
private async Task < Data ? > TryReadCurrentVersionBackupSnapshot ( )
{
var backupSettingsPath = GetBackupSettingsPath ( CURRENT_SETTINGS_VERSION ) ;
if ( ! File . Exists ( backupSettingsPath ) )
{
this . logger . LogInformation ( $"The settings backup file '{backupSettingsPath}' does not exist." ) ;
return null ;
}
var backupVersion = await this . TryReadSettingsVersion ( backupSettingsPath ) ;
if ( backupVersion . FailureReason is not SettingsWriteBlockReason . NONE )
{
this . logger . LogWarning ( $"The settings backup file '{backupSettingsPath}' could not be used because its version could not be identified. Reason: '{backupVersion.FailureReason}'." ) ;
return null ;
}
if ( backupVersion . Version ! = CURRENT_SETTINGS_VERSION )
{
this . logger . LogWarning ( $"The settings backup file '{backupSettingsPath}' uses version '{backupVersion.Version}' instead of '{CURRENT_SETTINGS_VERSION}'." ) ;
return null ;
}
var backupSettings = await this . TryDeserializeCurrentSettings ( backupSettingsPath , "settings backup file" ) ;
if ( backupSettings . FailureReason is not SettingsWriteBlockReason . NONE )
{
this . logger . LogWarning ( $"The settings backup file '{backupSettingsPath}' could not be used. Reason: '{backupSettings.FailureReason}'." ) ;
return null ;
}
return backupSettings . SettingsData ;
}
private async Task < Data ? > TryReadCurrentVersionBackupSnapshotForBlockedSettings ( )
{
var settingsData = await this . TryReadCurrentVersionBackupSnapshot ( ) ;
if ( settingsData is null )
{
this . logger . LogWarning ( $"No valid current-version settings backup was found while settings writes are blocked. Reason: '{this.SettingsWriteBlockReason}'." ) ;
return null ;
}
this . PrepareLoadedSettings ( settingsData ) ;
this . logger . LogWarning ( $"Loaded settings from the '{GetBackupSettingsFilename(CURRENT_SETTINGS_VERSION)}' backup file while settings writes remain blocked. Reason: '{this.SettingsWriteBlockReason}'." ) ;
return settingsData ;
}
private async Task < CurrentSettingsReadResult > TryDeserializeCurrentSettings ( string settingsPath , string sourceDescription )
{
try
{
var settingsData = JsonSerializer . Deserialize < Data > ( await File . ReadAllTextAsync ( settingsPath ) , JSON_OPTIONS ) ;
if ( settingsData is null )
{
this . logger . LogError ( $"Failed to parse the {sourceDescription} '{settingsPath}'." ) ;
return new ( null , SettingsWriteBlockReason . CURRENT_VERSION_INVALID ) ;
}
if ( settingsData . Version ! = CURRENT_SETTINGS_VERSION )
{
this . logger . LogError ( $"The {sourceDescription} '{settingsPath}' uses version '{settingsData.Version}' instead of '{CURRENT_SETTINGS_VERSION}'." ) ;
return new ( null , SettingsWriteBlockReason . CURRENT_VERSION_INVALID ) ;
}
return new ( settingsData , SettingsWriteBlockReason . NONE ) ;
}
catch ( Exception e )
{
this . logger . LogError ( e , $"Failed to parse the {sourceDescription} '{settingsPath}'." ) ;
return new ( null , SettingsWriteBlockReason . FILE_UNREADABLE ) ;
}
}
private void BlockSettingsWrites ( SettingsWriteBlockReason reason , string message )
{
this . SettingsWriteBlockReason = reason ;
this . logger . LogError ( $"{message} Reason: '{reason}'." ) ;
}
private void PrepareLoadedSettings ( Data settingsData )
{
//
// We filter the enabled preview features based on the preview visibility.
// This is necessary when the app starts up: some preview features may have
// been disabled or released from the last time the app was started.
//
settingsData . App . EnabledPreviewFeatures = settingsData . App . PreviewVisibility . FilterPreviewFeatures ( settingsData . App . EnabledPreviewFeatures ) ;
2024-04-19 19:19:13 +00:00
}
2024-04-20 15:06:50 +00:00
2024-05-04 08:55:00 +00:00
/// <summary>
/// Stores the settings to the file system.
/// </summary>
2024-04-20 15:06:50 +00:00
public async Task StoreSettings ( )
2024-04-19 19:19:13 +00:00
{
if ( ! this . IsSetUp )
2024-09-01 18:10:03 +00:00
{
this . logger . LogWarning ( "Cannot store settings, because the configuration is not set up yet." ) ;
2024-04-19 19:19:13 +00:00
return ;
2024-09-01 18:10:03 +00:00
}
2026-06-21 16:46:21 +00:00
if ( this . SettingsWriteBlocked )
{
this . logger . LogWarning ( $"Cannot store settings, because settings writes are blocked. Reason: '{this.SettingsWriteBlockReason}'." ) ;
return ;
}
2024-04-19 19:19:13 +00:00
var settingsPath = Path . Combine ( ConfigDirectory ! , SETTINGS_FILENAME ) ;
2026-06-21 16:46:21 +00:00
await this . StoreSettingsSnapshot ( this . ConfigurationData , settingsPath ) ;
await this . StoreCurrentVersionBackup ( this . ConfigurationData ) ;
}
private static string GetBackupSettingsFilename ( Version version ) = > $"settings.{version.ToString().ToLowerInvariant()}.json" ;
private static string GetBackupSettingsPath ( Version version ) = > Path . Combine ( ConfigDirectory ! , GetBackupSettingsFilename ( version ) ) ;
private async Task StoreCurrentVersionBackup ( Data settingsData )
{
if ( settingsData . Version ! = CURRENT_SETTINGS_VERSION )
{
this . logger . LogWarning ( $"Skipping settings backup because the settings version '{settingsData.Version}' is not the current version '{CURRENT_SETTINGS_VERSION}'." ) ;
return ;
}
var backupSettingsPath = GetBackupSettingsPath ( CURRENT_SETTINGS_VERSION ) ;
await this . StoreSettingsSnapshot ( settingsData , backupSettingsPath ) ;
this . logger . LogInformation ( $"Stored the settings backup file '{backupSettingsPath}'." ) ;
}
private async Task StoreSettingsSnapshot ( Data settingsData , string settingsPath )
{
2024-04-20 15:06:50 +00:00
if ( ! Directory . Exists ( ConfigDirectory ) )
2024-09-01 18:10:03 +00:00
{
this . logger . LogInformation ( "Creating the configuration directory." ) ;
2024-04-20 15:06:50 +00:00
Directory . CreateDirectory ( ConfigDirectory ! ) ;
2024-09-01 18:10:03 +00:00
}
2026-06-21 16:46:21 +00:00
var settingsJson = JsonSerializer . Serialize ( settingsData , JSON_OPTIONS ) ;
2026-08-15 17:55:42 +00:00
//
// We write the new settings next to the previous ones and replace them afterwards, so that
// no crash can leave a half-written settings file behind. The temporary file has to live in
// the configuration directory for that: replacing a file is a rename, and a rename across a
// file system boundary falls back to copying, which is exactly what we want to avoid. The
// temporary directory of the operating system is such another file system under Flatpak.
//
var tempFile = $"{settingsPath}.tmp-{Guid.NewGuid():N}" ;
try
{
await File . WriteAllTextAsync ( tempFile , settingsJson ) ;
File . Move ( tempFile , settingsPath , true ) ;
}
catch
{
try
{
if ( File . Exists ( tempFile ) )
File . Delete ( tempFile ) ;
}
catch ( Exception cleanupException )
{
this . logger . LogWarning ( cleanupException , $"Failed to delete the temporary settings file '{tempFile}'." ) ;
}
throw ;
}
2026-06-21 16:46:21 +00:00
this . logger . LogInformation ( $"Stored the settings to '{settingsPath}'." ) ;
2024-04-19 19:19:13 +00:00
}
2024-06-01 17:55:12 +00:00
2024-08-05 19:12:52 +00:00
public void InjectSpellchecking ( Dictionary < string , object? > attributes ) = > attributes [ "spellcheck" ] = this . ConfigurationData . App . EnableSpellchecking ? "true" : "false" ;
2024-09-04 13:44:23 +00:00
2024-09-14 17:20:33 +00:00
public ConfidenceLevel GetMinimumConfidenceLevel ( Tools . Components component )
{
var minimumLevel = ConfidenceLevel . NONE ;
2026-06-21 09:52:02 +00:00
var enforceGlobalMinimumConfidence = this . ConfigurationData . Confidence is { EnforceGlobalMinimumConfidence : true , GlobalMinimumConfidence : not ConfidenceLevel . NONE and not ConfidenceLevel . UNKNOWN } ;
2024-09-14 17:20:33 +00:00
if ( enforceGlobalMinimumConfidence )
2026-06-21 09:52:02 +00:00
minimumLevel = this . ConfigurationData . Confidence . GlobalMinimumConfidence ;
2024-09-14 17:20:33 +00:00
var componentMinimumLevel = component . MinimumConfidence ( this ) ;
if ( componentMinimumLevel > minimumLevel )
minimumLevel = componentMinimumLevel ;
return minimumLevel ;
}
2025-04-12 19:13:33 +00:00
/// <summary>
/// Checks if the given plugin is enabled.
/// </summary>
2026-08-29 12:12:14 +00:00
/// <remarks>
/// Which plugins are enabled is the user's decision, with two exceptions. Configuration plugins
/// have no switch at all: they carry what an organization configured, so turning them off would
/// mean opting out of that configuration. And an organization may require one of the assistant
/// plugins it approved to stay enabled, which is decided live from its approvals rather than from
/// the user's list.
/// </remarks>
2025-04-12 19:13:33 +00:00
/// <param name="plugin">The plugin to check.</param>
/// <returns>True, when the plugin is enabled, false otherwise.</returns>
2026-08-29 12:12:14 +00:00
public bool IsPluginEnabled ( IPluginMetadata plugin ) = > plugin . Type is PluginType . CONFIGURATION | | this . ConfigurationData . EnabledPlugins . Contains ( plugin . Id ) | | PluginFactory . IsAssistantActivationEnforced ( plugin . Id ) ;
2025-03-29 17:40:17 +00:00
2025-04-12 19:13:33 +00:00
/// <summary>
/// Returns the active language plugin.
/// </summary>
/// <returns>The active language plugin.</returns>
public async Task < ILanguagePlugin > GetActiveLanguagePlugin ( )
{
switch ( this . ConfigurationData . App . LanguageBehavior )
{
case LangBehavior . AUTO :
var languageCode = await this . rustService . ReadUserLanguage ( ) ;
2026-02-16 11:13:36 +00:00
var languagePlugins = PluginFactory . RunningPlugins . OfType < ILanguagePlugin > ( ) . ToList ( ) ;
if ( ! string . IsNullOrWhiteSpace ( languageCode ) )
2025-04-27 07:06:05 +00:00
{
2026-02-16 11:13:36 +00:00
var exactMatch = languagePlugins . FirstOrDefault ( x = > string . Equals ( x . IETFTag , languageCode , StringComparison . OrdinalIgnoreCase ) ) ;
if ( exactMatch is not null )
return exactMatch ;
var primaryLanguage = GetPrimaryLanguage ( languageCode ) ;
if ( ! string . IsNullOrWhiteSpace ( primaryLanguage ) )
{
var primaryLanguageMatch = languagePlugins
. Where ( x = > string . Equals ( GetPrimaryLanguage ( x . IETFTag ) , primaryLanguage , StringComparison . OrdinalIgnoreCase ) )
. OrderBy ( x = > x . IETFTag , StringComparer . OrdinalIgnoreCase )
. FirstOrDefault ( ) ;
if ( primaryLanguageMatch is not null )
{
this . logger . LogWarning ( $"No exact language plugin found for '{languageCode}'. Use language fallback '{primaryLanguageMatch.IETFTag}'." ) ;
return primaryLanguageMatch ;
}
}
2025-04-27 07:06:05 +00:00
}
2026-02-16 11:13:36 +00:00
this . logger . LogWarning ( $"The language plugin for the language '{languageCode}' (normalized='{languageCode}') is not available." ) ;
2025-04-12 19:13:33 +00:00
return PluginFactory . BaseLanguage ;
case LangBehavior . MANUAL :
var pluginId = this . ConfigurationData . App . LanguagePluginId ;
var plugin = PluginFactory . RunningPlugins . FirstOrDefault ( x = > x . Id = = pluginId ) ;
if ( plugin is null )
{
this . logger . LogWarning ( $"The chosen language plugin (id='{pluginId}') is not available." ) ;
return PluginFactory . BaseLanguage ;
}
if ( plugin is ILanguagePlugin chosenLangPlugin )
return chosenLangPlugin ;
2025-04-27 07:06:05 +00:00
2025-04-12 19:13:33 +00:00
this . logger . LogError ( "The chosen language plugin is not a language plugin." ) ;
return PluginFactory . BaseLanguage ;
}
this . logger . LogError ( "The language behavior is unknown." ) ;
return PluginFactory . BaseLanguage ;
}
2026-02-16 11:13:36 +00:00
private static string GetPrimaryLanguage ( string localeTag )
{
if ( string . IsNullOrWhiteSpace ( localeTag ) )
return string . Empty ;
var separatorIndex = localeTag . IndexOf ( '-' ) ;
if ( separatorIndex < 0 )
return localeTag ;
return localeTag [ . . separatorIndex ] ;
}
2025-04-12 19:13:33 +00:00
2025-02-23 14:05:29 +00:00
public Provider GetPreselectedProvider ( Tools . Components component , string? currentProviderId = null , bool usePreselectionBeforeCurrentProvider = false )
2024-09-04 13:44:23 +00:00
{
2024-09-14 17:20:33 +00:00
var minimumLevel = this . GetMinimumConfidenceLevel ( component ) ;
// When there is only one provider, and it has a confidence level that is high enough, we return it:
2024-09-13 21:29:19 +00:00
if ( this . ConfigurationData . Providers . Count = = 1 & & this . ConfigurationData . Providers [ 0 ] . UsedLLMProvider . GetConfidence ( this ) . Level > = minimumLevel )
2024-09-04 13:44:23 +00:00
return this . ConfigurationData . Providers [ 0 ] ;
2025-02-23 14:05:29 +00:00
// Is there a current provider with a sufficiently high confidence level?
2025-08-26 08:59:56 +00:00
var currentProvider = Provider . NONE ;
2025-02-23 14:05:29 +00:00
if ( currentProviderId is not null & & ! string . IsNullOrWhiteSpace ( currentProviderId ) )
2024-11-23 12:04:02 +00:00
{
2025-02-23 14:05:29 +00:00
var currentProviderProbe = this . ConfigurationData . Providers . FirstOrDefault ( x = > x . Id = = currentProviderId ) ;
2025-08-26 08:59:56 +00:00
if ( currentProviderProbe is not null & & currentProviderProbe . UsedLLMProvider . GetConfidence ( this ) . Level > = minimumLevel )
2025-02-23 14:05:29 +00:00
currentProvider = currentProviderProbe ;
2024-11-23 12:04:02 +00:00
}
2025-02-23 14:05:29 +00:00
// Is there a component-preselected provider with a sufficiently high confidence level?
2025-08-26 08:59:56 +00:00
var preselectedProvider = Provider . NONE ;
2025-02-23 14:05:29 +00:00
var preselectedProviderProbe = component . PreselectedProvider ( this ) ;
2025-08-26 08:59:56 +00:00
if ( preselectedProviderProbe ! = Provider . NONE & & preselectedProviderProbe . UsedLLMProvider . GetConfidence ( this ) . Level > = minimumLevel )
2025-02-23 14:05:29 +00:00
preselectedProvider = preselectedProviderProbe ;
//
// Case: The preselected provider should be used before the current provider,
// and the preselected provider is available and has a confidence level
// that is high enough.
//
2025-08-26 08:59:56 +00:00
if ( usePreselectionBeforeCurrentProvider & & preselectedProvider ! = Provider . NONE )
2025-02-23 14:05:29 +00:00
return preselectedProvider ;
//
// Case: The current provider is available and has a confidence level that is
// high enough.
//
2025-08-26 08:59:56 +00:00
if ( currentProvider ! = Provider . NONE )
2025-02-23 14:05:29 +00:00
return currentProvider ;
//
// Case: The current provider should be used before the preselected provider,
// but the current provider is not available or does not have a confidence
// level that is high enough. The preselected provider is available and
// has a confidence level that is high enough.
//
2025-08-26 08:59:56 +00:00
if ( preselectedProvider ! = Provider . NONE )
2024-09-14 17:20:33 +00:00
return preselectedProvider ;
// When there is an app-wide preselected provider, and it has a confidence level that is high enough, we return it:
2025-08-26 08:59:56 +00:00
return this . ConfigurationData . Providers . FirstOrDefault ( x = > x . Id = = this . ConfigurationData . App . PreselectedProvider & & x . UsedLLMProvider . GetConfidence ( this ) . Level > = minimumLevel ) ? ? Provider . NONE ;
2024-09-04 13:44:23 +00:00
}
2024-09-08 19:01:51 +00:00
2026-04-16 07:09:05 +00:00
public Provider GetChatProviderForLoadedChat ( string? chatProviderId = null )
{
var minimumLevel = this . GetMinimumConfidenceLevel ( Tools . Components . CHAT ) ;
var chatProvider = FindProviderById ( chatProviderId ) ;
if ( chatProvider is not null )
return chatProvider ;
var defaultChatProvider = this . ConfigurationData . Chat . PreselectOptions
? FindProviderById ( this . ConfigurationData . Chat . PreselectedProvider )
: null ;
2026-08-12 18:44:48 +00:00
2026-04-16 07:09:05 +00:00
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 ;
2026-08-12 18:44:48 +00:00
Provider ? FindProviderById ( string? providerId )
{
if ( string . IsNullOrWhiteSpace ( providerId ) )
return null ;
var provider = this . ConfigurationData . Providers . FirstOrDefault ( x = > x . Id = = providerId ) ;
return provider is not null & & IsSelectableProvider ( provider ) ? provider : null ;
}
bool IsSelectableProvider ( Provider provider ) = >
provider ! = Provider . NONE
& & provider . UsedLLMProvider ! = LLMProviders . NONE
& & provider . UsedLLMProvider . GetConfidence ( this ) . Level > = minimumLevel ;
}
/// <summary>
/// Returns all configured providers without applying any confidence filtering.
/// </summary>
/// <remarks>
/// <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>
2026-08-12 19:57:58 +00:00
/// The returned list is a sorted copy of the provider list, ordered by the used LLM provider and
/// then by the instance name. This way, all providers of the same LLM provider stay together, and
/// newly added providers appear at their alphabetical position instead of at the end. Callers must
/// not mutate the returned list: adding, editing, or removing providers stays inside the settings UI.
2026-08-12 18:44:48 +00:00
/// </para>
/// </remarks>
/// <returns>All configured providers, unfiltered.</returns>
2026-08-12 19:57:58 +00:00
public IReadOnlyList < Provider > GetAllProviders ( ) = > this . ConfigurationData . Providers
. OrderBy ( x = > x . UsedLLMProvider . ToName ( ) , StringComparer . OrdinalIgnoreCase )
. ThenBy ( x = > x . InstanceName , StringComparer . OrdinalIgnoreCase )
. ThenBy ( x = > x . Num )
. ToList ( ) ;
2026-08-12 18:44:48 +00:00
/// <summary>
/// Returns the provider with the given id, without applying any confidence filtering.
/// </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 ;
if ( string . Equals ( providerId , Provider . NONE . Id , StringComparison . OrdinalIgnoreCase ) )
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>
2026-08-12 19:57:58 +00:00
/// <returns>All providers the component may use, in the same order as GetAllProviders.</returns>
2026-08-12 18:44:48 +00:00
public IEnumerable < Provider > GetConfidentProviders ( Tools . Components component , ConfidenceLevel explicitMinimum = ConfidenceLevel . UNKNOWN )
{
var minimumLevel = this . GetEffectiveMinimumConfidenceLevel ( component , explicitMinimum ) ;
2026-08-12 19:57:58 +00:00
foreach ( var provider in this . GetAllProviders ( ) )
2026-08-12 18:44:48 +00:00
if ( provider . UsedLLMProvider is not LLMProviders . NONE & & provider . UsedLLMProvider . GetConfidence ( this ) . Level > = minimumLevel )
yield return provider ;
2026-04-16 07:09:05 +00:00
}
2026-08-12 19:57:58 +00:00
/// <summary>
/// Returns all configured embedding providers.
/// </summary>
/// <remarks>
/// The returned list is a sorted copy of the embedding provider list, ordered by the used LLM
/// provider and then by the name. Callers must not mutate the returned list: adding, editing, or
/// removing embedding providers stays inside the settings UI.
/// </remarks>
/// <returns>All configured embedding providers.</returns>
public IReadOnlyList < EmbeddingProvider > GetAllEmbeddingProviders ( ) = > this . ConfigurationData . EmbeddingProviders
. OrderBy ( x = > x . UsedLLMProvider . ToName ( ) , StringComparer . OrdinalIgnoreCase )
. ThenBy ( x = > x . Name , StringComparer . OrdinalIgnoreCase )
. ThenBy ( x = > x . Num )
. ToList ( ) ;
2026-08-25 10:46:10 +00:00
/// <summary>
/// Returns the embedding provider with the given id, without applying any confidence filtering.
/// </summary>
/// <remarks>
/// This method resolves a stored embedding provider reference by its id. It applies neither the
/// global minimum confidence level nor any component-specific minimum, so it returns the
/// requested embedding provider even when the user enforces a higher global minimum. Callers
/// that intend to send data to the returned embedding provider must check it themselves, for
/// example through IsTrustedForDataSourceSecurityChecks.
/// </remarks>
/// <param name="embeddingProviderId">The id of the embedding provider to look up.</param>
/// <returns>The embedding provider, or EmbeddingProvider.NONE when no embedding provider with that id exists.</returns>
public EmbeddingProvider GetEmbeddingProviderById ( string? embeddingProviderId )
{
if ( string . IsNullOrWhiteSpace ( embeddingProviderId ) )
return EmbeddingProvider . NONE ;
if ( string . Equals ( embeddingProviderId , EmbeddingProvider . NONE . Id , StringComparison . OrdinalIgnoreCase ) )
return EmbeddingProvider . NONE ;
return this . ConfigurationData . EmbeddingProviders . FirstOrDefault ( x = > x . Id . Equals ( embeddingProviderId , StringComparison . OrdinalIgnoreCase ) ) ? ? EmbeddingProvider . NONE ;
}
2026-08-12 19:57:58 +00:00
/// <summary>
/// Returns all configured transcription providers.
/// </summary>
/// <remarks>
/// The returned list is a sorted copy of the transcription provider list, ordered by the used LLM
/// provider and then by the name. Callers must not mutate the returned list: adding, editing, or
/// removing transcription providers stays inside the settings UI.
/// </remarks>
/// <returns>All configured transcription providers.</returns>
public IReadOnlyList < TranscriptionProvider > GetAllTranscriptionProviders ( ) = > this . ConfigurationData . TranscriptionProviders
. OrderBy ( x = > x . UsedLLMProvider . ToName ( ) , StringComparer . OrdinalIgnoreCase )
. ThenBy ( x = > x . Name , StringComparer . OrdinalIgnoreCase )
. ThenBy ( x = > x . Num )
. ToList ( ) ;
2026-08-25 10:46:10 +00:00
/// <summary>
/// Returns the transcription provider with the given id, without applying any confidence filtering.
/// </summary>
/// <remarks>
/// This method resolves a stored transcription provider reference by its id. It applies neither
/// the global minimum confidence level nor any component-specific minimum, so it returns the
/// requested transcription provider even when the user enforces a higher global minimum. Callers
/// that intend to send audio to the returned transcription provider must check its confidence
/// level themselves, the way GetFilteredTranscriptionProviders does for the app settings.
/// </remarks>
/// <param name="transcriptionProviderId">The id of the transcription provider to look up.</param>
/// <returns>The transcription provider, or TranscriptionProvider.NONE when no transcription provider with that id exists.</returns>
public TranscriptionProvider GetTranscriptionProviderById ( string? transcriptionProviderId )
{
if ( string . IsNullOrWhiteSpace ( transcriptionProviderId ) )
return TranscriptionProvider . NONE ;
if ( string . Equals ( transcriptionProviderId , TranscriptionProvider . NONE . Id , StringComparison . OrdinalIgnoreCase ) )
return TranscriptionProvider . NONE ;
return this . ConfigurationData . TranscriptionProviders . FirstOrDefault ( x = > x . Id . Equals ( transcriptionProviderId , StringComparison . OrdinalIgnoreCase ) ) ? ? TranscriptionProvider . NONE ;
}
2024-09-08 19:01:51 +00:00
public Profile GetPreselectedProfile ( Tools . Components component )
{
2026-03-14 14:40:07 +00:00
var preselection = component . GetProfilePreselection ( this ) ;
if ( preselection . DoNotPreselectProfile )
return Profile . NO_PROFILE ;
if ( preselection . UseSpecificProfile )
2026-06-10 19:01:27 +00:00
return this . GetProfileById ( preselection . SpecificProfileId ) ;
2026-03-14 14:40:07 +00:00
var appPreselection = ProfilePreselection . FromStoredValue ( this . ConfigurationData . App . PreselectedProfile ) ;
if ( appPreselection . DoNotPreselectProfile | | ! appPreselection . UseSpecificProfile )
return Profile . NO_PROFILE ;
2026-06-10 19:01:27 +00:00
return this . GetProfileById ( appPreselection . SpecificProfileId ) ;
2026-03-14 14:40:07 +00:00
}
public Profile GetAppPreselectedProfile ( )
{
var appPreselection = ProfilePreselection . FromStoredValue ( this . ConfigurationData . App . PreselectedProfile ) ;
if ( appPreselection . DoNotPreselectProfile | | ! appPreselection . UseSpecificProfile )
return Profile . NO_PROFILE ;
2026-06-10 19:01:27 +00:00
return this . GetProfileById ( appPreselection . SpecificProfileId ) ;
2024-09-08 19:01:51 +00:00
}
2025-05-24 10:27:00 +00:00
public ChatTemplate GetPreselectedChatTemplate ( Tools . Components component )
{
var preselection = component . PreselectedChatTemplate ( this ) ;
2025-08-18 18:40:52 +00:00
if ( preselection ! = ChatTemplate . NO_CHAT_TEMPLATE )
2025-05-24 10:27:00 +00:00
return preselection ;
2026-06-10 19:01:27 +00:00
return this . GetChatTemplateById ( this . ConfigurationData . App . PreselectedChatTemplate ) ;
}
public Profile GetProfileById ( string? profileId )
{
if ( string . IsNullOrWhiteSpace ( profileId ) )
return Profile . NO_PROFILE ;
if ( string . Equals ( profileId , Profile . NO_PROFILE . Id , StringComparison . OrdinalIgnoreCase ) )
return Profile . NO_PROFILE ;
return this . ConfigurationData . Profiles . FirstOrDefault ( x = > x . Id . Equals ( profileId , StringComparison . OrdinalIgnoreCase ) ) ? ? Profile . NO_PROFILE ;
}
public ChatTemplate GetChatTemplateById ( string? chatTemplateId )
{
if ( string . IsNullOrWhiteSpace ( chatTemplateId ) )
return ChatTemplate . NO_CHAT_TEMPLATE ;
if ( string . Equals ( chatTemplateId , ChatTemplate . NO_CHAT_TEMPLATE . Id , StringComparison . OrdinalIgnoreCase ) )
return ChatTemplate . NO_CHAT_TEMPLATE ;
return this . ConfigurationData . ChatTemplates . FirstOrDefault ( x = > x . Id . Equals ( chatTemplateId , StringComparison . OrdinalIgnoreCase ) ) ? ? ChatTemplate . NO_CHAT_TEMPLATE ;
2025-05-24 10:27:00 +00:00
}
2024-09-11 21:08:02 +00:00
2024-09-13 19:50:00 +00:00
public ConfidenceLevel GetConfiguredConfidenceLevel ( LLMProviders llmProvider )
2024-09-11 21:08:02 +00:00
{
2024-09-13 19:50:00 +00:00
if ( llmProvider is LLMProviders . NONE )
2024-09-11 21:08:02 +00:00
return ConfidenceLevel . NONE ;
2026-06-21 09:52:02 +00:00
switch ( this . ConfigurationData . Confidence . ConfidenceScheme )
2024-09-11 21:08:02 +00:00
{
2025-02-27 11:43:19 +00:00
case ConfidenceSchemes . TRUST_ALL :
return llmProvider switch
{
LLMProviders . SELF_HOSTED = > ConfidenceLevel . HIGH ,
_ = > ConfidenceLevel . MEDIUM ,
} ;
2024-09-11 21:08:02 +00:00
case ConfidenceSchemes . TRUST_USA_EUROPE :
2024-09-13 19:50:00 +00:00
return llmProvider switch
2024-09-11 21:08:02 +00:00
{
2024-09-13 19:50:00 +00:00
LLMProviders . SELF_HOSTED = > ConfidenceLevel . HIGH ,
2025-02-27 11:43:19 +00:00
LLMProviders . DEEP_SEEK = > ConfidenceLevel . LOW ,
2026-08-12 17:02:06 +00:00
LLMProviders . ALIBABA_CLOUD = > ConfidenceLevel . LOW ,
2024-09-11 21:08:02 +00:00
2026-08-12 17:02:06 +00:00
_ = > ConfidenceLevel . MEDIUM ,
2024-09-11 21:08:02 +00:00
} ;
case ConfidenceSchemes . TRUST_USA :
2024-09-13 19:50:00 +00:00
return llmProvider switch
2024-09-11 21:08:02 +00:00
{
2024-09-13 19:50:00 +00:00
LLMProviders . SELF_HOSTED = > ConfidenceLevel . HIGH ,
LLMProviders . MISTRAL = > ConfidenceLevel . LOW ,
2025-02-27 11:43:19 +00:00
LLMProviders . HELMHOLTZ = > ConfidenceLevel . LOW ,
LLMProviders . GWDG = > ConfidenceLevel . LOW ,
2026-08-12 17:02:06 +00:00
LLMProviders . HETZNER = > ConfidenceLevel . LOW ,
2025-02-27 11:43:19 +00:00
LLMProviders . DEEP_SEEK = > ConfidenceLevel . LOW ,
2026-08-12 17:02:06 +00:00
LLMProviders . ALIBABA_CLOUD = > ConfidenceLevel . LOW ,
2024-09-11 21:08:02 +00:00
_ = > ConfidenceLevel . MEDIUM ,
} ;
case ConfidenceSchemes . TRUST_EUROPE :
2024-09-13 19:50:00 +00:00
return llmProvider switch
2024-09-11 21:08:02 +00:00
{
2024-09-13 19:50:00 +00:00
LLMProviders . SELF_HOSTED = > ConfidenceLevel . HIGH ,
LLMProviders . MISTRAL = > ConfidenceLevel . MEDIUM ,
2025-02-27 11:43:19 +00:00
LLMProviders . HELMHOLTZ = > ConfidenceLevel . MEDIUM ,
LLMProviders . GWDG = > ConfidenceLevel . MEDIUM ,
2026-08-12 17:02:06 +00:00
LLMProviders . HETZNER = > ConfidenceLevel . MEDIUM ,
2025-02-27 11:43:19 +00:00
_ = > ConfidenceLevel . LOW ,
} ;
case ConfidenceSchemes . TRUST_ASIA :
return llmProvider switch
{
LLMProviders . SELF_HOSTED = > ConfidenceLevel . HIGH ,
LLMProviders . DEEP_SEEK = > ConfidenceLevel . MEDIUM ,
2026-08-12 17:02:06 +00:00
LLMProviders . ALIBABA_CLOUD = > ConfidenceLevel . MEDIUM ,
2024-09-11 21:08:02 +00:00
_ = > ConfidenceLevel . LOW ,
} ;
case ConfidenceSchemes . LOCAL_TRUST_ONLY :
2024-09-13 19:50:00 +00:00
return llmProvider switch
2024-09-11 21:08:02 +00:00
{
2024-09-13 19:50:00 +00:00
LLMProviders . SELF_HOSTED = > ConfidenceLevel . HIGH ,
2024-09-11 21:08:02 +00:00
_ = > ConfidenceLevel . VERY_LOW ,
} ;
case ConfidenceSchemes . CUSTOM :
2026-06-21 09:52:02 +00:00
return this . ConfigurationData . Confidence . CustomConfidenceScheme . GetValueOrDefault ( llmProvider , ConfidenceLevel . UNKNOWN ) ;
2024-09-11 21:08:02 +00:00
default :
return ConfidenceLevel . UNKNOWN ;
}
}
2025-06-27 20:52:34 +00:00
2025-08-09 17:29:43 +00:00
public static string ToSettingName < TIn , TOut > ( Expression < Func < TIn , TOut > > propertyExpression )
2025-06-27 20:52:34 +00:00
{
MemberExpression ? memberExpr ;
// Handle the case where the expression is a unary expression (e.g., when using Convert):
if ( propertyExpression . Body is UnaryExpression { NodeType : ExpressionType . Convert } unaryExpr )
memberExpr = unaryExpr . Operand as MemberExpression ;
else
memberExpr = propertyExpression . Body as MemberExpression ;
if ( memberExpr is null )
throw new ArgumentException ( "Expression must be a property access" , nameof ( propertyExpression ) ) ;
// Return the full name of the property, including the class name:
2025-08-09 17:29:43 +00:00
return $"{typeof(TIn).Name}.{memberExpr.Member.Name}" ;
2025-06-27 20:52:34 +00:00
}
2026-06-21 13:16:37 +00:00
}