diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs
index 2d331f7cd9..97f7c1b8f7 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs
@@ -116,6 +116,7 @@ namespace AdvancedPaste.Settings
CloseAfterLosingFocus = properties.CloseAfterLosingFocus;
AdvancedAIConfiguration = properties.AdvancedAIConfiguration ?? new AdvancedAIConfiguration();
PasteAIConfiguration = properties.PasteAIConfiguration ?? new PasteAIConfiguration();
+ PasteAIConfiguration.EnsureActiveProvider();
var sourceAdditionalActions = properties.AdditionalActions;
(PasteFormats Format, IAdvancedPasteAction[] Actions)[] additionalActionFormats =
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/CustomActionTransformService.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/CustomActionTransformService.cs
index 4d2f1ccdfd..ee2bf7d3cd 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/CustomActionTransformService.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/CustomActionTransformService.cs
@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
@@ -111,21 +112,23 @@ namespace AdvancedPaste.Services.CustomActions
private PasteAIConfig BuildProviderConfig(PasteAIConfiguration config)
{
config ??= new PasteAIConfiguration();
- var serviceType = NormalizeServiceType(config.ServiceTypeKind);
- var systemPrompt = string.IsNullOrWhiteSpace(config.SystemPrompt) ? DefaultSystemPrompt : config.SystemPrompt;
+ var provider = config.ActiveProvider ?? config.Providers?.FirstOrDefault() ?? new PasteAIProviderDefinition();
+ var serviceType = NormalizeServiceType(provider.ServiceTypeKind);
+ var systemPrompt = string.IsNullOrWhiteSpace(provider.SystemPrompt) ? DefaultSystemPrompt : provider.SystemPrompt;
var apiKey = AcquireApiKey(serviceType);
- var modelName = config.ModelName;
+ var modelName = provider.ModelName;
var providerConfig = new PasteAIConfig
{
ProviderType = serviceType,
ApiKey = apiKey,
Model = modelName,
- Endpoint = config.EndpointUrl,
- DeploymentName = config.DeploymentName,
- ModelPath = config.ModelPath,
+ Endpoint = provider.EndpointUrl,
+ DeploymentName = provider.DeploymentName,
+ LocalModelPath = provider.ModelPath,
+ ModelPath = provider.ModelPath,
SystemPrompt = systemPrompt,
- ModerationEnabled = config.ModerationEnabled,
+ ModerationEnabled = provider.ModerationEnabled,
};
return providerConfig;
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/EnhancedVaultCredentialsProvider.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/EnhancedVaultCredentialsProvider.cs
index 2094bb5572..5946b6de76 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Services/EnhancedVaultCredentialsProvider.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/EnhancedVaultCredentialsProvider.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using AdvancedPaste.Settings;
using Microsoft.PowerToys.Settings.UI.Library;
using Windows.Security.Credentials;
@@ -20,6 +21,8 @@ public sealed class EnhancedVaultCredentialsProvider : IAICredentialsProvider
{
public AIServiceType ServiceType { get; set; } = AIServiceType.Unknown;
+ public string ProviderId { get; set; } = string.Empty;
+
public (string Resource, string Username)? Entry { get; set; }
public string Key { get; set; } = string.Empty;
@@ -68,14 +71,17 @@ public sealed class EnhancedVaultCredentialsProvider : IAICredentialsProvider
private bool UpdateSlot(AICredentialScope scope, bool forceRefresh)
{
var slot = _slots[scope];
- var desiredServiceType = NormalizeServiceType(ResolveServiceType(scope));
+ var (serviceType, providerId) = ResolveCredentialTarget(scope);
+ var desiredServiceType = NormalizeServiceType(serviceType);
+ providerId ??= string.Empty;
var hasChanged = false;
- if (slot.ServiceType != desiredServiceType)
+ if (slot.ServiceType != desiredServiceType || !string.Equals(slot.ProviderId, providerId, StringComparison.Ordinal))
{
slot.ServiceType = desiredServiceType;
- slot.Entry = BuildCredentialEntry(desiredServiceType, scope);
+ slot.ProviderId = providerId;
+ slot.Entry = BuildCredentialEntry(desiredServiceType, providerId, scope);
forceRefresh = true;
hasChanged = true;
}
@@ -95,13 +101,13 @@ public sealed class EnhancedVaultCredentialsProvider : IAICredentialsProvider
return hasChanged;
}
- private AIServiceType ResolveServiceType(AICredentialScope scope)
+ private (AIServiceType ServiceType, string ProviderId) ResolveCredentialTarget(AICredentialScope scope)
{
return scope switch
{
- AICredentialScope.AdvancedAI => _userSettings.AdvancedAIConfiguration?.ServiceTypeKind ?? AIServiceType.OpenAI,
- AICredentialScope.PasteAI => _userSettings.PasteAIConfiguration?.ServiceTypeKind ?? AIServiceType.OpenAI,
- _ => AIServiceType.OpenAI,
+ AICredentialScope.AdvancedAI => (ResolveAdvancedAiServiceType(), string.Empty),
+ AICredentialScope.PasteAI => ResolvePasteAiServiceTarget(),
+ _ => (AIServiceType.OpenAI, string.Empty),
};
}
@@ -110,6 +116,22 @@ public sealed class EnhancedVaultCredentialsProvider : IAICredentialsProvider
return serviceType == AIServiceType.Unknown ? AIServiceType.OpenAI : serviceType;
}
+ private AIServiceType ResolveAdvancedAiServiceType()
+ {
+ return _userSettings.AdvancedAIConfiguration?.ServiceTypeKind ?? AIServiceType.OpenAI;
+ }
+
+ private (AIServiceType ServiceType, string ProviderId) ResolvePasteAiServiceTarget()
+ {
+ var provider = _userSettings.PasteAIConfiguration?.ActiveProvider;
+ if (provider is null)
+ {
+ return (AIServiceType.OpenAI, string.Empty);
+ }
+
+ return (provider.ServiceTypeKind, provider.Id ?? string.Empty);
+ }
+
private static string LoadKey((string Resource, string Username)? entry)
{
if (entry is null)
@@ -128,12 +150,12 @@ public sealed class EnhancedVaultCredentialsProvider : IAICredentialsProvider
}
}
- private static (string Resource, string Username)? BuildCredentialEntry(AIServiceType serviceType, AICredentialScope scope)
+ private static (string Resource, string Username)? BuildCredentialEntry(AIServiceType serviceType, string providerId, AICredentialScope scope)
{
return scope switch
{
AICredentialScope.AdvancedAI => GetAdvancedAiEntry(serviceType),
- AICredentialScope.PasteAI => GetPasteAiEntry(serviceType),
+ AICredentialScope.PasteAI => GetPasteAiEntry(serviceType, providerId),
_ => null,
};
}
@@ -155,20 +177,60 @@ public sealed class EnhancedVaultCredentialsProvider : IAICredentialsProvider
};
}
- private static (string Resource, string Username)? GetPasteAiEntry(AIServiceType serviceType)
+ private static (string Resource, string Username)? GetPasteAiEntry(AIServiceType serviceType, string providerId)
{
- return serviceType switch
+ string resource;
+ string serviceKey;
+
+ switch (serviceType)
{
- AIServiceType.OpenAI => ("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_PasteAI_OpenAI"),
- AIServiceType.AzureOpenAI => ("https://azure.microsoft.com/products/ai-services/openai-service", "PowerToys_AdvancedPaste_PasteAI_AzureOpenAI"),
- AIServiceType.AzureAIInference => ("https://azure.microsoft.com/products/ai-services/ai-inference", "PowerToys_AdvancedPaste_PasteAI_AzureAIInference"),
- AIServiceType.Mistral => ("https://console.mistral.ai/account/api-keys", "PowerToys_AdvancedPaste_PasteAI_Mistral"),
- AIServiceType.Google => ("https://ai.google.dev/", "PowerToys_AdvancedPaste_PasteAI_Google"),
- AIServiceType.HuggingFace => ("https://huggingface.co/settings/tokens", "PowerToys_AdvancedPaste_PasteAI_HuggingFace"),
- AIServiceType.Ollama => null,
- AIServiceType.Anthropic => null,
- AIServiceType.AmazonBedrock => null,
- _ => null,
- };
+ case AIServiceType.OpenAI:
+ resource = "https://platform.openai.com/api-keys";
+ serviceKey = "openai";
+ break;
+ case AIServiceType.AzureOpenAI:
+ resource = "https://azure.microsoft.com/products/ai-services/openai-service";
+ serviceKey = "azureopenai";
+ break;
+ case AIServiceType.AzureAIInference:
+ resource = "https://azure.microsoft.com/products/ai-services/ai-inference";
+ serviceKey = "azureaiinference";
+ break;
+ case AIServiceType.Mistral:
+ resource = "https://console.mistral.ai/account/api-keys";
+ serviceKey = "mistral";
+ break;
+ case AIServiceType.Google:
+ resource = "https://ai.google.dev/";
+ serviceKey = "google";
+ break;
+ case AIServiceType.HuggingFace:
+ resource = "https://huggingface.co/settings/tokens";
+ serviceKey = "huggingface";
+ break;
+ case AIServiceType.FoundryLocal:
+ case AIServiceType.ML:
+ case AIServiceType.Onnx:
+ case AIServiceType.Ollama:
+ case AIServiceType.Anthropic:
+ case AIServiceType.AmazonBedrock:
+ return null;
+ default:
+ return null;
+ }
+
+ string username = $"PowerToys_AdvancedPaste_PasteAI_{serviceKey}_{NormalizeProviderIdentifier(providerId)}";
+ return (resource, username);
+ }
+
+ 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();
}
}
diff --git a/src/settings-ui/Settings.UI.Library/AIServiceTypeExtensions.cs b/src/settings-ui/Settings.UI.Library/AIServiceTypeExtensions.cs
index 8982161c70..91f035d23a 100644
--- a/src/settings-ui/Settings.UI.Library/AIServiceTypeExtensions.cs
+++ b/src/settings-ui/Settings.UI.Library/AIServiceTypeExtensions.cs
@@ -26,7 +26,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
"azureopenai" or "azure" => AIServiceType.AzureOpenAI,
"onnx" => AIServiceType.Onnx,
"foundrylocal" or "foundry" or "fl" => AIServiceType.FoundryLocal,
- "ml" => AIServiceType.ML,
+ "ml" or "windowsml" or "winml" => AIServiceType.ML,
"mistral" => AIServiceType.Mistral,
"google" or "googleai" or "googlegemini" => AIServiceType.Google,
"huggingface" => AIServiceType.HuggingFace,
diff --git a/src/settings-ui/Settings.UI.Library/AIServiceTypeMetadata.cs b/src/settings-ui/Settings.UI.Library/AIServiceTypeMetadata.cs
new file mode 100644
index 0000000000..cb4cc15258
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/AIServiceTypeMetadata.cs
@@ -0,0 +1,26 @@
+// 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.Generic;
+using System.Linq;
+
+namespace Microsoft.PowerToys.Settings.UI.Library
+{
+ ///
+ /// Metadata information for an AI service type.
+ ///
+ public class AIServiceTypeMetadata
+ {
+ public AIServiceType ServiceType { get; init; }
+
+ public string DisplayName { get; init; }
+
+ public string IconPath { get; init; }
+
+ public bool IsOnlineService { get; init; }
+
+ public bool IsAvailableInUI { get; init; } = true;
+ }
+}
diff --git a/src/settings-ui/Settings.UI.Library/AIServiceTypeRegistry.cs b/src/settings-ui/Settings.UI.Library/AIServiceTypeRegistry.cs
new file mode 100644
index 0000000000..f87d03db7c
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/AIServiceTypeRegistry.cs
@@ -0,0 +1,172 @@
+// 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.Generic;
+using System.Linq;
+
+namespace Microsoft.PowerToys.Settings.UI.Library;
+
+///
+/// Centralized registry for AI service type metadata.
+///
+public static class AIServiceTypeRegistry
+{
+ private static readonly Dictionary MetadataMap = new()
+ {
+ [AIServiceType.OpenAI] = new AIServiceTypeMetadata
+ {
+ ServiceType = AIServiceType.OpenAI,
+ DisplayName = "OpenAI",
+ IconPath = "ms-appx:///Assets/Settings/Icons/Models/OpenAI.light.svg",
+ IsOnlineService = true,
+ },
+ [AIServiceType.AzureOpenAI] = new AIServiceTypeMetadata
+ {
+ ServiceType = AIServiceType.AzureOpenAI,
+ DisplayName = "Azure OpenAI",
+ IconPath = "ms-appx:///Assets/Settings/Icons/Models/AzureAI.svg",
+ IsOnlineService = true,
+ },
+ [AIServiceType.Mistral] = new AIServiceTypeMetadata
+ {
+ ServiceType = AIServiceType.Mistral,
+ DisplayName = "Mistral",
+ IconPath = "ms-appx:///Assets/Settings/Icons/Models/Mistral.svg",
+ IsOnlineService = true,
+ },
+ [AIServiceType.Google] = new AIServiceTypeMetadata
+ {
+ ServiceType = AIServiceType.Google,
+ DisplayName = "Google",
+ IconPath = "ms-appx:///Assets/Settings/Icons/Models/Gemini.svg",
+ IsOnlineService = true,
+ },
+ [AIServiceType.AzureAIInference] = new AIServiceTypeMetadata
+ {
+ ServiceType = AIServiceType.AzureAIInference,
+ DisplayName = "Azure AI Inference",
+ IconPath = "ms-appx:///Assets/Settings/Icons/Models/AzureAI.svg",
+ IsOnlineService = true,
+ },
+ [AIServiceType.Ollama] = new AIServiceTypeMetadata
+ {
+ ServiceType = AIServiceType.Ollama,
+ DisplayName = "Ollama",
+ IconPath = "ms-appx:///Assets/Settings/Icons/Models/Ollama.svg",
+ IsOnlineService = true,
+ },
+ [AIServiceType.Anthropic] = new AIServiceTypeMetadata
+ {
+ ServiceType = AIServiceType.Anthropic,
+ DisplayName = "Anthropic",
+ IconPath = "ms-appx:///Assets/Settings/Icons/Models/Anthropic.svg",
+ IsOnlineService = true,
+ },
+ [AIServiceType.AmazonBedrock] = new AIServiceTypeMetadata
+ {
+ ServiceType = AIServiceType.AmazonBedrock,
+ DisplayName = "Amazon Bedrock",
+ IconPath = "ms-appx:///Assets/Settings/Icons/Models/Bedrock.svg",
+ IsOnlineService = true,
+ },
+ [AIServiceType.FoundryLocal] = new AIServiceTypeMetadata
+ {
+ ServiceType = AIServiceType.FoundryLocal,
+ DisplayName = "Foundry Local",
+ IconPath = "ms-appx:///Assets/Settings/Icons/Models/FoundryLocal.svg",
+ IsOnlineService = false,
+ },
+ [AIServiceType.ML] = new AIServiceTypeMetadata
+ {
+ ServiceType = AIServiceType.ML,
+ DisplayName = "Windows ML",
+ IconPath = "ms-appx:///Assets/Settings/Icons/Models/WindowsML.svg",
+ IsOnlineService = false,
+ },
+ [AIServiceType.HuggingFace] = new AIServiceTypeMetadata
+ {
+ ServiceType = AIServiceType.HuggingFace,
+ DisplayName = "Hugging Face",
+ IconPath = "ms-appx:///Assets/Settings/Icons/Models/HuggingFace.svg",
+ IsOnlineService = true,
+ IsAvailableInUI = false, // Currently disabled in UI
+ },
+ [AIServiceType.Onnx] = new AIServiceTypeMetadata
+ {
+ ServiceType = AIServiceType.Onnx,
+ DisplayName = "ONNX",
+ IconPath = "ms-appx:///Assets/Settings/Icons/Models/Onnx.svg",
+ IsOnlineService = false,
+ IsAvailableInUI = false,
+ },
+ [AIServiceType.Unknown] = new AIServiceTypeMetadata
+ {
+ ServiceType = AIServiceType.Unknown,
+ DisplayName = "Unknown",
+ IconPath = "ms-appx:///Assets/Settings/Icons/Models/OpenAI.light.svg",
+ IsOnlineService = false,
+ IsAvailableInUI = false,
+ },
+ };
+
+ ///
+ /// Get metadata for a specific service type.
+ ///
+ public static AIServiceTypeMetadata GetMetadata(AIServiceType serviceType)
+ {
+ return MetadataMap.TryGetValue(serviceType, out var metadata)
+ ? metadata
+ : MetadataMap[AIServiceType.Unknown];
+ }
+
+ ///
+ /// Get metadata for a service type from its string representation.
+ ///
+ public static AIServiceTypeMetadata GetMetadata(string serviceType)
+ {
+ var type = serviceType.ToAIServiceType();
+ return GetMetadata(type);
+ }
+
+ ///
+ /// Get icon path for a service type.
+ ///
+ public static string GetIconPath(AIServiceType serviceType)
+ {
+ return GetMetadata(serviceType).IconPath;
+ }
+
+ ///
+ /// Get icon path for a service type from its string representation.
+ ///
+ public static string GetIconPath(string serviceType)
+ {
+ return GetMetadata(serviceType).IconPath;
+ }
+
+ ///
+ /// Get all service types available in the UI.
+ ///
+ public static IEnumerable GetAvailableServiceTypes()
+ {
+ return MetadataMap.Values.Where(m => m.IsAvailableInUI);
+ }
+
+ ///
+ /// Get all online service types available in the UI.
+ ///
+ public static IEnumerable GetOnlineServiceTypes()
+ {
+ return GetAvailableServiceTypes().Where(m => m.IsOnlineService);
+ }
+
+ ///
+ /// Get all local service types available in the UI.
+ ///
+ public static IEnumerable GetLocalServiceTypes()
+ {
+ return GetAvailableServiceTypes().Where(m => !m.IsOnlineService);
+ }
+}
diff --git a/src/settings-ui/Settings.UI.Library/PasteAIConfiguration.cs b/src/settings-ui/Settings.UI.Library/PasteAIConfiguration.cs
index 4e7ded0d22..6b4a0d1bef 100644
--- a/src/settings-ui/Settings.UI.Library/PasteAIConfiguration.cs
+++ b/src/settings-ui/Settings.UI.Library/PasteAIConfiguration.cs
@@ -4,7 +4,9 @@
using System;
using System.Collections.Generic;
+using System.Collections.ObjectModel;
using System.ComponentModel;
+using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -16,66 +18,33 @@ namespace Microsoft.PowerToys.Settings.UI.Library
///
public class PasteAIConfiguration : INotifyPropertyChanged
{
- private string _serviceType = "OpenAI";
- private string _modelName = "gpt-3.5-turbo";
- private string _endpointUrl = string.Empty;
- private string _apiVersion = string.Empty;
- private string _deploymentName = string.Empty;
- private string _modelPath = string.Empty;
+ private string _activeProviderId = string.Empty;
+ private ObservableCollection _providers = new();
private bool _useSharedCredentials = true;
- private string _systemPrompt = string.Empty;
- private bool _moderationEnabled = true;
- private Dictionary _providerConfigurations = new(StringComparer.OrdinalIgnoreCase);
+ private string _legacyServiceType = "OpenAI";
+ private string _legacyModelName = "gpt-3.5-turbo";
+ private string _legacyEndpointUrl = string.Empty;
+ private string _legacyApiVersion = string.Empty;
+ private string _legacyDeploymentName = string.Empty;
+ private string _legacyModelPath = string.Empty;
+ private string _legacySystemPrompt = string.Empty;
+ private bool _legacyModerationEnabled = true;
+ private Dictionary _legacyProviderConfigurations;
public event PropertyChangedEventHandler PropertyChanged;
- [JsonPropertyName("service-type")]
- public string ServiceType
+ [JsonPropertyName("active-provider-id")]
+ public string ActiveProviderId
{
- get => _serviceType;
- set => SetProperty(ref _serviceType, value);
+ get => _activeProviderId;
+ set => SetProperty(ref _activeProviderId, value ?? string.Empty);
}
- [JsonIgnore]
- public AIServiceType ServiceTypeKind
+ [JsonPropertyName("providers")]
+ public ObservableCollection Providers
{
- get => _serviceType.ToAIServiceType();
- set => ServiceType = value.ToConfigurationString();
- }
-
- [JsonPropertyName("model-name")]
- public string ModelName
- {
- get => _modelName;
- set => SetProperty(ref _modelName, value);
- }
-
- [JsonPropertyName("endpoint-url")]
- public string EndpointUrl
- {
- get => _endpointUrl;
- set => SetProperty(ref _endpointUrl, value);
- }
-
- [JsonPropertyName("api-version")]
- public string ApiVersion
- {
- get => _apiVersion;
- set => SetProperty(ref _apiVersion, value);
- }
-
- [JsonPropertyName("deployment-name")]
- public string DeploymentName
- {
- get => _deploymentName;
- set => SetProperty(ref _deploymentName, value);
- }
-
- [JsonPropertyName("model-path")]
- public string ModelPath
- {
- get => _modelPath;
- set => SetProperty(ref _modelPath, value);
+ get => _providers;
+ set => SetProperty(ref _providers, value ?? new ObservableCollection());
}
[JsonPropertyName("use-shared-credentials")]
@@ -85,49 +54,192 @@ namespace Microsoft.PowerToys.Settings.UI.Library
set => SetProperty(ref _useSharedCredentials, value);
}
- [JsonPropertyName("system-prompt")]
- public string SystemPrompt
+ // Legacy properties retained for migration. They will be cleared once converted to the new format.
+ [JsonPropertyName("service-type")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string LegacyServiceType
{
- get => _systemPrompt;
- set => SetProperty(ref _systemPrompt, value?.Trim() ?? string.Empty);
+ get => _legacyServiceType;
+ set => _legacyServiceType = value;
+ }
+
+ [JsonPropertyName("model-name")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string LegacyModelName
+ {
+ get => _legacyModelName;
+ set => _legacyModelName = value;
+ }
+
+ [JsonPropertyName("endpoint-url")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string LegacyEndpointUrl
+ {
+ get => _legacyEndpointUrl;
+ set => _legacyEndpointUrl = value;
+ }
+
+ [JsonPropertyName("api-version")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string LegacyApiVersion
+ {
+ get => _legacyApiVersion;
+ set => _legacyApiVersion = value;
+ }
+
+ [JsonPropertyName("deployment-name")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string LegacyDeploymentName
+ {
+ get => _legacyDeploymentName;
+ set => _legacyDeploymentName = value;
+ }
+
+ [JsonPropertyName("model-path")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string LegacyModelPath
+ {
+ get => _legacyModelPath;
+ set => _legacyModelPath = value;
+ }
+
+ [JsonPropertyName("system-prompt")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string LegacySystemPrompt
+ {
+ get => _legacySystemPrompt;
+ set => _legacySystemPrompt = value;
}
[JsonPropertyName("moderation-enabled")]
- public bool ModerationEnabled
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
+ public bool LegacyModerationEnabled
{
- get => _moderationEnabled;
- set => SetProperty(ref _moderationEnabled, value);
+ get => _legacyModerationEnabled;
+ set => _legacyModerationEnabled = value;
}
[JsonPropertyName("provider-configurations")]
- public Dictionary ProviderConfigurations
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public Dictionary LegacyProviderConfigurations
{
- get => _providerConfigurations;
- set => SetProperty(ref _providerConfigurations, value ?? new Dictionary(StringComparer.OrdinalIgnoreCase));
+ get => _legacyProviderConfigurations;
+ set => _legacyProviderConfigurations = value;
}
- public bool HasProviderConfiguration(string serviceType)
+ [JsonIgnore]
+ public PasteAIProviderDefinition ActiveProvider
{
- return _providerConfigurations.ContainsKey(NormalizeServiceType(serviceType));
- }
-
- public AIProviderConfigurationSnapshot GetOrCreateProviderConfiguration(string serviceType)
- {
- var key = NormalizeServiceType(serviceType);
- if (!_providerConfigurations.TryGetValue(key, out var snapshot))
+ get
{
- snapshot = new AIProviderConfigurationSnapshot();
- _providerConfigurations[key] = snapshot;
- OnPropertyChanged(nameof(ProviderConfigurations));
+ if (_providers is null || _providers.Count == 0)
+ {
+ return null;
+ }
+
+ if (!string.IsNullOrWhiteSpace(_activeProviderId))
+ {
+ var match = _providers.FirstOrDefault(provider => string.Equals(provider.Id, _activeProviderId, StringComparison.OrdinalIgnoreCase));
+ if (match is not null)
+ {
+ return match;
+ }
+ }
+
+ return _providers[0];
+ }
+ }
+
+ [JsonIgnore]
+ public AIServiceType ActiveServiceTypeKind => ActiveProvider?.ServiceTypeKind ?? AIServiceType.OpenAI;
+
+ public void EnsureActiveProvider()
+ {
+ EnsureProvidersFromLegacyData();
+
+ if (_providers is null || _providers.Count == 0)
+ {
+ _activeProviderId = string.Empty;
+ return;
}
- return snapshot;
+ if (string.IsNullOrWhiteSpace(_activeProviderId) || !_providers.Any(provider => string.Equals(provider.Id, _activeProviderId, StringComparison.OrdinalIgnoreCase)))
+ {
+ _activeProviderId = _providers[0].Id;
+ }
}
- public void SetProviderConfiguration(string serviceType, AIProviderConfigurationSnapshot snapshot)
+ private void EnsureProvidersFromLegacyData()
{
- _providerConfigurations[NormalizeServiceType(serviceType)] = snapshot ?? new AIProviderConfigurationSnapshot();
- OnPropertyChanged(nameof(ProviderConfigurations));
+ _providers ??= new ObservableCollection();
+
+ if (_providers.Count > 0)
+ {
+ return;
+ }
+
+ bool migrated = false;
+
+ if (_legacyProviderConfigurations is not null && _legacyProviderConfigurations.Count > 0)
+ {
+ foreach (var kvp in _legacyProviderConfigurations)
+ {
+ var snapshot = kvp.Value ?? new AIProviderConfigurationSnapshot();
+ string serviceType = string.IsNullOrWhiteSpace(kvp.Key) ? _legacyServiceType ?? "OpenAI" : kvp.Key;
+
+ var provider = new PasteAIProviderDefinition
+ {
+ ServiceType = serviceType ?? "OpenAI",
+ ModelName = snapshot.ModelName ?? string.Empty,
+ 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,
+ };
+
+ _providers.Add(provider);
+ }
+
+ migrated = true;
+ }
+ else if (!string.IsNullOrWhiteSpace(_legacyServiceType)
+ || !string.IsNullOrWhiteSpace(_legacyModelName)
+ || !string.IsNullOrWhiteSpace(_legacyEndpointUrl)
+ || !string.IsNullOrWhiteSpace(_legacyApiVersion)
+ || !string.IsNullOrWhiteSpace(_legacyDeploymentName)
+ || !string.IsNullOrWhiteSpace(_legacyModelPath)
+ || !string.IsNullOrWhiteSpace(_legacySystemPrompt))
+ {
+ var provider = new PasteAIProviderDefinition
+ {
+ ServiceType = _legacyServiceType ?? "OpenAI",
+ ModelName = _legacyModelName ?? "gpt-3.5-turbo",
+ EndpointUrl = _legacyEndpointUrl ?? string.Empty,
+ ApiVersion = _legacyApiVersion ?? string.Empty,
+ DeploymentName = _legacyDeploymentName ?? string.Empty,
+ ModelPath = _legacyModelPath ?? string.Empty,
+ SystemPrompt = _legacySystemPrompt ?? string.Empty,
+ ModerationEnabled = _legacyModerationEnabled,
+ };
+
+ _providers.Add(provider);
+ migrated = true;
+ }
+
+ if (migrated)
+ {
+ _legacyServiceType = null;
+ _legacyModelName = null;
+ _legacyEndpointUrl = null;
+ _legacyApiVersion = null;
+ _legacyDeploymentName = null;
+ _legacyModelPath = null;
+ _legacySystemPrompt = null;
+ _legacyModerationEnabled = false;
+ _legacyProviderConfigurations = null;
+ }
}
public override string ToString()
@@ -149,10 +261,5 @@ namespace Microsoft.PowerToys.Settings.UI.Library
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
-
- private static string NormalizeServiceType(string serviceType)
- {
- return string.IsNullOrWhiteSpace(serviceType) ? "OpenAI" : serviceType;
- }
}
}
diff --git a/src/settings-ui/Settings.UI.Library/PasteAIProviderDefinition.cs b/src/settings-ui/Settings.UI.Library/PasteAIProviderDefinition.cs
new file mode 100644
index 0000000000..c59dd573b0
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/PasteAIProviderDefinition.cs
@@ -0,0 +1,142 @@
+// 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.Generic;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.PowerToys.Settings.UI.Library
+{
+ ///
+ /// Represents a single Paste AI provider configuration entry.
+ ///
+ public class PasteAIProviderDefinition : INotifyPropertyChanged
+ {
+ private string _id = Guid.NewGuid().ToString("N");
+ private string _serviceType = "OpenAI";
+ private string _modelName = string.Empty;
+ private string _endpointUrl = string.Empty;
+ private string _apiVersion = string.Empty;
+ private string _deploymentName = string.Empty;
+ private string _modelPath = string.Empty;
+ private string _systemPrompt = string.Empty;
+ private bool _moderationEnabled = true;
+ private bool _isActive;
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ [JsonPropertyName("id")]
+ public string Id
+ {
+ get => _id;
+ set => SetProperty(ref _id, value);
+ }
+
+ [JsonPropertyName("service-type")]
+ public string ServiceType
+ {
+ get => _serviceType;
+ set => SetProperty(ref _serviceType, string.IsNullOrWhiteSpace(value) ? "OpenAI" : value);
+ }
+
+ [JsonIgnore]
+ public AIServiceType ServiceTypeKind
+ {
+ get => ServiceType.ToAIServiceType();
+ set => ServiceType = value.ToConfigurationString();
+ }
+
+ [JsonPropertyName("model-name")]
+ public string ModelName
+ {
+ get => _modelName;
+ set => SetProperty(ref _modelName, value ?? string.Empty);
+ }
+
+ [JsonPropertyName("endpoint-url")]
+ public string EndpointUrl
+ {
+ get => _endpointUrl;
+ set => SetProperty(ref _endpointUrl, value ?? string.Empty);
+ }
+
+ [JsonPropertyName("api-version")]
+ public string ApiVersion
+ {
+ get => _apiVersion;
+ set => SetProperty(ref _apiVersion, value ?? string.Empty);
+ }
+
+ [JsonPropertyName("deployment-name")]
+ public string DeploymentName
+ {
+ get => _deploymentName;
+ set => SetProperty(ref _deploymentName, value ?? string.Empty);
+ }
+
+ [JsonPropertyName("model-path")]
+ public string ModelPath
+ {
+ get => _modelPath;
+ set => SetProperty(ref _modelPath, value ?? string.Empty);
+ }
+
+ [JsonPropertyName("system-prompt")]
+ public string SystemPrompt
+ {
+ get => _systemPrompt;
+ set => SetProperty(ref _systemPrompt, value?.Trim() ?? string.Empty);
+ }
+
+ [JsonPropertyName("moderation-enabled")]
+ public bool ModerationEnabled
+ {
+ get => _moderationEnabled;
+ set => SetProperty(ref _moderationEnabled, value);
+ }
+
+ [JsonIgnore]
+ public bool IsActive
+ {
+ get => _isActive;
+ set => SetProperty(ref _isActive, value);
+ }
+
+ public PasteAIProviderDefinition Clone()
+ {
+ return new PasteAIProviderDefinition
+ {
+ Id = Id,
+ ServiceType = ServiceType,
+ ModelName = ModelName,
+ EndpointUrl = EndpointUrl,
+ ApiVersion = ApiVersion,
+ DeploymentName = DeploymentName,
+ ModelPath = ModelPath,
+ SystemPrompt = SystemPrompt,
+ ModerationEnabled = ModerationEnabled,
+ IsActive = IsActive,
+ };
+ }
+
+ protected bool SetProperty(ref T field, T value, [CallerMemberName] string propertyName = null)
+ {
+ if (EqualityComparer.Default.Equals(field, value))
+ {
+ return false;
+ }
+
+ field = value;
+ OnPropertyChanged(propertyName);
+ return true;
+ }
+
+ protected void OnPropertyChanged(string propertyName)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI/Converters/ServiceTypeToIconConverter.cs b/src/settings-ui/Settings.UI/Converters/ServiceTypeToIconConverter.cs
new file mode 100644
index 0000000000..7d632906c2
--- /dev/null
+++ b/src/settings-ui/Settings.UI/Converters/ServiceTypeToIconConverter.cs
@@ -0,0 +1,30 @@
+// 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 Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Data;
+using Microsoft.UI.Xaml.Media.Imaging;
+
+namespace Microsoft.PowerToys.Settings.UI.Converters;
+
+public partial class ServiceTypeToIconConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, string language)
+ {
+ if (value is not string serviceType || string.IsNullOrWhiteSpace(serviceType))
+ {
+ return new ImageIcon { Source = new SvgImageSource(new Uri(AIServiceTypeRegistry.GetIconPath(AIServiceType.OpenAI))) };
+ }
+
+ var iconPath = AIServiceTypeRegistry.GetIconPath(serviceType);
+ return new ImageIcon { Source = new SvgImageSource(new Uri(iconPath)) };
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, string language)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml
index fa82f3d5b8..73fd9f9b97 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml
@@ -3,17 +3,24 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
+ xmlns:converters="using:Microsoft.PowerToys.Settings.UI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Microsoft.PowerToys.Settings.UI.Helpers"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="using:Microsoft.PowerToys.Settings.UI.Library"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:ui="using:CommunityToolkit.WinUI"
+ xmlns:viewmodels="using:Microsoft.PowerToys.Settings.UI.ViewModels"
x:Name="RootPage"
AutomationProperties.LandmarkType="Main"
mc:Ignorable="d">
+
+
+
+
+
ms-appx:///Assets/Settings/Modules/APDialog.dark.png
@@ -125,100 +132,55 @@
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ Spacing="16">
+
+
+ Text="{x:Bind ViewModel.PasteAIProviderDraft.ModelName, Mode=TwoWay}" />
+ Text="{x:Bind ViewModel.PasteAIProviderDraft.EndpointUrl, Mode=TwoWay}" />
+ Text="{x:Bind ViewModel.PasteAIProviderDraft.DeploymentName, Mode=TwoWay}" />
+ Text="{x:Bind ViewModel.PasteAIProviderDraft.ModelPath, Mode=TwoWay}" />
+ Visibility="{x:Bind GetServiceLegalVisibility(ViewModel.PasteAIProviderDraft?.ServiceType), Mode=OneWay}">
+ Content="{x:Bind GetServiceTermsLabel(ViewModel.PasteAIProviderDraft?.ServiceType), Mode=OneWay}"
+ NavigateUri="{x:Bind GetServiceTermsUri(ViewModel.PasteAIProviderDraft?.ServiceType), Mode=OneWay}"
+ Visibility="{x:Bind GetServiceTermsVisibility(ViewModel.PasteAIProviderDraft?.ServiceType), Mode=OneWay}" />
+ Content="{x:Bind GetServicePrivacyLabel(ViewModel.PasteAIProviderDraft?.ServiceType), Mode=OneWay}"
+ NavigateUri="{x:Bind GetServicePrivacyUri(ViewModel.PasteAIProviderDraft?.ServiceType), Mode=OneWay}"
+ Visibility="{x:Bind GetServicePrivacyVisibility(ViewModel.PasteAIProviderDraft?.ServiceType), Mode=OneWay}" />
-
+
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml.cs
index c74fd1b1c1..03cf973c8e 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml.cs
@@ -19,6 +19,7 @@ using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Media.Imaging;
namespace Microsoft.PowerToys.Settings.UI.Views
{
@@ -30,6 +31,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
private bool _suppressFoundrySelectionChanged;
private bool _isFoundryLocalAvailable;
private bool _disposed;
+ private const string PasteAiDialogDefaultTitle = "Paste with AI provider configuration";
private static readonly Dictionary ServiceLegalInformation = new(StringComparer.OrdinalIgnoreCase)
{
@@ -253,7 +255,10 @@ namespace Microsoft.PowerToys.Settings.UI.Views
if (!string.IsNullOrEmpty(selectedFile))
{
PasteAIModelPathTextBox.Text = selectedFile;
- ViewModel.PasteAIConfiguration.ModelPath = selectedFile;
+ if (ViewModel?.PasteAIProviderDraft is not null)
+ {
+ ViewModel.PasteAIProviderDraft.ModelPath = selectedFile;
+ }
}
}
@@ -296,47 +301,54 @@ namespace Microsoft.PowerToys.Settings.UI.Views
System.Diagnostics.Debug.WriteLine($"{configType} API key saved successfully");
}
- private async void PasteAIServiceTypeComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
- {
- UpdatePasteAIUIVisibility();
- await UpdateFoundryLocalUIAsync();
- }
-
private void UpdatePasteAIUIVisibility()
{
- if (PasteAIServiceTypeListView?.SelectedValue == null)
+ var draft = ViewModel?.PasteAIProviderDraft;
+ if (draft is null)
{
return;
}
- string selectedType = PasteAIServiceTypeListView.SelectedValue.ToString();
+ string selectedType = draft.ServiceType ?? string.Empty;
+ AIServiceType serviceKind = draft.ServiceTypeKind;
- bool isOnnx = string.Equals(selectedType, "Onnx", StringComparison.OrdinalIgnoreCase);
- bool isFoundryLocal = string.Equals(selectedType, "FoundryLocal", StringComparison.OrdinalIgnoreCase);
- bool showEndpoint = string.Equals(selectedType, "AzureOpenAI", StringComparison.OrdinalIgnoreCase)
- || string.Equals(selectedType, "AzureAIInference", StringComparison.OrdinalIgnoreCase)
- || string.Equals(selectedType, "Mistral", StringComparison.OrdinalIgnoreCase)
- || string.Equals(selectedType, "HuggingFace", StringComparison.OrdinalIgnoreCase)
- || string.Equals(selectedType, "Ollama", StringComparison.OrdinalIgnoreCase);
- bool showDeployment = string.Equals(selectedType, "AzureOpenAI", StringComparison.OrdinalIgnoreCase);
+ bool requiresEndpoint = serviceKind is AIServiceType.AzureOpenAI
+ or AIServiceType.AzureAIInference
+ or AIServiceType.Mistral
+ or AIServiceType.HuggingFace
+ or AIServiceType.Ollama;
+ bool requiresDeployment = serviceKind == AIServiceType.AzureOpenAI;
+ bool requiresApiVersion = serviceKind == AIServiceType.AzureOpenAI;
+ bool requiresModelPath = serviceKind == AIServiceType.Onnx;
+ bool isFoundryLocal = serviceKind == AIServiceType.FoundryLocal;
bool requiresApiKey = RequiresApiKeyForService(selectedType);
- bool showModerationToggle = string.Equals(selectedType, "OpenAI", StringComparison.OrdinalIgnoreCase);
+ bool showModerationToggle = serviceKind == AIServiceType.OpenAI;
- if (ViewModel.PasteAIConfiguration is not null)
+ if (string.IsNullOrWhiteSpace(draft.EndpointUrl))
{
- ViewModel.PasteAIConfiguration.EndpointUrl = ViewModel.GetPasteAIEndpoint(selectedType);
+ string storedEndpoint = ViewModel.GetPasteAIEndpoint(draft.Id, selectedType);
+ if (!string.IsNullOrWhiteSpace(storedEndpoint))
+ {
+ draft.EndpointUrl = storedEndpoint;
+ }
}
- PasteAIEndpointUrlTextBox.Visibility = showEndpoint ? Visibility.Visible : Visibility.Collapsed;
- PasteAIDeploymentNameTextBox.Visibility = showDeployment ? Visibility.Visible : Visibility.Collapsed;
- PasteAIModelPanel.Visibility = isOnnx ? Visibility.Visible : Visibility.Collapsed;
+ PasteAIEndpointUrlTextBox.Visibility = requiresEndpoint ? Visibility.Visible : Visibility.Collapsed;
+ if (requiresEndpoint)
+ {
+ PasteAIEndpointUrlTextBox.PlaceholderText = GetEndpointPlaceholder(serviceKind);
+ }
+
+ PasteAIDeploymentNameTextBox.Visibility = requiresDeployment ? Visibility.Visible : Visibility.Collapsed;
+ PasteAIApiVersionTextBox.Visibility = requiresApiVersion ? Visibility.Visible : Visibility.Collapsed;
+ PasteAIModelPanel.Visibility = requiresModelPath ? Visibility.Visible : Visibility.Collapsed;
PasteAIModerationToggle.Visibility = showModerationToggle ? Visibility.Visible : Visibility.Collapsed;
PasteAIApiKeyPasswordBox.Visibility = requiresApiKey ? Visibility.Visible : Visibility.Collapsed;
PasteAIModelNameTextBox.Visibility = isFoundryLocal ? Visibility.Collapsed : Visibility.Visible;
if (requiresApiKey)
{
- PasteAIApiKeyPasswordBox.Password = ViewModel.GetPasteAIApiKey(selectedType);
+ PasteAIApiKeyPasswordBox.Password = ViewModel.GetPasteAIApiKey(draft.Id, selectedType);
}
else
{
@@ -384,13 +396,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views
private Task UpdateFoundryLocalUIAsync(bool refreshFoundry = false)
{
- if (PasteAIServiceTypeListView?.SelectedValue == null)
- {
- return Task.CompletedTask;
- }
-
- string selectedType = PasteAIServiceTypeListView.SelectedValue.ToString();
- bool isFoundryLocal = string.Equals(selectedType, "FoundryLocal", StringComparison.Ordinal);
+ string selectedType = ViewModel?.PasteAIProviderDraft?.ServiceType ?? string.Empty;
+ bool isFoundryLocal = string.Equals(selectedType, "FoundryLocal", StringComparison.OrdinalIgnoreCase);
if (FoundryLocalPanel is not null)
{
@@ -409,11 +416,18 @@ namespace Microsoft.PowerToys.Settings.UI.Views
FoundryLocalPicker.SelectedModel = null;
}
- PasteAIProviderConfigurationDialog.IsPrimaryButtonEnabled = true;
+ if (PasteAIProviderConfigurationDialog is not null)
+ {
+ PasteAIProviderConfigurationDialog.IsPrimaryButtonEnabled = true;
+ }
+
return Task.CompletedTask;
}
- PasteAIProviderConfigurationDialog.IsPrimaryButtonEnabled = false;
+ if (PasteAIProviderConfigurationDialog is not null)
+ {
+ PasteAIProviderConfigurationDialog.IsPrimaryButtonEnabled = false;
+ }
FoundryLocalPicker?.RequestLoad(refreshFoundry);
@@ -565,7 +579,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
return;
}
- var currentModelReference = ViewModel?.PasteAIConfiguration?.ModelName;
+ var currentModelReference = ViewModel?.PasteAIProviderDraft?.ModelName;
ModelDetails matchingModel = null;
@@ -587,9 +601,9 @@ namespace Microsoft.PowerToys.Settings.UI.Views
if (matchingModel is null)
{
- if (ViewModel?.PasteAIConfiguration is not null)
+ if (ViewModel?.PasteAIProviderDraft is not null)
{
- ViewModel.PasteAIConfiguration.ModelName = string.Empty;
+ ViewModel.PasteAIProviderDraft.ModelName = string.Empty;
}
if (FoundryLocalPicker is not null)
@@ -601,9 +615,9 @@ namespace Microsoft.PowerToys.Settings.UI.Views
}
else
{
- if (ViewModel?.PasteAIConfiguration is not null)
+ if (ViewModel?.PasteAIProviderDraft is not null)
{
- ViewModel.PasteAIConfiguration.ModelName = NormalizeFoundryModelReference(matchingModel.Url ?? matchingModel.Name);
+ ViewModel.PasteAIProviderDraft.ModelName = NormalizeFoundryModelReference(matchingModel.Url ?? matchingModel.Name);
}
if (FoundryLocalPicker is not null)
@@ -635,9 +649,9 @@ namespace Microsoft.PowerToys.Settings.UI.Views
return;
}
- bool isFoundrySelected = string.Equals(PasteAIServiceTypeListView?.SelectedValue?.ToString(), "FoundryLocal", StringComparison.Ordinal);
+ bool isFoundrySelected = string.Equals(ViewModel?.PasteAIProviderDraft?.ServiceType, "FoundryLocal", StringComparison.OrdinalIgnoreCase);
- if (!isFoundrySelected)
+ if (!isFoundrySelected || ViewModel?.PasteAIProviderDraft is null)
{
PasteAIProviderConfigurationDialog.IsPrimaryButtonEnabled = true;
return;
@@ -662,9 +676,9 @@ namespace Microsoft.PowerToys.Settings.UI.Views
if (selectedModel is not null)
{
- if (ViewModel?.PasteAIConfiguration is not null)
+ if (ViewModel?.PasteAIProviderDraft is not null)
{
- ViewModel.PasteAIConfiguration.ModelName = NormalizeFoundryModelReference(selectedModel.Url ?? selectedModel.Name);
+ ViewModel.PasteAIProviderDraft.ModelName = NormalizeFoundryModelReference(selectedModel.Url ?? selectedModel.Name);
}
if (FoundryLocalPicker is not null)
@@ -674,9 +688,9 @@ namespace Microsoft.PowerToys.Settings.UI.Views
}
else
{
- if (ViewModel?.PasteAIConfiguration is not null)
+ if (ViewModel?.PasteAIProviderDraft is not null)
{
- ViewModel.PasteAIConfiguration.ModelName = string.Empty;
+ ViewModel.PasteAIProviderDraft.ModelName = string.Empty;
}
if (FoundryLocalPicker is not null)
@@ -814,10 +828,17 @@ namespace Microsoft.PowerToys.Settings.UI.Views
private void PasteAIProviderConfigurationDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
- string serviceType = PasteAIServiceTypeListView.SelectedValue?.ToString() ?? "OpenAI";
+ var draft = ViewModel?.PasteAIProviderDraft;
+ if (draft is null)
+ {
+ args.Cancel = true;
+ return;
+ }
+
+ string serviceType = draft.ServiceType ?? "OpenAI";
string apiKey = PasteAIApiKeyPasswordBox.Password;
string trimmedApiKey = apiKey?.Trim() ?? string.Empty;
- string endpoint = (ViewModel.PasteAIConfiguration.EndpointUrl ?? string.Empty).Trim();
+ string endpoint = (draft.EndpointUrl ?? string.Empty).Trim();
if (RequiresApiKeyForService(serviceType) && string.IsNullOrWhiteSpace(trimmedApiKey))
{
@@ -825,10 +846,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views
return;
}
- ViewModel.PasteAIConfiguration.EndpointUrl = endpoint;
- ViewModel.SavePasteAICredential(serviceType, endpoint, trimmedApiKey);
- ViewModel.PasteAIConfiguration.EndpointUrl = ViewModel.GetPasteAIEndpoint(serviceType);
- PasteAIApiKeyPasswordBox.Password = ViewModel.GetPasteAIApiKey(serviceType);
+ ViewModel.CommitPasteAIProviderDraft(trimmedApiKey, endpoint);
+ PasteAIApiKeyPasswordBox.Password = string.Empty;
// Show success message
ShowApiKeySavedMessage("Paste AI");
@@ -847,27 +866,31 @@ namespace Microsoft.PowerToys.Settings.UI.Views
RefreshDialogBindings();
}
- private async void PasteAIServiceTypeListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
- {
- UpdatePasteAIUIVisibility();
- await UpdateFoundryLocalUIAsync(refreshFoundry: true);
- RefreshDialogBindings();
- }
-
private static bool RequiresApiKeyForService(string serviceType)
{
- if (string.IsNullOrWhiteSpace(serviceType))
- {
- return true;
- }
+ var serviceKind = serviceType.ToAIServiceType();
- return serviceType.Equals("Onnx", StringComparison.OrdinalIgnoreCase)
- ? false
- : !serviceType.Equals("Ollama", StringComparison.OrdinalIgnoreCase)
- && !serviceType.Equals("FoundryLocal", StringComparison.OrdinalIgnoreCase)
- && !serviceType.Equals("WindowsML", StringComparison.OrdinalIgnoreCase)
- && !serviceType.Equals("Anthropic", StringComparison.OrdinalIgnoreCase)
- && !serviceType.Equals("AmazonBedrock", StringComparison.OrdinalIgnoreCase);
+ return serviceKind switch
+ {
+ AIServiceType.Onnx => false,
+ AIServiceType.Ollama => false,
+ AIServiceType.FoundryLocal => false,
+ AIServiceType.ML => false,
+ _ => true,
+ };
+ }
+
+ private static string GetEndpointPlaceholder(AIServiceType serviceKind)
+ {
+ return serviceKind switch
+ {
+ AIServiceType.AzureOpenAI => "https://your-resource.openai.azure.com/",
+ AIServiceType.AzureAIInference => "https://{resource-name}.cognitiveservices.azure.com/",
+ AIServiceType.Mistral => "https://api.mistral.ai/v1/",
+ AIServiceType.HuggingFace => "https://api-inference.huggingface.co/models/",
+ AIServiceType.Ollama => "http://localhost:11434/",
+ _ => "https://your-resource.openai.azure.com/",
+ };
}
private bool HasServiceLegalInfo(string serviceType) => TryGetServiceLegalInfo(serviceType, out _);
@@ -977,12 +1000,155 @@ namespace Microsoft.PowerToys.Settings.UI.Views
}
}
- private void ProviderMenuFlyoutItem_Click(object sender, RoutedEventArgs e)
+ private void AddProviderMenuFlyout_Opening(object sender, object e)
{
- if (sender is MenuFlyoutItem menuItem && menuItem.Tag is string tag)
+ if (sender is not MenuFlyout menuFlyout)
{
- // TODO: Open dialog and set the right fields
+ return;
}
+
+ // Clear existing items
+ menuFlyout.Items.Clear();
+
+ // Add online models header
+ var onlineHeader = new MenuFlyoutItem
+ {
+ Text = "Online models",
+ FontSize = 12,
+ Foreground = (Microsoft.UI.Xaml.Media.Brush)Application.Current.Resources["TextFillColorSecondaryBrush"],
+ IsEnabled = false,
+ IsHitTestVisible = false,
+ };
+ menuFlyout.Items.Add(onlineHeader);
+
+ // Add online providers
+ foreach (var metadata in AIServiceTypeRegistry.GetOnlineServiceTypes())
+ {
+ var menuItem = new MenuFlyoutItem
+ {
+ Text = metadata.DisplayName,
+ Tag = metadata.ServiceType.ToConfigurationString(),
+ Icon = new ImageIcon { Source = new SvgImageSource(new Uri(metadata.IconPath)) },
+ };
+ menuItem.Click += ProviderMenuFlyoutItem_Click;
+ menuFlyout.Items.Add(menuItem);
+ }
+
+ // Add local models header
+ var localHeader = new MenuFlyoutItem
+ {
+ Text = "Local models",
+ FontSize = 12,
+ Foreground = (Microsoft.UI.Xaml.Media.Brush)Application.Current.Resources["TextFillColorSecondaryBrush"],
+ IsEnabled = false,
+ IsHitTestVisible = false,
+ Margin = new Thickness(0, 16, 0, 0),
+ };
+ menuFlyout.Items.Add(localHeader);
+
+ // Add local providers
+ foreach (var metadata in AIServiceTypeRegistry.GetLocalServiceTypes())
+ {
+ var menuItem = new MenuFlyoutItem
+ {
+ Text = metadata.DisplayName,
+ Tag = metadata.ServiceType.ToConfigurationString(),
+ Icon = new ImageIcon { Source = new SvgImageSource(new Uri(metadata.IconPath)) },
+ };
+ menuItem.Click += ProviderMenuFlyoutItem_Click;
+ menuFlyout.Items.Add(menuItem);
+ }
+ }
+
+ private async void ProviderMenuFlyoutItem_Click(object sender, RoutedEventArgs e)
+ {
+ if (sender is not MenuFlyoutItem menuItem || menuItem.Tag is not string tag || string.IsNullOrWhiteSpace(tag))
+ {
+ return;
+ }
+
+ if (ViewModel is null || PasteAIProviderConfigurationDialog is null)
+ {
+ return;
+ }
+
+ string serviceType = tag.Trim();
+ string displayName = string.IsNullOrWhiteSpace(menuItem.Text) ? serviceType : menuItem.Text.Trim();
+
+ ViewModel.BeginAddPasteAIProvider(serviceType);
+ if (ViewModel.PasteAIProviderDraft is null)
+ {
+ return;
+ }
+
+ PasteAIProviderConfigurationDialog.Title = PasteAiDialogDefaultTitle;
+ if (!string.IsNullOrWhiteSpace(displayName))
+ {
+ PasteAIProviderConfigurationDialog.Title = $"{displayName} provider configuration";
+ }
+
+ UpdatePasteAIUIVisibility();
+ await UpdateFoundryLocalUIAsync(refreshFoundry: true);
+ RefreshDialogBindings();
+
+ PasteAIApiKeyPasswordBox.Password = string.Empty;
+ await PasteAIProviderConfigurationDialog.ShowAsync();
+ }
+
+ private async void EditPasteAIProviderButton_Click(object sender, RoutedEventArgs e)
+ {
+ // sender is MenuFlyoutItem with PasteAIProviderDefinition Tag
+ if (sender is not MenuFlyoutItem menuItem || menuItem.Tag is not PasteAIProviderDefinition provider)
+ {
+ return;
+ }
+
+ if (ViewModel is null || PasteAIProviderConfigurationDialog is null)
+ {
+ return;
+ }
+
+ ViewModel.BeginEditPasteAIProvider(provider);
+
+ string titlePrefix = string.IsNullOrWhiteSpace(provider.ModelName) ? provider.ServiceType : provider.ModelName;
+ PasteAIProviderConfigurationDialog.Title = string.IsNullOrWhiteSpace(titlePrefix)
+ ? PasteAiDialogDefaultTitle
+ : $"{titlePrefix} provider configuration";
+
+ UpdatePasteAIUIVisibility();
+ await UpdateFoundryLocalUIAsync(refreshFoundry: false);
+ RefreshDialogBindings();
+ PasteAIApiKeyPasswordBox.Password = ViewModel.GetPasteAIApiKey(provider.Id, provider.ServiceType);
+ await PasteAIProviderConfigurationDialog.ShowAsync();
+ }
+
+ private void RemovePasteAIProviderButton_Click(object sender, RoutedEventArgs e)
+ {
+ // sender is MenuFlyoutItem with PasteAIProviderDefinition Tag
+ if (sender is not MenuFlyoutItem menuItem || menuItem.Tag is not PasteAIProviderDefinition provider)
+ {
+ return;
+ }
+
+ ViewModel?.RemovePasteAIProvider(provider);
+ }
+
+ private void SetActivePasteAIProvider_Click(object sender, RoutedEventArgs e)
+ {
+ // sender is MenuFlyoutItem with PasteAIProviderDefinition Tag
+ if (sender is not MenuFlyoutItem menuItem || menuItem.Tag is not PasteAIProviderDefinition provider)
+ {
+ return;
+ }
+
+ ViewModel?.SetActivePasteAIProvider(provider);
+ }
+
+ private void PasteAIProviderConfigurationDialog_Closed(ContentDialog sender, ContentDialogClosedEventArgs args)
+ {
+ ViewModel?.CancelPasteAIProviderDraft();
+ PasteAIProviderConfigurationDialog.Title = PasteAiDialogDefaultTitle;
+ PasteAIApiKeyPasswordBox.Password = string.Empty;
}
}
}
diff --git a/src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs
index c9eba0247f..45f78c78e9 100644
--- a/src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs
+++ b/src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs
@@ -37,20 +37,10 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
nameof(AdvancedAIConfiguration.ModerationEnabled),
};
- private static readonly HashSet PasteAITrackedProperties = new(StringComparer.Ordinal)
- {
- nameof(PasteAIConfiguration.ModelName),
- nameof(PasteAIConfiguration.EndpointUrl),
- nameof(PasteAIConfiguration.ApiVersion),
- nameof(PasteAIConfiguration.DeploymentName),
- nameof(PasteAIConfiguration.ModelPath),
- nameof(PasteAIConfiguration.SystemPrompt),
- nameof(PasteAIConfiguration.ModerationEnabled),
- };
-
private bool _disposed;
private bool _isLoadingAdvancedAIProviderConfiguration;
- private bool _isLoadingPasteAIProviderConfiguration;
+ private PasteAIProviderDefinition _pasteAIProviderDraft;
+ private PasteAIProviderDefinition _editingPasteAIProvider;
protected override string ModuleName => AdvancedPasteSettings.ModuleName;
@@ -90,6 +80,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
_advancedPasteSettings = advancedPasteSettingsRepository.SettingsConfig;
SeedProviderConfigurationSnapshots();
+ _advancedPasteSettings?.Properties?.PasteAIConfiguration?.EnsureActiveProvider();
AttachConfigurationHandlers();
@@ -100,7 +91,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
_customActions = _advancedPasteSettings.Properties.CustomActions.Value;
LoadAdvancedAIProviderConfiguration();
- LoadPasteAIProviderConfiguration();
+ InitializePasteAIProviderState();
InitializeEnabledValue();
MigrateLegacyAIEnablement();
@@ -221,6 +212,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
public AdvancedPasteAdditionalActions AdditionalActions => _additionalActions;
+ public static IEnumerable AvailableProviders => AIServiceTypeRegistry.GetAvailableServiceTypes();
+
public bool IsAIEnabled => _advancedPasteSettings.Properties.IsAIEnabled && !IsOnlineAIModelsDisallowedByGPO;
private bool LegacyOpenAIKeyExists()
@@ -234,7 +227,10 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
if (legacyOpenAIKey != null)
{
string credentialResource = GetAICredentialResource("OpenAI");
- string credentialUserName = GetPasteAICredentialUserName("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);
@@ -440,6 +436,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
var newValue = value ?? new PasteAIConfiguration();
_advancedPasteSettings.Properties.PasteAIConfiguration = newValue;
SubscribeToPasteAIConfiguration(newValue);
+ newValue?.EnsureActiveProvider();
+ UpdateActivePasteAIProviderFlags();
OnPropertyChanged(nameof(PasteAIConfiguration));
SaveAndNotifySettings();
@@ -447,6 +445,21 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
+ public PasteAIProviderDefinition PasteAIProviderDraft
+ {
+ get => _pasteAIProviderDraft;
+ private set
+ {
+ if (!ReferenceEquals(_pasteAIProviderDraft, value))
+ {
+ _pasteAIProviderDraft = value;
+ OnPropertyChanged(nameof(PasteAIProviderDraft));
+ }
+ }
+ }
+
+ public bool IsEditingPasteAIProvider => _editingPasteAIProvider is not null;
+
public bool ShowCustomPreview
{
get => _advancedPasteSettings.Properties.ShowCustomPreview;
@@ -505,6 +518,179 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
OnPropertyChanged(nameof(IsAIEnabled));
}
+ public void BeginAddPasteAIProvider(string serviceType)
+ {
+ var normalizedServiceType = NormalizeServiceType(serviceType, out var persistedServiceType);
+
+ var provider = new PasteAIProviderDefinition
+ {
+ ServiceType = persistedServiceType,
+ ModelName = GetDefaultModelName(normalizedServiceType),
+ EndpointUrl = string.Empty,
+ ApiVersion = string.Empty,
+ DeploymentName = string.Empty,
+ ModelPath = string.Empty,
+ SystemPrompt = string.Empty,
+ ModerationEnabled = normalizedServiceType == AIServiceType.OpenAI,
+ };
+
+ if (normalizedServiceType is AIServiceType.FoundryLocal or AIServiceType.Onnx or AIServiceType.ML)
+ {
+ provider.ModelName = string.Empty;
+ }
+
+ _editingPasteAIProvider = null;
+ PasteAIProviderDraft = provider;
+ }
+
+ private static AIServiceType NormalizeServiceType(string serviceType, out string persistedServiceType)
+ {
+ if (string.IsNullOrWhiteSpace(serviceType))
+ {
+ persistedServiceType = AIServiceType.OpenAI.ToConfigurationString();
+ return AIServiceType.OpenAI;
+ }
+
+ var trimmed = serviceType.Trim();
+ var serviceTypeKind = trimmed.ToAIServiceType();
+
+ if (serviceTypeKind == AIServiceType.Unknown)
+ {
+ persistedServiceType = AIServiceType.OpenAI.ToConfigurationString();
+ return AIServiceType.OpenAI;
+ }
+
+ persistedServiceType = trimmed;
+ 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-1.5-pro",
+ AIServiceType.AzureAIInference => "gpt-4o-mini",
+ AIServiceType.Ollama => "llama3",
+ AIServiceType.Anthropic => "claude-3-5-sonnet",
+ AIServiceType.AmazonBedrock => "anthropic.claude-3-haiku",
+ _ => string.Empty,
+ };
+ }
+
+ public void BeginEditPasteAIProvider(PasteAIProviderDefinition provider)
+ {
+ ArgumentNullException.ThrowIfNull(provider);
+
+ _editingPasteAIProvider = provider;
+ var draft = provider.Clone();
+ var storedEndpoint = GetPasteAIEndpoint(draft.Id, draft.ServiceType);
+ if (!string.IsNullOrWhiteSpace(storedEndpoint))
+ {
+ draft.EndpointUrl = storedEndpoint;
+ }
+
+ PasteAIProviderDraft = draft;
+ }
+
+ public void CancelPasteAIProviderDraft()
+ {
+ PasteAIProviderDraft = null;
+ _editingPasteAIProvider = null;
+ }
+
+ public void CommitPasteAIProviderDraft(string apiKey, string endpoint)
+ {
+ if (PasteAIProviderDraft is null)
+ {
+ return;
+ }
+
+ var config = PasteAIConfiguration ?? new PasteAIConfiguration();
+ if (_advancedPasteSettings.Properties.PasteAIConfiguration is null)
+ {
+ PasteAIConfiguration = config;
+ }
+
+ var draft = PasteAIProviderDraft;
+ draft.EndpointUrl = endpoint?.Trim() ?? string.Empty;
+
+ SavePasteAICredential(draft.Id, draft.ServiceType, draft.EndpointUrl, apiKey);
+
+ if (_editingPasteAIProvider is null)
+ {
+ config.Providers.Add(draft);
+ config.ActiveProviderId ??= draft.Id;
+ }
+ else
+ {
+ UpdateProviderFromDraft(_editingPasteAIProvider, draft);
+ _editingPasteAIProvider = null;
+ }
+
+ config.EnsureActiveProvider();
+ UpdateActivePasteAIProviderFlags();
+ PasteAIProviderDraft = null;
+ SaveAndNotifySettings();
+ OnPropertyChanged(nameof(PasteAIConfiguration));
+ }
+
+ public void RemovePasteAIProvider(PasteAIProviderDefinition provider)
+ {
+ if (provider is null)
+ {
+ return;
+ }
+
+ var config = PasteAIConfiguration;
+ if (config?.Providers is null)
+ {
+ return;
+ }
+
+ if (config.Providers.Remove(provider))
+ {
+ RemovePasteAICredentials(provider.Id, provider.ServiceType);
+ config.EnsureActiveProvider();
+ UpdateActivePasteAIProviderFlags();
+ SaveAndNotifySettings();
+ OnPropertyChanged(nameof(PasteAIConfiguration));
+ }
+ }
+
+ public void SetActivePasteAIProvider(PasteAIProviderDefinition provider)
+ {
+ if (provider is null)
+ {
+ return;
+ }
+
+ var config = PasteAIConfiguration;
+ if (config is null)
+ {
+ return;
+ }
+
+ if (!string.Equals(config.ActiveProviderId, provider.Id, StringComparison.OrdinalIgnoreCase))
+ {
+ config.ActiveProviderId = provider.Id;
+ UpdateActivePasteAIProviderFlags();
+ SaveAndNotifySettings();
+ OnPropertyChanged(nameof(PasteAIConfiguration));
+ }
+ }
+
+ public bool IsActivePasteAIProvider(string providerId)
+ {
+ var activeId = PasteAIConfiguration?.ActiveProviderId ?? string.Empty;
+ providerId ??= string.Empty;
+ return string.Equals(activeId, providerId, StringComparison.OrdinalIgnoreCase);
+ }
+
+ public bool CanSetActivePasteAIProvider(string providerId) => !IsActivePasteAIProvider(providerId);
+
protected override void Dispose(bool disposing)
{
if (!_disposed)
@@ -630,15 +816,18 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
- internal void SavePasteAICredential(string serviceType, string endpoint, string apiKey)
+ internal void SavePasteAICredential(string providerId, string serviceType, string endpoint, string apiKey)
{
try
{
endpoint = endpoint?.Trim() ?? string.Empty;
apiKey = apiKey?.Trim() ?? string.Empty;
+ serviceType = string.IsNullOrWhiteSpace(serviceType) ? "OpenAI" : serviceType;
+ providerId ??= string.Empty;
+
string credentialResource = GetAICredentialResource(serviceType);
- string credentialUserName = GetPasteAICredentialUserName(serviceType);
- string endpointCredentialUserName = GetPasteAIEndpointCredentialUserName(serviceType);
+ string credentialUserName = GetPasteAICredentialUserName(providerId, serviceType);
+ string endpointCredentialUserName = GetPasteAIEndpointCredentialUserName(providerId, serviceType);
PasswordVault vault = new();
TryRemoveCredential(vault, credentialResource, credentialUserName);
TryRemoveCredential(vault, credentialResource, endpointCredentialUserName);
@@ -672,12 +861,13 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
GetAdvancedAICredentialUserName(serviceType));
}
- internal string GetPasteAIApiKey(string serviceType)
+ internal string GetPasteAIApiKey(string providerId, string serviceType)
{
serviceType = string.IsNullOrWhiteSpace(serviceType) ? "OpenAI" : serviceType;
+ providerId ??= string.Empty;
return RetrieveCredentialValue(
GetAICredentialResource(serviceType),
- GetPasteAICredentialUserName(serviceType));
+ GetPasteAICredentialUserName(providerId, serviceType));
}
internal string GetAdvancedAIEndpoint(string serviceType)
@@ -688,12 +878,13 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
GetAdvancedAIEndpointCredentialUserName(serviceType));
}
- internal string GetPasteAIEndpoint(string serviceType)
+ internal string GetPasteAIEndpoint(string providerId, string serviceType)
{
serviceType = string.IsNullOrWhiteSpace(serviceType) ? "OpenAI" : serviceType;
+ providerId ??= string.Empty;
return RetrieveCredentialValue(
GetAICredentialResource(serviceType),
- GetPasteAIEndpointCredentialUserName(serviceType));
+ GetPasteAIEndpointCredentialUserName(providerId, serviceType));
}
private static string GetAdvancedAICredentialUserName(string serviceType)
@@ -737,49 +928,96 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
};
}
- private string GetPasteAICredentialUserName(string serviceType)
+ private string GetPasteAICredentialUserName(string providerId, string serviceType)
{
serviceType = string.IsNullOrWhiteSpace(serviceType) ? "OpenAI" : serviceType;
- return serviceType.ToLowerInvariant() switch
- {
- "openai" => "PowerToys_AdvancedPaste_PasteAI_OpenAI",
- "azureopenai" => "PowerToys_AdvancedPaste_PasteAI_AzureOpenAI",
- "azureaiinference" => "PowerToys_AdvancedPaste_PasteAI_AzureAIInference",
- "onnx" => "PowerToys_AdvancedPaste_PasteAI_Onnx", // Onnx doesn't need credentials but keeping consistency
- "mistral" => "PowerToys_AdvancedPaste_PasteAI_Mistral",
- "google" => "PowerToys_AdvancedPaste_PasteAI_Google",
- "huggingface" => "PowerToys_AdvancedPaste_PasteAI_HuggingFace",
- "anthropic" => "PowerToys_AdvancedPaste_PasteAI_Anthropic",
- "amazonbedrock" => "PowerToys_AdvancedPaste_PasteAI_AmazonBedrock",
- "ollama" => "PowerToys_AdvancedPaste_PasteAI_Ollama",
- _ => "PowerToys_AdvancedPaste_PasteAI_OpenAI",
- };
+ providerId ??= string.Empty;
+
+ string service = serviceType.ToLowerInvariant();
+ string normalizedId = NormalizeProviderIdentifier(providerId);
+
+ return $"PowerToys_AdvancedPaste_PasteAI_{service}_{normalizedId}";
}
- private string GetPasteAIEndpointCredentialUserName(string serviceType)
+ private string GetPasteAIEndpointCredentialUserName(string providerId, string serviceType)
{
- return GetPasteAICredentialUserName(serviceType) + "_Endpoint";
+ return GetPasteAICredentialUserName(providerId, serviceType) + "_Endpoint";
+ }
+
+ private static void UpdateProviderFromDraft(PasteAIProviderDefinition target, PasteAIProviderDefinition source)
+ {
+ if (target is null || source is null)
+ {
+ return;
+ }
+
+ target.ServiceType = source.ServiceType;
+ target.ModelName = source.ModelName;
+ target.EndpointUrl = source.EndpointUrl;
+ target.ApiVersion = source.ApiVersion;
+ target.DeploymentName = source.DeploymentName;
+ target.ModelPath = source.ModelPath;
+ target.SystemPrompt = source.SystemPrompt;
+ target.ModerationEnabled = source.ModerationEnabled;
+ }
+
+ private void RemovePasteAICredentials(string providerId, string serviceType)
+ {
+ try
+ {
+ serviceType = string.IsNullOrWhiteSpace(serviceType) ? "OpenAI" : serviceType;
+ providerId ??= string.Empty;
+
+ string credentialResource = GetAICredentialResource(serviceType);
+ PasswordVault vault = new();
+ TryRemoveCredential(vault, credentialResource, GetPasteAICredentialUserName(providerId, serviceType));
+ TryRemoveCredential(vault, credentialResource, GetPasteAIEndpointCredentialUserName(providerId, serviceType));
+ }
+ catch (Exception)
+ {
+ }
+ }
+
+ 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();
}
private static bool RequiresCredentialStorage(string serviceType)
{
- if (string.IsNullOrWhiteSpace(serviceType))
- {
- return true;
- }
+ var serviceTypeKind = serviceType.ToAIServiceType();
- return serviceType.ToLowerInvariant() switch
+ return serviceTypeKind switch
{
- "onnx" => false,
- "ollama" => false,
- "foundrylocal" => false,
- "windowsml" => false,
- "anthropic" => false,
- "amazonbedrock" => false,
+ AIServiceType.Onnx => false,
+ AIServiceType.Ollama => false,
+ AIServiceType.FoundryLocal => false,
+ AIServiceType.ML => false,
_ => true,
};
}
+ private void UpdateActivePasteAIProviderFlags()
+ {
+ var providers = PasteAIConfiguration?.Providers;
+ if (providers is null)
+ {
+ return;
+ }
+
+ string activeId = PasteAIConfiguration.ActiveProviderId ?? string.Empty;
+ foreach (var provider in providers)
+ {
+ provider.IsActive = string.Equals(provider.Id, activeId, StringComparison.OrdinalIgnoreCase);
+ }
+ }
+
private static void TryRemoveCredential(PasswordVault vault, string credentialResource, string credentialUserName)
{
try
@@ -929,6 +1167,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
if (configuration is not null)
{
configuration.PropertyChanged += OnPasteAIConfigurationPropertyChanged;
+ SubscribeToPasteAIProviders(configuration);
}
}
@@ -937,6 +1176,80 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
if (configuration is not null)
{
configuration.PropertyChanged -= OnPasteAIConfigurationPropertyChanged;
+ UnsubscribeFromPasteAIProviders(configuration);
+ }
+ }
+
+ private void SubscribeToPasteAIProviders(PasteAIConfiguration configuration)
+ {
+ if (configuration?.Providers is null)
+ {
+ return;
+ }
+
+ configuration.Providers.CollectionChanged -= OnPasteAIProvidersCollectionChanged;
+ configuration.Providers.CollectionChanged += OnPasteAIProvidersCollectionChanged;
+
+ foreach (var provider in configuration.Providers)
+ {
+ provider.PropertyChanged -= OnPasteAIProviderPropertyChanged;
+ provider.PropertyChanged += OnPasteAIProviderPropertyChanged;
+ }
+ }
+
+ private void UnsubscribeFromPasteAIProviders(PasteAIConfiguration configuration)
+ {
+ if (configuration?.Providers is null)
+ {
+ return;
+ }
+
+ configuration.Providers.CollectionChanged -= OnPasteAIProvidersCollectionChanged;
+
+ foreach (var provider in configuration.Providers)
+ {
+ provider.PropertyChanged -= OnPasteAIProviderPropertyChanged;
+ }
+ }
+
+ private void OnPasteAIProvidersCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ if (e?.NewItems is not null)
+ {
+ foreach (PasteAIProviderDefinition provider in e.NewItems)
+ {
+ provider.PropertyChanged += OnPasteAIProviderPropertyChanged;
+ }
+ }
+
+ if (e?.OldItems is not null)
+ {
+ foreach (PasteAIProviderDefinition provider in e.OldItems)
+ {
+ provider.PropertyChanged -= OnPasteAIProviderPropertyChanged;
+ }
+ }
+
+ var pasteConfig = _advancedPasteSettings?.Properties?.PasteAIConfiguration;
+ pasteConfig?.EnsureActiveProvider();
+ UpdateActivePasteAIProviderFlags();
+
+ OnPropertyChanged(nameof(PasteAIConfiguration));
+ SaveAndNotifySettings();
+ }
+
+ private void OnPasteAIProviderPropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ if (sender is PasteAIProviderDefinition provider)
+ {
+ // When service type changes we may need to update credentials entry names.
+ if (string.Equals(e.PropertyName, nameof(PasteAIProviderDefinition.ServiceType), StringComparison.Ordinal))
+ {
+ SaveAndNotifySettings();
+ return;
+ }
+
+ SaveAndNotifySettings();
}
}
@@ -964,24 +1277,24 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private void OnPasteAIConfigurationPropertyChanged(object sender, PropertyChangedEventArgs e)
{
- if (_isLoadingPasteAIProviderConfiguration)
+ if (string.Equals(e.PropertyName, nameof(PasteAIConfiguration.Providers), StringComparison.Ordinal))
{
- return;
- }
-
- if (string.Equals(e.PropertyName, nameof(PasteAIConfiguration.ServiceType), StringComparison.Ordinal))
- {
- LoadPasteAIProviderConfiguration();
+ SubscribeToPasteAIProviders(PasteAIConfiguration);
+ UpdateActivePasteAIProviderFlags();
SaveAndNotifySettings();
return;
}
- if (e.PropertyName is not null && PasteAITrackedProperties.Contains(e.PropertyName))
+ if (string.Equals(e.PropertyName, nameof(PasteAIConfiguration.ActiveProviderId), StringComparison.Ordinal)
+ || string.Equals(e.PropertyName, nameof(PasteAIConfiguration.UseSharedCredentials), StringComparison.Ordinal))
{
- PersistPasteAIProviderConfiguration();
- }
+ if (string.Equals(e.PropertyName, nameof(PasteAIConfiguration.ActiveProviderId), StringComparison.Ordinal))
+ {
+ UpdateActivePasteAIProviderFlags();
+ }
- SaveAndNotifySettings();
+ SaveAndNotifySettings();
+ }
}
private void SeedProviderConfigurationSnapshots()
@@ -1003,22 +1316,22 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
});
}
+ // Paste AI provider configurations are handled through the new provider list.
+ }
+
+ private void InitializePasteAIProviderState()
+ {
var pasteConfig = _advancedPasteSettings?.Properties?.PasteAIConfiguration;
- if (pasteConfig is not null && !pasteConfig.HasProviderConfiguration(pasteConfig.ServiceType))
+ if (pasteConfig is null)
{
- pasteConfig.SetProviderConfiguration(
- pasteConfig.ServiceType,
- new AIProviderConfigurationSnapshot
- {
- ModelName = pasteConfig.ModelName,
- EndpointUrl = pasteConfig.EndpointUrl,
- ApiVersion = pasteConfig.ApiVersion,
- DeploymentName = pasteConfig.DeploymentName,
- ModelPath = pasteConfig.ModelPath,
- SystemPrompt = pasteConfig.SystemPrompt,
- ModerationEnabled = pasteConfig.ModerationEnabled,
- });
+ _advancedPasteSettings.Properties.PasteAIConfiguration = new PasteAIConfiguration();
+ pasteConfig = _advancedPasteSettings.Properties.PasteAIConfiguration;
}
+
+ pasteConfig.Providers ??= new ObservableCollection();
+ pasteConfig.EnsureActiveProvider();
+ UpdateActivePasteAIProviderFlags();
+ SubscribeToPasteAIProviders(pasteConfig);
}
private void LoadAdvancedAIProviderConfiguration()
@@ -1050,35 +1363,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
- private void LoadPasteAIProviderConfiguration()
- {
- var config = _advancedPasteSettings?.Properties?.PasteAIConfiguration;
- if (config is null)
- {
- return;
- }
-
- var snapshot = config.GetOrCreateProviderConfiguration(config.ServiceType);
- _isLoadingPasteAIProviderConfiguration = true;
- try
- {
- config.ModelName = snapshot.ModelName ?? string.Empty;
- config.EndpointUrl = snapshot.EndpointUrl ?? string.Empty;
- config.ApiVersion = snapshot.ApiVersion ?? string.Empty;
- config.DeploymentName = snapshot.DeploymentName ?? string.Empty;
- config.ModelPath = snapshot.ModelPath ?? string.Empty;
- config.SystemPrompt = snapshot.SystemPrompt ?? string.Empty;
- config.ModerationEnabled = snapshot.ModerationEnabled;
- string storedEndpoint = GetPasteAIEndpoint(config.ServiceType);
- config.EndpointUrl = storedEndpoint;
- snapshot.EndpointUrl = storedEndpoint;
- }
- finally
- {
- _isLoadingPasteAIProviderConfiguration = false;
- }
- }
-
private void PersistAdvancedAIProviderConfiguration()
{
if (_isLoadingAdvancedAIProviderConfiguration)
@@ -1102,29 +1386,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
snapshot.ModerationEnabled = config.ModerationEnabled;
}
- private void PersistPasteAIProviderConfiguration()
- {
- if (_isLoadingPasteAIProviderConfiguration)
- {
- return;
- }
-
- var config = _advancedPasteSettings?.Properties?.PasteAIConfiguration;
- if (config is null)
- {
- return;
- }
-
- var snapshot = config.GetOrCreateProviderConfiguration(config.ServiceType);
- snapshot.ModelName = config.ModelName ?? string.Empty;
- snapshot.EndpointUrl = config.EndpointUrl ?? string.Empty;
- snapshot.ApiVersion = config.ApiVersion ?? string.Empty;
- snapshot.DeploymentName = config.DeploymentName ?? string.Empty;
- snapshot.ModelPath = config.ModelPath ?? string.Empty;
- snapshot.SystemPrompt = config.SystemPrompt ?? string.Empty;
- snapshot.ModerationEnabled = config.ModerationEnabled;
- }
-
private static string RetrieveCredentialValue(string credentialResource, string credentialUserName)
{
if (string.IsNullOrWhiteSpace(credentialResource) || string.IsNullOrWhiteSpace(credentialUserName))