using System.Diagnostics; using System.Linq.Expressions; using System.Text; using System.Text.Json; using AIStudio.Provider; using AIStudio.Settings.DataModel; using AIStudio.Tools.PluginSystem; using AIStudio.Tools.Services; // ReSharper disable NotAccessedPositionalProperty.Local namespace AIStudio.Settings; /// /// The settings manager. /// public sealed class SettingsManager { public readonly record struct ToolMinimumProviderConfidenceResolution(ConfidenceLevel ConfidenceLevel, string Source); private const string SETTINGS_FILENAME = "settings.json"; 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); internal static readonly JsonSerializerOptions JSON_OPTIONS = new() { WriteIndented = true, Converters = { new TolerantEnumConverter() }, }; /// /// From this duration on, storing the settings is reported as slow. /// /// /// The settings file is a few hundred kilobytes at most, so storing it usually takes some /// milliseconds. Far beyond that, something is blocking: on one machine, every store took 18 /// seconds, and each one held up the start of AI Studio. /// private static readonly TimeSpan SLOW_STORE_THRESHOLD = TimeSpan.FromSeconds(1); private readonly ILogger logger; private readonly RustService rustService; /// /// Lets only one operation at a time touch the settings files. /// /// /// Reading takes this as well as writing does, for two reasons. A read migrates and backs up /// what it found, so it writes the very files a store writes. And it re-evaluates whether /// writes are blocked at all, starting out by clearing that block: a store slipping through /// that moment would overwrite the settings the block exists to protect.

/// What this does not do is guard the settings themselves. It guards the files: what one store /// writes, the next one no longer has to fear. The configuration data behind them stays open to /// everybody, and a store serializes it while the rest of the app goes on editing it -- a list /// growing mid-serialization still throws. Whoever wants that answered needs one of their own; /// this lock is not it. ///
private readonly SemaphoreSlim settingsFileSemaphore = new(1, 1); /// /// The settings manager. /// public SettingsManager(ILogger logger, RustService rustService) { this.logger = logger; this.rustService = rustService; this.logger.LogInformation("Settings manager created."); } /// /// The directory where the configuration files are stored. /// public static string? ConfigDirectory { get; set; } /// /// The directory where the data files are stored. /// public static string? DataDirectory { get; set; } /// /// Whether the app is in dark mode. /// public bool IsDarkMode { get; set; } /// /// Ensures that the startup start-page redirect is evaluated at most once per app session. /// public bool StartupStartPageRedirectHandled { get; set; } /// /// Indicates that the initial settings load attempt has completed. /// public bool HasCompletedInitialSettingsLoad { get; private set; } /// /// Indicates why settings writes are blocked for the current session. /// public SettingsWriteBlockReason SettingsWriteBlockReason { get; private set; } = SettingsWriteBlockReason.NONE; /// /// Indicates that settings writes are blocked for the current session. /// public bool SettingsWriteBlocked => this.SettingsWriteBlockReason is not SettingsWriteBlockReason.NONE; /// /// The configuration data. /// public Data ConfigurationData { get; private set; } = new(); private bool IsSetUp => !string.IsNullOrWhiteSpace(ConfigDirectory) && !string.IsNullOrWhiteSpace(DataDirectory); /// /// Loads the settings from the file system. /// public async Task LoadSettings() { var settingsSnapshot = await this.TryReadSettingsSnapshot(); if (settingsSnapshot is not null) this.ConfigurationData = settingsSnapshot; this.HasCompletedInitialSettingsLoad = true; } /// /// Reads the settings from disk without mutating the current in-memory state. /// /// A (migrated) settings snapshot, or null if it could not be read. public async Task TryReadSettingsSnapshot() { await this.settingsFileSemaphore.WaitAsync(); try { return await this.ReadSettingsSnapshot(); } finally { this.settingsFileSemaphore.Release(); } } private async Task ReadSettingsSnapshot() { this.SettingsWriteBlockReason = SettingsWriteBlockReason.NONE; if(!this.IsSetUp) { this.logger.LogWarning("Cannot load settings, because the configuration is not set up yet."); return null; } var settingsPath = Path.Combine(ConfigDirectory!, SETTINGS_FILENAME); if(!File.Exists(settingsPath)) { this.logger.LogWarning("Cannot load settings, because the settings file does not exist."); return null; } var settingsVersion = await this.TryReadSettingsVersion(settingsPath); if(settingsVersion.FailureReason is not SettingsWriteBlockReason.NONE) { 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(); } 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(); } Data? settingsData; if(settingsVersion.Version < CURRENT_SETTINGS_VERSION) { settingsData = await this.TryReadCurrentVersionBackupSnapshot(); if(settingsData is not null) { 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; } 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); // Compatibility shim, remove after 2027-10-31, see KeepTheLegacyEncryptionExportFormat. // Settings of an older version always come from before the export format existed: settingsData.App.EnterpriseEncryptionExportFormat = EnterpriseEncryptionFormat.V1; this.PrepareLoadedSettings(settingsData); await this.StoreSettingsSnapshot(settingsData, settingsPath); await this.StoreCurrentVersionBackup(settingsData); return settingsData; } 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 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 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 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 TryDeserializeCurrentSettings(string settingsPath, string sourceDescription) { try { var settingsJson = await File.ReadAllTextAsync(settingsPath); var settingsData = JsonSerializer.Deserialize(settingsJson, 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); } KeepTheLegacyEncryptionExportFormat(settingsData, settingsJson); return new(settingsData, SettingsWriteBlockReason.NONE); } catch(Exception e) { this.logger.LogError(e, $"Failed to parse the {sourceDescription} '{settingsPath}'."); return new(null, SettingsWriteBlockReason.FILE_UNREADABLE); } } /// /// Lets an installation which existed before v26.10.1 keep exporting encrypted secrets in the /// format v1, which every version of AI Studio reads. /// /// /// Compatibility shim, remove after 2027-10-31, see /// documentation/compatibility-shims/2026-10-enterprise-encryption-export-format.md.

