diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs
index b6b6c19734..cf93f10796 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs
@@ -163,28 +163,143 @@ namespace AdvancedPaste.Settings
return false;
}
- if (settings.Properties.IsAIEnabled || !LegacyOpenAIKeyExists())
+ var properties = settings.Properties;
+ var configuration = properties.PasteAIConfiguration;
+
+ if (configuration is null)
+ {
+ configuration = new PasteAIConfiguration();
+ properties.PasteAIConfiguration = configuration;
+ }
+
+ bool hasLegacyProviders = configuration.LegacyProviderConfigurations is { Count: > 0 };
+ bool legacyAdvancedAIConsumed = properties.TryConsumeLegacyAdvancedAIEnabled(out var advancedFlag);
+ bool legacyAdvancedAIEnabled = legacyAdvancedAIConsumed && advancedFlag;
+ PasswordCredential legacyCredential = TryGetLegacyOpenAICredential();
+
+ if (!hasLegacyProviders && legacyCredential is null && !legacyAdvancedAIConsumed)
{
return false;
}
- settings.Properties.IsAIEnabled = true;
- return true;
+ bool configurationUpdated = false;
+
+ if (hasLegacyProviders)
+ {
+ configurationUpdated |= AdvancedPasteMigrationHelper.MigrateLegacyProviderConfigurations(configuration);
+ }
+
+ PasteAIProviderDefinition openAIProvider = null;
+ if (legacyCredential is not null || hasLegacyProviders || legacyAdvancedAIConsumed)
+ {
+ var ensureResult = AdvancedPasteMigrationHelper.EnsureOpenAIProvider(configuration);
+ openAIProvider = ensureResult.Provider;
+ configurationUpdated |= ensureResult.Updated;
+ }
+
+ if (legacyAdvancedAIConsumed && openAIProvider is not null && openAIProvider.EnableAdvancedAI != legacyAdvancedAIEnabled)
+ {
+ openAIProvider.EnableAdvancedAI = legacyAdvancedAIEnabled;
+ configurationUpdated = true;
+ }
+
+ if (legacyCredential is not null && openAIProvider is not null)
+ {
+ StoreMigratedOpenAICredential(openAIProvider.Id, openAIProvider.ServiceType, legacyCredential.Password);
+ RemoveLegacyOpenAICredential();
+ }
+
+ bool enabledUpdated = false;
+ if (!properties.IsAIEnabled && legacyCredential is not null)
+ {
+ properties.IsAIEnabled = true;
+ enabledUpdated = true;
+ }
+
+ return configurationUpdated || enabledUpdated || legacyAdvancedAIConsumed;
}
- private static bool LegacyOpenAIKeyExists()
+ private static PasswordCredential TryGetLegacyOpenAICredential()
{
try
{
PasswordVault vault = new();
- return vault.Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey") is not null;
+ var credential = vault.Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey");
+ credential?.RetrievePassword();
+ return credential;
}
catch (Exception)
{
- return false;
+ return null;
}
}
+ private static void RemoveLegacyOpenAICredential()
+ {
+ try
+ {
+ PasswordVault vault = new();
+ TryRemoveCredential(vault, "https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey");
+ }
+ catch (Exception)
+ {
+ }
+ }
+
+ private static void StoreMigratedOpenAICredential(string providerId, string serviceType, string password)
+ {
+ if (string.IsNullOrWhiteSpace(password))
+ {
+ return;
+ }
+
+ try
+ {
+ var serviceKind = serviceType.ToAIServiceType();
+ if (serviceKind != AIServiceType.OpenAI)
+ {
+ return;
+ }
+
+ string resource = "https://platform.openai.com/api-keys";
+ string username = $"PowerToys_AdvancedPaste_PasteAI_openai_{NormalizeProviderIdentifier(providerId)}";
+
+ PasswordVault vault = new();
+ TryRemoveCredential(vault, resource, username);
+
+ PasswordCredential credential = new(resource, username, password);
+ vault.Add(credential);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Failed to migrate legacy OpenAI credential", ex);
+ }
+ }
+
+ private static void TryRemoveCredential(PasswordVault vault, string credentialResource, string credentialUserName)
+ {
+ try
+ {
+ PasswordCredential existingCred = vault.Retrieve(credentialResource, credentialUserName);
+ vault.Remove(existingCred);
+ }
+ catch (Exception)
+ {
+ // Credential doesn't exist, which is fine
+ }
+ }
+
+ private static string NormalizeProviderIdentifier(string providerId)
+ {
+ if (string.IsNullOrWhiteSpace(providerId))
+ {
+ return "default";
+ }
+
+ var filtered = new string(providerId.Where(char.IsLetterOrDigit).ToArray());
+ return string.IsNullOrWhiteSpace(filtered) ? "default" : filtered.ToLowerInvariant();
+ }
+
public async Task SetActiveAIProviderAsync(string providerId)
{
if (string.IsNullOrWhiteSpace(providerId))
diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteMigrationHelper.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteMigrationHelper.cs
new file mode 100644
index 0000000000..8a606ad83a
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteMigrationHelper.cs
@@ -0,0 +1,205 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.ObjectModel;
+using System.Linq;
+
+namespace Microsoft.PowerToys.Settings.UI.Library
+{
+ ///
+ /// Helper methods for migrating legacy Advanced Paste settings to the updated schema.
+ ///
+ public static class AdvancedPasteMigrationHelper
+ {
+ ///
+ /// Moves legacy provider configuration snapshots into the strongly-typed providers collection.
+ ///
+ /// The configuration instance to migrate.
+ /// True if the configuration was modified.
+ public static bool MigrateLegacyProviderConfigurations(PasteAIConfiguration configuration)
+ {
+ if (configuration is null)
+ {
+ return false;
+ }
+
+ configuration.Providers ??= new ObservableCollection();
+
+ bool configurationUpdated = false;
+
+ if (configuration.LegacyProviderConfigurations is { Count: > 0 })
+ {
+ foreach (var entry in configuration.LegacyProviderConfigurations)
+ {
+ var result = EnsureProvider(configuration, entry.Key, entry.Value);
+ configurationUpdated |= result.Updated;
+ }
+
+ configuration.LegacyProviderConfigurations = null;
+ }
+
+ configurationUpdated |= EnsureActiveProviderIsValid(configuration);
+
+ return configurationUpdated;
+ }
+
+ ///
+ /// Ensures an OpenAI provider exists in the configuration, creating one if necessary.
+ ///
+ /// The configuration instance.
+ /// The ensured provider and a flag indicating whether changes were made.
+ public static (PasteAIProviderDefinition Provider, bool Updated) EnsureOpenAIProvider(PasteAIConfiguration configuration)
+ {
+ return EnsureProvider(configuration, AIServiceType.OpenAI.ToConfigurationString(), null);
+ }
+
+ ///
+ /// Ensures a provider for the supplied service type exists, optionally applying a legacy snapshot.
+ ///
+ /// The configuration instance.
+ /// The persisted service type key.
+ /// An optional snapshot containing legacy values.
+ /// The ensured provider and whether the configuration was updated.
+ public static (PasteAIProviderDefinition Provider, bool Updated) EnsureProvider(PasteAIConfiguration configuration, string serviceTypeKey, AIProviderConfigurationSnapshot snapshot)
+ {
+ if (configuration is null)
+ {
+ return (null, false);
+ }
+
+ configuration.Providers ??= new ObservableCollection();
+
+ var normalizedServiceType = NormalizeServiceType(serviceTypeKey);
+ var existingProvider = configuration.Providers.FirstOrDefault(provider => string.Equals(provider.ServiceType, normalizedServiceType, StringComparison.OrdinalIgnoreCase));
+ bool configurationUpdated = false;
+
+ if (existingProvider is null)
+ {
+ existingProvider = CreateProvider(normalizedServiceType, snapshot);
+ configuration.Providers.Add(existingProvider);
+ configurationUpdated = true;
+ }
+ else if (snapshot is not null)
+ {
+ configurationUpdated |= ApplySnapshot(existingProvider, snapshot);
+ }
+
+ configurationUpdated |= EnsureActiveProviderIsValid(configuration, existingProvider);
+
+ return (existingProvider, configurationUpdated);
+ }
+
+ private static string NormalizeServiceType(string serviceTypeKey)
+ {
+ var serviceType = serviceTypeKey.ToAIServiceType();
+ return serviceType.ToConfigurationString();
+ }
+
+ private static PasteAIProviderDefinition CreateProvider(string serviceTypeKey, AIProviderConfigurationSnapshot snapshot)
+ {
+ var serviceType = serviceTypeKey.ToAIServiceType();
+ var metadata = AIServiceTypeRegistry.GetMetadata(serviceType);
+ var provider = new PasteAIProviderDefinition
+ {
+ ServiceType = serviceTypeKey,
+ ModelName = !string.IsNullOrWhiteSpace(snapshot?.ModelName) ? snapshot.ModelName : PasteAIProviderDefaults.GetDefaultModelName(serviceType),
+ EndpointUrl = snapshot?.EndpointUrl ?? string.Empty,
+ ApiVersion = snapshot?.ApiVersion ?? string.Empty,
+ DeploymentName = snapshot?.DeploymentName ?? string.Empty,
+ ModelPath = snapshot?.ModelPath ?? string.Empty,
+ SystemPrompt = snapshot?.SystemPrompt ?? string.Empty,
+ ModerationEnabled = snapshot?.ModerationEnabled ?? true,
+ IsLocalModel = metadata.IsLocalModel,
+ };
+
+ return provider;
+ }
+
+ private static bool ApplySnapshot(PasteAIProviderDefinition provider, AIProviderConfigurationSnapshot snapshot)
+ {
+ bool updated = false;
+
+ if (!string.IsNullOrWhiteSpace(snapshot.ModelName) && !string.Equals(provider.ModelName, snapshot.ModelName, StringComparison.Ordinal))
+ {
+ provider.ModelName = snapshot.ModelName;
+ updated = true;
+ }
+
+ if (!string.IsNullOrWhiteSpace(snapshot.EndpointUrl) && !string.Equals(provider.EndpointUrl, snapshot.EndpointUrl, StringComparison.Ordinal))
+ {
+ provider.EndpointUrl = snapshot.EndpointUrl;
+ updated = true;
+ }
+
+ if (!string.IsNullOrWhiteSpace(snapshot.ApiVersion) && !string.Equals(provider.ApiVersion, snapshot.ApiVersion, StringComparison.Ordinal))
+ {
+ provider.ApiVersion = snapshot.ApiVersion;
+ updated = true;
+ }
+
+ if (!string.IsNullOrWhiteSpace(snapshot.DeploymentName) && !string.Equals(provider.DeploymentName, snapshot.DeploymentName, StringComparison.Ordinal))
+ {
+ provider.DeploymentName = snapshot.DeploymentName;
+ updated = true;
+ }
+
+ if (!string.IsNullOrWhiteSpace(snapshot.ModelPath) && !string.Equals(provider.ModelPath, snapshot.ModelPath, StringComparison.Ordinal))
+ {
+ provider.ModelPath = snapshot.ModelPath;
+ updated = true;
+ }
+
+ if (!string.IsNullOrWhiteSpace(snapshot.SystemPrompt) && !string.Equals(provider.SystemPrompt, snapshot.SystemPrompt, StringComparison.Ordinal))
+ {
+ provider.SystemPrompt = snapshot.SystemPrompt;
+ updated = true;
+ }
+
+ if (provider.ModerationEnabled != snapshot.ModerationEnabled)
+ {
+ provider.ModerationEnabled = snapshot.ModerationEnabled;
+ updated = true;
+ }
+
+ return updated;
+ }
+
+ private static bool EnsureActiveProviderIsValid(PasteAIConfiguration configuration, PasteAIProviderDefinition preferredProvider = null)
+ {
+ if (configuration?.Providers is null || configuration.Providers.Count == 0)
+ {
+ if (!string.IsNullOrWhiteSpace(configuration?.ActiveProviderId))
+ {
+ configuration.ActiveProviderId = string.Empty;
+ return true;
+ }
+
+ return false;
+ }
+
+ bool updated = false;
+
+ var activeProvider = configuration.Providers.FirstOrDefault(provider => string.Equals(provider.Id, configuration.ActiveProviderId, StringComparison.OrdinalIgnoreCase));
+ if (activeProvider is null)
+ {
+ activeProvider = preferredProvider ?? configuration.Providers.First();
+ configuration.ActiveProviderId = activeProvider.Id;
+ updated = true;
+ }
+
+ foreach (var provider in configuration.Providers)
+ {
+ bool shouldBeActive = string.Equals(provider.Id, configuration.ActiveProviderId, StringComparison.OrdinalIgnoreCase);
+ if (provider.IsActive != shouldBeActive)
+ {
+ provider.IsActive = shouldBeActive;
+ updated = true;
+ }
+ }
+
+ return updated;
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteProperties.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteProperties.cs
index 200e5e459d..3c377900f8 100644
--- a/src/settings-ui/Settings.UI.Library/AdvancedPasteProperties.cs
+++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteProperties.cs
@@ -52,10 +52,68 @@ namespace Microsoft.PowerToys.Settings.UI.Library
_extensionData.Remove("IsOpenAIEnabled");
}
+
+ if (_extensionData != null && _extensionData.TryGetValue("IsAdvancedAIEnabled", out var legacyAdvancedElement))
+ {
+ bool? legacyValue = legacyAdvancedElement.ValueKind switch
+ {
+ JsonValueKind.True => true,
+ JsonValueKind.False => false,
+ JsonValueKind.Object when legacyAdvancedElement.TryGetProperty("value", out var advancedValueElement) => advancedValueElement.ValueKind switch
+ {
+ JsonValueKind.True => true,
+ JsonValueKind.False => false,
+ _ => null,
+ },
+ _ => null,
+ };
+
+ if (legacyValue.HasValue)
+ {
+ LegacyAdvancedAIEnabled = legacyValue.Value;
+ }
+
+ _extensionData.Remove("IsAdvancedAIEnabled");
+ }
}
}
private Dictionary _extensionData;
+ private bool? _legacyAdvancedAIEnabled;
+
+ [JsonPropertyName("IsAdvancedAIEnabled")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public BoolProperty LegacyAdvancedAIEnabledProperty
+ {
+ get => null;
+ set
+ {
+ if (value is not null)
+ {
+ LegacyAdvancedAIEnabled = value.Value;
+ }
+ }
+ }
+
+ [JsonIgnore]
+ public bool? LegacyAdvancedAIEnabled
+ {
+ get => _legacyAdvancedAIEnabled;
+ private set => _legacyAdvancedAIEnabled = value;
+ }
+
+ public bool TryConsumeLegacyAdvancedAIEnabled(out bool value)
+ {
+ if (_legacyAdvancedAIEnabled is bool flag)
+ {
+ value = flag;
+ _legacyAdvancedAIEnabled = null;
+ return true;
+ }
+
+ value = default;
+ return false;
+ }
[JsonConverter(typeof(BoolPropertyJsonConverter))]
public bool ShowCustomPreview { get; set; }
diff --git a/src/settings-ui/Settings.UI.Library/PasteAIProviderDefaults.cs b/src/settings-ui/Settings.UI.Library/PasteAIProviderDefaults.cs
new file mode 100644
index 0000000000..1ccfa753fa
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/PasteAIProviderDefaults.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Microsoft.PowerToys.Settings.UI.Library
+{
+ ///
+ /// Provides default values for Paste AI provider definitions.
+ ///
+ public static class PasteAIProviderDefaults
+ {
+ ///
+ /// Gets the default model name for a given AI service type.
+ ///
+ public static string GetDefaultModelName(AIServiceType serviceType)
+ {
+ return serviceType switch
+ {
+ AIServiceType.OpenAI => "gpt-4o",
+ AIServiceType.AzureOpenAI => "gpt-4o",
+ AIServiceType.Mistral => "mistral-large-latest",
+ AIServiceType.Google => "gemini-1.5-pro",
+ AIServiceType.AzureAIInference => "gpt-4o-mini",
+ AIServiceType.Ollama => "llama3",
+ _ => string.Empty,
+ };
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml
index 8d6b0afa68..457615bfe5 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml
@@ -499,7 +499,7 @@
MinWidth="200"
HorizontalAlignment="Stretch"
Header="Model name"
- PlaceholderText="gpt-4"
+ PlaceholderText="gpt-4o"
Text="{x:Bind ViewModel.PasteAIProviderDraft.ModelName, Mode=TwoWay}" />
0 };
+ PasswordCredential legacyCredential = TryGetLegacyOpenAICredential();
+
+ if (!hasLegacyProviders && legacyCredential is null && !legacyAdvancedAIConsumed)
{
return;
}
- _advancedPasteSettings.Properties.IsAIEnabled = true;
- SaveAndNotifySettings();
- OnPropertyChanged(nameof(IsAIEnabled));
+ bool configurationUpdated = false;
+
+ if (hasLegacyProviders)
+ {
+ configurationUpdated |= AdvancedPasteMigrationHelper.MigrateLegacyProviderConfigurations(configuration);
+ }
+
+ PasteAIProviderDefinition openAIProvider = null;
+ if (legacyCredential is not null || hasLegacyProviders || legacyAdvancedAIConsumed)
+ {
+ var ensureResult = AdvancedPasteMigrationHelper.EnsureOpenAIProvider(configuration);
+ openAIProvider = ensureResult.Provider;
+ configurationUpdated |= ensureResult.Updated;
+ }
+
+ if (legacyAdvancedAIConsumed && openAIProvider is not null && openAIProvider.EnableAdvancedAI != legacyAdvancedAIEnabled)
+ {
+ openAIProvider.EnableAdvancedAI = legacyAdvancedAIEnabled;
+ configurationUpdated = true;
+ }
+
+ if (legacyCredential is not null && openAIProvider is not null)
+ {
+ SavePasteAIApiKey(openAIProvider.Id, openAIProvider.ServiceType, legacyCredential.Password);
+ RemoveLegacyOpenAICredential();
+ }
+
+ bool enabledChanged = false;
+ if (!properties.IsAIEnabled && legacyCredential is not null)
+ {
+ properties.IsAIEnabled = true;
+ enabledChanged = true;
+ }
+
+ bool shouldPersist = configurationUpdated || enabledChanged || legacyAdvancedAIConsumed;
+
+ if (shouldPersist)
+ {
+ SaveAndNotifySettings();
+
+ if (configurationUpdated)
+ {
+ OnPropertyChanged(nameof(PasteAIConfiguration));
+ }
+
+ if (enabledChanged)
+ {
+ OnPropertyChanged(nameof(IsAIEnabled));
+ }
+ }
}
public bool IsEnabled
@@ -229,34 +300,30 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
public bool IsAIEnabled => _advancedPasteSettings.Properties.IsAIEnabled && !IsOnlineAIModelsDisallowedByGPO;
- private bool LegacyOpenAIKeyExists()
+ private PasswordCredential TryGetLegacyOpenAICredential()
{
try
{
PasswordVault vault = new();
-
- // return vault.Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey") is not null;
- var legacyOpenAIKey = vault.Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey");
- if (legacyOpenAIKey != null)
- {
- string credentialResource = GetAICredentialResource("OpenAI");
- var targetProvider = PasteAIConfiguration?.ActiveProvider ?? PasteAIConfiguration?.Providers?.FirstOrDefault();
- string providerId = targetProvider?.Id ?? string.Empty;
- string serviceType = targetProvider?.ServiceType ?? "OpenAI";
- string credentialUserName = GetPasteAICredentialUserName(providerId, serviceType);
- PasswordCredential cred = new(credentialResource, credentialUserName, legacyOpenAIKey.Password);
- vault.Add(cred);
-
- // delete old key
- TryRemoveCredential(vault, "https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey");
- return true;
- }
-
- return false;
+ var credential = vault.Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey");
+ credential?.RetrievePassword();
+ return credential;
+ }
+ catch (Exception)
+ {
+ return null;
+ }
+ }
+
+ private void RemoveLegacyOpenAICredential()
+ {
+ try
+ {
+ PasswordVault vault = new();
+ TryRemoveCredential(vault, "https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey");
}
catch (Exception)
{
- return false;
}
}
@@ -519,7 +586,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
var provider = new PasteAIProviderDefinition
{
ServiceType = persistedServiceType,
- ModelName = GetDefaultModelName(normalizedServiceType),
+ ModelName = PasteAIProviderDefaults.GetDefaultModelName(normalizedServiceType),
EndpointUrl = string.Empty,
ApiVersion = string.Empty,
DeploymentName = string.Empty,
@@ -559,20 +626,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
return serviceTypeKind;
}
- private static string GetDefaultModelName(AIServiceType serviceType)
- {
- return serviceType switch
- {
- AIServiceType.OpenAI => "gpt-4",
- AIServiceType.AzureOpenAI => "gpt-4",
- AIServiceType.Mistral => "mistral-large-latest",
- AIServiceType.Google => "gemini-2.5-pro",
- AIServiceType.AzureAIInference => "gpt-4o-mini",
- AIServiceType.Ollama => "llama3",
- _ => string.Empty,
- };
- }
-
public bool IsServiceTypeAllowedByGPO(AIServiceType serviceType)
{
var metadata = AIServiceTypeRegistry.GetMetadata(serviceType);
@@ -1352,7 +1405,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
pasteConfig.Providers ??= new ObservableCollection();
+
+ bool configurationUpdated = AdvancedPasteMigrationHelper.MigrateLegacyProviderConfigurations(pasteConfig);
+
SubscribeToPasteAIProviders(pasteConfig);
+
+ if (configurationUpdated)
+ {
+ SaveAndNotifySettings();
+ OnPropertyChanged(nameof(PasteAIConfiguration));
+ }
}
private static string RetrieveCredentialValue(string credentialResource, string credentialUserName)