/// New installations export in the authenticated format v2. An existing installation, however, /// may belong to an administrator whose organization still runs an older version, which reads /// v1 only. Settings written before v26.10.1 lack the export format, and nothing else tells /// such an installation apart. Once the settings are stored, they hold the format, and this /// changes nothing anymore. ///
private static void KeepTheLegacyEncryptionExportFormat(Data settingsData, string settingsJson) { using var settingsDocument = JsonDocument.Parse(settingsJson); if(settingsDocument.RootElement.TryGetProperty(nameof(Data.App), out var appElement) && appElement.ValueKind is JsonValueKind.Object && appElement.TryGetProperty(nameof(DataApp.EnterpriseEncryptionExportFormat), out _)) return; settingsData.App.EnterpriseEncryptionExportFormat = EnterpriseEncryptionFormat.V1; } 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); } /// /// Stores the settings to the file system. /// public async Task StoreSettings() { await this.settingsFileSemaphore.WaitAsync(); try { if(!this.IsSetUp) { this.logger.LogWarning("Cannot store settings, because the configuration is not set up yet."); return; } if(this.SettingsWriteBlocked) { this.logger.LogWarning($"Cannot store settings, because settings writes are blocked. Reason: '{this.SettingsWriteBlockReason}'."); return; } // // Serializing and writing are measured on their own, because a slow store has two // different causes: a property which asks the file system or the network while it is // serialized, or a file system which is slow to write, e.g., because of a virus scanner // or a synchronized profile folder. Only the two durations tell them apart: // var serializingStartedAt = Stopwatch.GetTimestamp(); var settingsJson = JsonSerializer.Serialize(this.ConfigurationData, JSON_OPTIONS); var serializingDuration = Stopwatch.GetElapsedTime(serializingStartedAt); var writingStartedAt = Stopwatch.GetTimestamp(); var settingsPath = Path.Combine(ConfigDirectory!, SETTINGS_FILENAME); await this.StoreSerializedSettings(settingsJson, settingsPath); await this.StoreSerializedVersionBackup(this.ConfigurationData.Version, settingsJson); var writingDuration = Stopwatch.GetElapsedTime(writingStartedAt); if (serializingDuration + writingDuration >= SLOW_STORE_THRESHOLD) this.logger.LogWarning($"Storing the settings took {(serializingDuration + writingDuration).TotalMilliseconds:F0} ms, which is unusually long: serializing took {serializingDuration.TotalMilliseconds:F0} ms, writing took {writingDuration.TotalMilliseconds:F0} ms. The settings are {Encoding.UTF8.GetByteCount(settingsJson) / 1024} KB. A slow serialization points to a setting which reaches out to the file system or the network while it is serialized; slow writing points to the file system itself, e.g., a virus scanner or a synchronized profile folder."); } finally { this.settingsFileSemaphore.Release(); } } private static string GetBackupSettingsFilename(Version version) => $"settings.{version.ToString().ToLowerInvariant()}.json"; private static string GetBackupSettingsPath(Version version) => Path.Combine(ConfigDirectory!, GetBackupSettingsFilename(version)); private Task StoreCurrentVersionBackup(Data settingsData) => this.StoreSerializedVersionBackup(settingsData.Version, JsonSerializer.Serialize(settingsData, JSON_OPTIONS)); /// /// Writes the backup file from settings which were serialized already. /// /// /// The store hands the same JSON to this method and to the one writing the settings file, so /// that both files say the same thing. Serializing twice cannot promise that: the configuration /// data may well have changed in between, and the backup would then describe a state the /// settings file never had. /// /// The version the serialized settings carry. /// The serialized settings. private async Task StoreSerializedVersionBackup(Version settingsVersion, string settingsJson) { if(settingsVersion != CURRENT_SETTINGS_VERSION) { this.logger.LogWarning($"Skipping settings backup because the settings version '{settingsVersion}' is not the current version '{CURRENT_SETTINGS_VERSION}'."); return; } var backupSettingsPath = GetBackupSettingsPath(CURRENT_SETTINGS_VERSION); await this.StoreSerializedSettings(settingsJson, backupSettingsPath); this.logger.LogInformation($"Stored the settings backup file '{backupSettingsPath}'."); } private Task StoreSettingsSnapshot(Data settingsData, string settingsPath) => this.StoreSerializedSettings(JsonSerializer.Serialize(settingsData, JSON_OPTIONS), settingsPath); /// /// Writes settings which were serialized already to the given path. /// /// The serialized settings. /// The file to write them to. private async Task StoreSerializedSettings(string settingsJson, string settingsPath) { if(!Directory.Exists(ConfigDirectory)) { this.logger.LogInformation("Creating the configuration directory."); Directory.CreateDirectory(ConfigDirectory!); } // // 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; } this.logger.LogInformation($"Stored the settings to '{settingsPath}'."); } public void InjectSpellchecking(Dictionary attributes) => attributes["spellcheck"] = this.ConfigurationData.App.EnableSpellchecking ? "true" : "false"; public ConfidenceLevel GetMinimumConfidenceLevel(Tools.Components component) { var minimumLevel = ConfidenceLevel.NONE; var enforceGlobalMinimumConfidence = this.ConfigurationData.Confidence is { EnforceGlobalMinimumConfidence: true, GlobalMinimumConfidence: not ConfidenceLevel.NONE and not ConfidenceLevel.UNKNOWN }; if (enforceGlobalMinimumConfidence) minimumLevel = this.ConfigurationData.Confidence.GlobalMinimumConfidence; var componentMinimumLevel = component.MinimumConfidence(this); if (componentMinimumLevel > minimumLevel) minimumLevel = componentMinimumLevel; return minimumLevel; } /// /// Checks if the given plugin is enabled. /// /// /// 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. /// /// The plugin to check. /// True, when the plugin is enabled, false otherwise. public bool IsPluginEnabled(IPluginMetadata plugin) => plugin.Type is PluginType.CONFIGURATION || this.ConfigurationData.EnabledPlugins.Contains(plugin.Id) || PluginFactory.IsAssistantActivationEnforced(plugin.Id); /// /// Returns the active language plugin. /// /// The active language plugin. public async Task GetActiveLanguagePlugin() { switch (this.ConfigurationData.App.LanguageBehavior) { case LangBehavior.AUTO: var languageCode = await this.rustService.ReadUserLanguage(); var languagePlugins = PluginFactory.RunningPlugins.OfType().ToList(); if (!string.IsNullOrWhiteSpace(languageCode)) { 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; } } } this.logger.LogWarning($"The language plugin for the language '{languageCode}' (normalized='{languageCode}') is not available."); 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; 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; } 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]; } public Provider GetPreselectedProvider(Tools.Components component, string? currentProviderId = null, bool usePreselectionBeforeCurrentProvider = false) { var minimumLevel = this.GetMinimumConfidenceLevel(component); // When there is only one provider, and it has a confidence level that is high enough, we return it: if (this.ConfigurationData.Providers.Count == 1 && this.IsProviderUsable(this.ConfigurationData.Providers[0].UsedLLMProvider, minimumLevel)) return this.ConfigurationData.Providers[0]; // Is there a current provider with a sufficiently high confidence level? var currentProvider = Provider.NONE; if (currentProviderId is not null && !string.IsNullOrWhiteSpace(currentProviderId)) { var currentProviderProbe = this.ConfigurationData.Providers.FirstOrDefault(x => x.Id == currentProviderId); if (currentProviderProbe is not null && this.IsProviderUsable(currentProviderProbe.UsedLLMProvider, minimumLevel)) currentProvider = currentProviderProbe; } // Is there a component-preselected provider with a sufficiently high confidence level? var preselectedProvider = Provider.NONE; var preselectedProviderProbe = component.PreselectedProvider(this); if(preselectedProviderProbe != Provider.NONE && this.IsProviderUsable(preselectedProviderProbe.UsedLLMProvider, minimumLevel)) 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. // if(usePreselectionBeforeCurrentProvider && preselectedProvider != Provider.NONE) return preselectedProvider; // // Case: The current provider is available and has a confidence level that is // high enough. // if(currentProvider != Provider.NONE) 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. // if(preselectedProvider != Provider.NONE) return preselectedProvider; // When there is an app-wide preselected provider, and it has a confidence level that is high enough, we return it: return this.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.ConfigurationData.App.PreselectedProvider && this.IsProviderUsable(x.UsedLLMProvider, minimumLevel)) ?? Provider.NONE; } 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; 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) { 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 && this.IsProviderUsable(provider.UsedLLMProvider, minimumLevel); } /// /// Returns all configured providers without applying any confidence filtering. /// /// /// /// This method applies neither the global minimum confidence level (see /// with EnforceGlobalMinimumConfidence) nor any /// component-specific minimum. Even when the user enforces a global minimum of, say, /// , 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. /// /// /// 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 and leave out the providers the organization /// prohibited. This method returns those as well, so that the settings can mark them. /// /// /// 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. /// /// /// All configured providers, unfiltered. public IReadOnlyList GetAllProviders() => this.ConfigurationData.Providers .OrderBy(x => x.UsedLLMProvider.ToName(), StringComparer.OrdinalIgnoreCase) .ThenBy(x => x.InstanceName, StringComparer.OrdinalIgnoreCase) .ThenBy(x => x.Num) .ToList(); /// /// Returns the provider with the given id, without applying any confidence filtering. /// /// /// 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 or the organization prohibited /// it. Callers that intend to use the returned provider for an LLM request must check it /// themselves through IsProviderConfident or fall back to GetPreselectedProvider. /// /// The id of the provider to look up. /// The provider, or when no provider with that id exists. 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; } /// /// Determines the minimum confidence level a provider must have for the given component. /// /// The component for which the providers get filtered. /// An explicit minimum level, which is applied when it is higher than the component's minimum. /// The effective minimum confidence level. 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; } /// /// Checks whether the given provider satisfies the minimum confidence level of the given component. /// /// /// A provider the organization prohibited never does, see IsProviderUsable. /// /// The provider to check. /// The component for which the provider gets checked. /// An explicit minimum level, which is applied when it is higher than the component's minimum. /// True, when the provider may be used by the component, false otherwise. public bool IsProviderConfident(Provider provider, Tools.Components component, ConfidenceLevel explicitMinimum = ConfidenceLevel.UNKNOWN) { if (provider.UsedLLMProvider is LLMProviders.NONE) return false; return this.IsProviderUsable(provider.UsedLLMProvider, this.GetEffectiveMinimumConfidenceLevel(component, explicitMinimum)); } /// /// Checks whether the organization prohibited the given provider. /// /// /// A prohibition is a rule of its own, next to confidence and data-source security: no confidence /// level and no trust by configuration makes a prohibited provider usable. It also makes the provider /// untrusted, see GetConfiguredConfidenceLevel, but not the other way around. It names the operator /// of an API, so a hub or a self-hosted server which serves the same models stays allowed. /// /// The provider to check, for LLMs, embeddings, and transcriptions alike. /// True, when the organization prohibited the provider. NONE is never prohibited. public bool IsProviderProhibited(LLMProviders provider) => provider is not LLMProviders.NONE && this.ConfigurationData.App.ProhibitedProviders.Contains(provider); /// /// Checks whether the given provider may be used where the given minimum confidence level applies. /// /// /// The one check for every place which picks a provider: the organization did not prohibit it, and /// its confidence level reaches the minimum. A prohibited provider is untrusted, see /// GetConfiguredConfidenceLevel, but comparing the confidence level alone would still let it through /// wherever no minimum is enforced, since even untrusted reaches the minimum NONE. /// /// The provider to check, for LLMs, embeddings, and transcriptions alike. /// The minimum confidence level which applies. /// True, when the provider may be used, false otherwise. public bool IsProviderUsable(LLMProviders provider, ConfidenceLevel minimumLevel) => !this.IsProviderProhibited(provider) && provider.GetConfidence(this).Level >= minimumLevel; /// /// Returns the prohibited providers which have a level of their own in the custom confidence scheme. /// /// /// The prohibition outranks these levels, see GetConfiguredConfidenceLevel. /// /// The prohibited providers with a custom confidence level, in no particular order. public IEnumerable GetProhibitedProvidersWithCustomConfidenceLevel() => this.ConfigurationData.Confidence.CustomConfidenceScheme.Keys.Where(this.IsProviderProhibited); /// /// Returns all providers that satisfy the minimum confidence level of the given component. /// /// /// The providers the organization prohibited are left out, see IsProviderUsable. /// /// The component for which the providers get filtered. /// An explicit minimum level, which is applied when it is higher than the component's minimum. /// All providers the component may use, in the same order as GetAllProviders. public IEnumerable GetConfidentProviders(Tools.Components component, ConfidenceLevel explicitMinimum = ConfidenceLevel.UNKNOWN) { var minimumLevel = this.GetEffectiveMinimumConfidenceLevel(component, explicitMinimum); foreach (var provider in this.GetAllProviders()) if (provider.UsedLLMProvider is not LLMProviders.NONE && this.IsProviderUsable(provider.UsedLLMProvider, minimumLevel)) yield return provider; } /// /// Returns all configured embedding providers. /// /// /// 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. /// /// All configured embedding providers. public IReadOnlyList GetAllEmbeddingProviders() => this.ConfigurationData.EmbeddingProviders .OrderBy(x => x.UsedLLMProvider.ToName(), StringComparer.OrdinalIgnoreCase) .ThenBy(x => x.Name, StringComparer.OrdinalIgnoreCase) .ThenBy(x => x.Num) .ToList(); /// /// Returns the embedding provider with the given id, without applying any confidence filtering. /// /// /// 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. /// /// The id of the embedding provider to look up. /// The embedding provider, or EmbeddingProvider.NONE when no embedding provider with that id exists. 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; } /// /// Returns all configured transcription providers. /// /// /// 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. /// /// All configured transcription providers. public IReadOnlyList GetAllTranscriptionProviders() => this.ConfigurationData.TranscriptionProviders .OrderBy(x => x.UsedLLMProvider.ToName(), StringComparer.OrdinalIgnoreCase) .ThenBy(x => x.Name, StringComparer.OrdinalIgnoreCase) .ThenBy(x => x.Num) .ToList(); /// /// Returns the transcription provider with the given id, without applying any confidence filtering. /// /// /// 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. /// /// The id of the transcription provider to look up. /// The transcription provider, or TranscriptionProvider.NONE when no transcription provider with that id exists. 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; } public Profile GetPreselectedProfile(Tools.Components component) { var preselection = component.GetProfilePreselection(this); if (preselection.DoNotPreselectProfile) return Profile.NO_PROFILE; if (preselection.UseSpecificProfile) return this.GetProfileById(preselection.SpecificProfileId); var appPreselection = ProfilePreselection.FromStoredValue(this.ConfigurationData.App.PreselectedProfile); if (appPreselection.DoNotPreselectProfile || !appPreselection.UseSpecificProfile) return Profile.NO_PROFILE; return this.GetProfileById(appPreselection.SpecificProfileId); } public Profile GetAppPreselectedProfile() { var appPreselection = ProfilePreselection.FromStoredValue(this.ConfigurationData.App.PreselectedProfile); if (appPreselection.DoNotPreselectProfile || !appPreselection.UseSpecificProfile) return Profile.NO_PROFILE; return this.GetProfileById(appPreselection.SpecificProfileId); } public ChatTemplate GetPreselectedChatTemplate(Tools.Components component) { var preselection = component.PreselectedChatTemplate(this); if (preselection != ChatTemplate.NO_CHAT_TEMPLATE) return preselection; return this.GetChatTemplateById(this.ConfigurationData.App.PreselectedChatTemplate); } /// /// Returns the profile a chat template preselects for the chats started with it. /// /// /// Null whenever the template has nothing to say which could be used: it names no profile, it /// does not allow profiles, or the profile it names is gone. The chat then keeps the profile it /// would have without the template. Starting without a profile is an answer of its own, and it /// comes back as Profile.NO_PROFILE. /// /// The chat template of the chat about to start. /// The profile to start with, or null to keep the usual one. public Profile? GetChatTemplateProfile(ChatTemplate chatTemplate) { if (!chatTemplate.AllowProfileUsage || chatTemplate.ProfileId is null) return null; if (chatTemplate.StartsWithoutProfile()) return Profile.NO_PROFILE; var profile = this.GetProfileById(chatTemplate.ProfileId); return profile == Profile.NO_PROFILE ? null : profile; } /// /// Returns the profile a chat started with the given chat template begins with. /// /// /// Null when the template says nothing about profiles, so the chat keeps the profile it has. /// A template whose profile is gone starts its chats with the default profile of the chat /// settings, which may be none — not with whatever profile the previous chat happened to use, /// which would be a choice nobody made for this chat. /// /// The chat template of the chat about to start. /// The profile to start with, or null to keep the current one. public Profile? GetChatTemplateStartProfile(ChatTemplate chatTemplate) { if (!chatTemplate.AllowProfileUsage || chatTemplate.ProfileId is null) return null; return this.GetChatTemplateProfile(chatTemplate) ?? this.GetPreselectedProfile(Tools.Components.CHAT); } /// /// Returns the provider a chat template preselects for the chats started with it. /// /// /// The template is no way around the confidence levels: a provider which may not be used for /// chats comes back as Provider.NONE, just like one which is gone or one the /// template does not name at all. The chat then keeps the provider it would have without the /// template. /// /// The chat template of the chat about to start. /// The provider to start with, or Provider.NONE to keep the usual one. public Provider GetChatTemplateProvider(ChatTemplate chatTemplate) => this.GetChatTemplateProvider(chatTemplate.ProviderId); /// /// Returns the provider a chat template may preselect under the given ID. /// /// /// The one rule for the provider of a chat template: it exists and meets the confidence level /// required for chats. The chat, assistant chat launchers, the chat template dialog, and the /// import of a chat template all ask here, so that none of them accepts a provider another /// one would leave out. /// /// The ID the chat template names, or null when it names none. /// The provider, or Provider.NONE when the ID names no provider a chat may use. public Provider GetChatTemplateProvider(string? providerId) { if (string.IsNullOrWhiteSpace(providerId)) return Provider.NONE; var provider = this.GetProviderById(providerId); return provider != Provider.NONE && this.IsProviderConfident(provider, Tools.Components.CHAT) ? provider : Provider.NONE; } /// /// Returns the profile a chat begins with when it starts with the given chat template now. /// /// /// A template which names a profile decides, see GetChatTemplateStartProfile. One /// which says nothing keeps the current profile, with one exception: when that profile is the one /// the previous template preselected, the chat starts with the default profile of the chat /// settings instead. Keeping it would carry the choice of another template over into this chat. /// /// The chat template of the chat about to start. /// The chat template the current profile was chosen with; the same one when the template does not change. /// The profile the chat has right now. /// The profile to start with. public Profile ChooseChatTemplateProfile(ChatTemplate chatTemplate, ChatTemplate previousChatTemplate, Profile currentProfile) { if (this.GetChatTemplateStartProfile(chatTemplate) is { } templateProfile) return templateProfile; if (this.GetChatTemplateStartProfile(previousChatTemplate) is { } previousTemplateProfile && previousTemplateProfile.Id == currentProfile.Id) return this.GetPreselectedProfile(Tools.Components.CHAT); return currentProfile; } /// /// Returns the provider a chat begins with when it starts with the given chat template now. /// /// /// The same as for the profile, see ChooseChatTemplateProfile: a template which /// names a usable provider decides, and the provider of the previous template is not carried /// over. In its place comes the default provider of the chat settings. Without such a default, /// the current provider stays, so that the chat does not lose its provider altogether. /// /// The chat template of the chat about to start. /// The chat template the current provider was chosen with; the same one when the template does not change. /// The provider the chat has right now. /// The provider to start with. public Provider ChooseChatTemplateProvider(ChatTemplate chatTemplate, ChatTemplate previousChatTemplate, Provider currentProvider) { var templateProvider = this.GetChatTemplateProvider(chatTemplate); if (templateProvider != Provider.NONE) return templateProvider; var previousTemplateProvider = this.GetChatTemplateProvider(previousChatTemplate); if (previousTemplateProvider == Provider.NONE || previousTemplateProvider.Id != currentProvider.Id) return currentProvider; var defaultProvider = this.GetPreselectedProvider(Tools.Components.CHAT); return defaultProvider == Provider.NONE ? currentProvider : defaultProvider; } /// /// Returns the name of the workspace a chat template keeps the chats started with it in. /// /// /// Null when the template names none, and also while workspaces are switched off: a chat then /// belongs to no workspace, whatever its template says. /// /// The chat template of the chat about to start. /// The trimmed name of the workspace, or null to keep the chat where it would go anyway. public string? GetChatTemplateWorkspaceName(ChatTemplate chatTemplate) { if (string.IsNullOrWhiteSpace(chatTemplate.WorkspaceName) || this.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.DISABLE_WORKSPACES) return null; return chatTemplate.WorkspaceName.Trim(); } 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; } public bool AreToolsEnabled() => this.ConfigurationData.Tools.EnableTools; /// /// The document analysis is deliberately absent: there its policy names the tools, so the user /// has nothing to select. /// public bool IsToolSelectionVisible(AIStudio.Tools.Components component) => component switch { AIStudio.Tools.Components.CHAT or AIStudio.Tools.Components.CODING_ASSISTANT or AIStudio.Tools.Components.SLIDE_BUILDER_ASSISTANT => true, _ => this.ConfigurationData.Tools.VisibleToolSelectionComponents.Contains(component.ToString()), }; public void SetToolSelectionVisibility(AIStudio.Tools.Components component, bool isVisible) { if (component is AIStudio.Tools.Components.CHAT or AIStudio.Tools.Components.CODING_ASSISTANT or AIStudio.Tools.Components.SLIDE_BUILDER_ASSISTANT) return; var key = component.ToString(); if (isVisible) this.ConfigurationData.Tools.VisibleToolSelectionComponents.Add(key); else this.ConfigurationData.Tools.VisibleToolSelectionComponents.Remove(key); } /// /// Resolves which provider confidence a tool collection needs, and where that value came from. /// /// /// Which IDs stand for the collection and its default are passed in rather than looked up here. /// Both belong to the definitions, and the definitions live in the tool registry — which already /// depends on this class, so asking it back would be a circle.

/// When several of the IDs carry a level, the strictest one wins: an administrator who raised /// the level of one tool of a collection must not find it lowered by the entry of another one. /// For the same reason, an invalid managed level anywhere among them asks for HIGH. ///
/// The IDs which stand for the collection: its own, then those of its tools. /// The collection's own minimum, used when nothing overrides it. public ToolMinimumProviderConfidenceResolution GetMinimumProviderConfidenceResolution(IReadOnlyList settingsIds, ConfidenceLevel defaultLevel) { if (ManagedConfiguration.TryGet(x => x.Tools, x => x.MinimumProviderConfidenceByToolId, out var configMeta) && configMeta.IsLocked) { var managedValues = configMeta.GetValue(); ConfidenceLevel? strictestManagedLevel = null; foreach (var settingsId in settingsIds) { if (!managedValues.TryGetValue(settingsId, out var configuredManagedLevel)) continue; if (!TryParseMinimumProviderConfidence(configuredManagedLevel, out var managedConfidenceLevel)) { this.logger.LogError( "Managed minimum provider confidence '{ConfiguredLevel}' for tool '{ToolId}' is invalid. Requiring HIGH as a safe fallback.", configuredManagedLevel, settingsId); return new(ConfidenceLevel.HIGH, "invalid managed config; safe fallback"); } if (strictestManagedLevel is null || managedConfidenceLevel > strictestManagedLevel) strictestManagedLevel = managedConfidenceLevel; } if (strictestManagedLevel is { } managedLevel) return new(managedLevel, "managed config"); } ConfidenceLevel? strictestStoredLevel = null; foreach (var settingsId in settingsIds) { if (this.ConfigurationData.Tools.MinimumProviderConfidenceByToolId.TryGetValue(settingsId, out var configuredLevel) && TryParseMinimumProviderConfidence(configuredLevel, out var confidenceLevel) && (strictestStoredLevel is null || confidenceLevel > strictestStoredLevel)) { strictestStoredLevel = confidenceLevel; } } if (strictestStoredLevel is { } storedLevel) return new(storedLevel, "stored override"); return new(defaultLevel, "default fallback"); } private static bool TryParseMinimumProviderConfidence(string? configuredLevel, out ConfidenceLevel confidenceLevel) => Enum.TryParse(configuredLevel, true, out confidenceLevel) && Enum.IsDefined(confidenceLevel) && confidenceLevel is not ConfidenceLevel.UNKNOWN; /// /// Stores which provider confidence a tool collection needs. /// /// /// Stored under the ID of the collection. Entries under the IDs of its tools are removed, since /// the strictest entry wins and an older one would otherwise outvote the level just chosen. /// /// The IDs which stand for the collection: its own, then those of its tools. /// The level the user chose. /// The collection's own minimum. Choosing it again removes the override. public void SetMinimumProviderConfidence(IReadOnlyList settingsIds, ConfidenceLevel confidenceLevel, ConfidenceLevel defaultLevel) { foreach (var settingsId in settingsIds) this.ConfigurationData.Tools.MinimumProviderConfidenceByToolId.Remove(settingsId); if (confidenceLevel != defaultLevel) this.ConfigurationData.Tools.MinimumProviderConfidenceByToolId[settingsIds[0]] = confidenceLevel.ToString(); } /// /// Returns the confidence level of the given provider. /// /// /// A prohibition by the organization comes first: a prohibited provider is always untrusted, no matter /// what the confidence scheme says, and no matter which level the user or a configuration plugin chose /// for it in the custom scheme. Those levels stay stored, so they apply again once the prohibition ends. /// After that, the confidence scheme decides, and in the custom scheme, the level chosen for the provider. /// /// The provider, for LLMs, embeddings, and transcriptions alike. /// The confidence level of the provider. public ConfidenceLevel GetConfiguredConfidenceLevel(LLMProviders llmProvider) { if(llmProvider is LLMProviders.NONE) return ConfidenceLevel.NONE; if (this.IsProviderProhibited(llmProvider)) return ConfidenceLevel.UNTRUSTED; switch (this.ConfigurationData.Confidence.ConfidenceScheme) { case ConfidenceSchemes.TRUST_ALL: return llmProvider switch { LLMProviders.SELF_HOSTED => ConfidenceLevel.HIGH, _ => ConfidenceLevel.MEDIUM, }; case ConfidenceSchemes.TRUST_USA_EUROPE: return llmProvider switch { LLMProviders.SELF_HOSTED => ConfidenceLevel.HIGH, LLMProviders.DEEP_SEEK => ConfidenceLevel.LOW, LLMProviders.ALIBABA_CLOUD => ConfidenceLevel.LOW, _ => ConfidenceLevel.MEDIUM, }; case ConfidenceSchemes.TRUST_USA: return llmProvider switch { LLMProviders.SELF_HOSTED => ConfidenceLevel.HIGH, LLMProviders.MISTRAL => ConfidenceLevel.LOW, LLMProviders.HELMHOLTZ => ConfidenceLevel.LOW, LLMProviders.GWDG => ConfidenceLevel.LOW, LLMProviders.HETZNER => ConfidenceLevel.LOW, LLMProviders.IONOS => ConfidenceLevel.LOW, LLMProviders.DEEP_SEEK => ConfidenceLevel.LOW, LLMProviders.ALIBABA_CLOUD => ConfidenceLevel.LOW, _ => ConfidenceLevel.MEDIUM, }; case ConfidenceSchemes.TRUST_EUROPE: return llmProvider switch { LLMProviders.SELF_HOSTED => ConfidenceLevel.HIGH, LLMProviders.MISTRAL => ConfidenceLevel.MEDIUM, LLMProviders.HELMHOLTZ => ConfidenceLevel.MEDIUM, LLMProviders.GWDG => ConfidenceLevel.MEDIUM, LLMProviders.HETZNER => ConfidenceLevel.MEDIUM, LLMProviders.IONOS => ConfidenceLevel.MEDIUM, _ => ConfidenceLevel.LOW, }; case ConfidenceSchemes.TRUST_ASIA: return llmProvider switch { LLMProviders.SELF_HOSTED => ConfidenceLevel.HIGH, LLMProviders.DEEP_SEEK => ConfidenceLevel.MEDIUM, LLMProviders.ALIBABA_CLOUD => ConfidenceLevel.MEDIUM, _ => ConfidenceLevel.LOW, }; case ConfidenceSchemes.LOCAL_TRUST_ONLY: return llmProvider switch { LLMProviders.SELF_HOSTED => ConfidenceLevel.HIGH, _ => ConfidenceLevel.VERY_LOW, }; case ConfidenceSchemes.CUSTOM: return this.ConfigurationData.Confidence.CustomConfidenceScheme.GetValueOrDefault(llmProvider, ConfidenceLevel.UNKNOWN); default: return ConfidenceLevel.UNKNOWN; } } public static string ToSettingName(Expression> propertyExpression) { 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: return $"{typeof(TIn).Name}.{memberExpr.Member.Name}"; } }