[AdvancedPaste] Enhance AI settings with custom endpoint, model name, and moderation options
Summary of the Pull Request
This Pull Request introduces enhanced configuration options for the Advanced Paste AI functionality. Users can now customize:
- A dedicated API endpoint
- Preferred model selection
- Content moderation preferences
These additions provide administrators and power users with greater control over AI integration within Advanced Paste.
PR Checklist
- [x] Communication: Feature request thoroughly discussed in GitHub Issue #32960
- [x] Tests: Minor change applied to existing tests, since some Service now depend on
UserSettings - [x] Localization: UI strings added to settings interface
- [ ] Developer Documentation: No changes needed
- [ ] Binary Updates: No new binaries included
- [ ] Documentation Updates: No documentation modifications
Implementation Details
The update extends Advanced Paste capabilities with three key configuration options:
- Custom API Endpoint: Enables specification of alternative API endpoints for self-hosted solutions, regional deployments, or proxy configurations.
- Model Selection: Supports explicit model designation for compatibility with various API-accessible AI models.
- Content Moderation: Provides granular control over moderation behavior to meet organizational compliance requirements.
These settings have been seamlessly integrated into the existing Advanced Paste configuration interface.
Related Issues
Validation Protocol
-
UI Validation:
- Verified proper rendering of new configuration fields
- Confirmed appropriate labeling and tooltip functionality
-
Configuration Persistence:
- Validated settings retention across application restarts
-
Default Configuration:
- Confirmed proper initialization of default values
-
Functional Testing:
- Maintained baseline functionality with default settings
- Successfully integrated with test endpoints
- Verified model specification behavior
- Validated moderation setting impacts
-
Localization Verification:
- Confirmed proper functionality in en-US locale
@microsoft-github-policy-service agree
@check-spelling-bot Report
:red_circle: Please review
See the :open_file_folder: files view, the :scroll:action log, or :memo: job summary for details.
Unrecognized words (3)
claude finetune generativelanguage
To accept these unrecognized words as correct, you could run the following commands
... in a clone of the [email protected]:sepcnt/PowerToys.git repository
on the main branch (:information_source: how do I use this?):
curl -s -S -L 'https://raw.githubusercontent.com/check-spelling/check-spelling/v0.0.24/apply.pl' |
perl - 'https://github.com/microsoft/PowerToys/actions/runs/14417081208/attempts/1'
Warnings (1)
See the :open_file_folder: files view, the :scroll:action log, or :memo: job summary for details.
| :warning: Warnings | Count |
|---|---|
| :warning: no-newline-at-eof | 1 |
See :warning: Event descriptions for more information.
If the flagged items are :exploding_head: false positives
If items relate to a ...
-
binary file (or some other file you wouldn't want to check at all).
Please add a file path to the
excludes.txtfile matching the containing file.File paths are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your files.
^refers to the file's path from the root of the repository, so^README\.md$would exclude README.md (on whichever branch you're using). -
well-formed pattern.
If you can write a pattern that would match it, try adding it to the
patterns.txtfile.Patterns are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your lines.
Note that patterns can't match multiline strings.
@sepcnt
- Can you please add some screenshots to the PR description.
- Do you think it is possible to implement Group Policies for company enforcement configuration. That would especially make sense for the compliance related settings. => Can part of a second PR.
@htcfreek
I think the following group policies could be added:
- Endpoint Enforcement
- Available model whitelist
- Moderation level
Additionally, some people have expressed concern about the risk of sending sensitive credentials to the cloud. Therefore, a local credential scanner could be added as a form of local moderation.
@htcfreek
I think the following group policies could be added:
- Endpoint Enforcement
- Available model whitelist
- Moderation level
Sounds good.
Additionally, some people have expressed concern about the risk of sending sensitive credentials to the cloud. Therefore, a local credential scanner could be added as a form of local moderation.
I think this is something that we can add later. No need to have in step one.
Where to add the api keys for custom endpoints? Same ui as with old behavior?
Where to add the api keys for custom endpoints? Same ui as with old behavior?
Yes, it is still in the credential vault.
maybe put the part with parameter settings (temperature, maximum response length) in some xml/json/txt config instead of a hardcode?
@wellmorq
Makes sense—this could be implemented in a separate PR. Features could include: multiple generation profiles (model name, parameters) with a profile selector and actions tied to specific profiles.
Hi @sepcat , thanks for the awesome contribution!
We're going to need some time to take a deeper look at this. This is because this feature deals with generative AI, and we have strict requirements at Microsoft to keep users safe from potential harmful content that could from this this (In other words, we take responsible AI very seriously!). This feature request is exciting but also carries a lot of potential safety risks (Users choosing an uncensored AI and getting harmful results from their clipboard) and security risks (Their AI endpoint being changed to something else without them knowing) and so we will need to go through internal responsible AI and security reviews to check on the feasibility of adding this. Again, thanks for the contribution and please let us know if you have any questions in the meantime.
@craigloewen-msft If this is based on compliance concerns, I think it's possible to move these features to Group Policy. Customizing endpoints is important for businesses and schools
@craigloewen-msft If this is based on compliance concerns, I think it's possible to move these features to Group Policy. Customizing endpoints is important for businesses and schools
Interesting idea. My two cents on this:
- We can make this GPO only settings and only show gpo configuration in ui.
- In the policy description we can add an warning hint that ms is not responsible for data leaks because of chosen unsave settings.
But I personally think if a users chooses unsave setting it is his/her problem and not the one of MS. So we can add a info bar in ui (severity warning or error) that we not recommend to change this and that using unsecure endpoint is a risk and hapoens by the decision of the user.
@check-spelling-bot Report
:red_circle: Please review
See the :open_file_folder: files view, the :scroll:action log, or :memo: job summary for details.
Unrecognized words (2)
hashcode milli
These words are not needed and should be removed
DEFT iextn localappdata pswd SHELLEXTENSION SHELLNEWVALUE SHGFIICON SHGFILARGEICONSome files were automatically ignored :see_no_evil:
These sample patterns would exclude them:
^\Q.pipelines/272MSSharedLibSN2048.snk\E$
You should consider adding them to:
.github/actions/spell-check/excludes.txt
File matching is via Perl regular expressions.
To check these files, more of their words need to be in the dictionary than not. You can use patterns.txt to exclude portions, add items to the dictionary (e.g. by adding them to allow.txt), or fix typos.
To accept these unrecognized words as correct, update file exclusions, and remove the previously acknowledged and now absent words, you could run the following commands
... in a clone of the [email protected]:sepcnt/PowerToys.git repository
on the main branch (:information_source: how do I use this?):
curl -s -S -L 'https://raw.githubusercontent.com/check-spelling/check-spelling/67debf50669c7fc76fc8f5d7f996384535a72b77/apply.pl' |
perl - 'https://github.com/microsoft/PowerToys/actions/runs/15106634436/attempts/1'
Errors (4)
See the :open_file_folder: files view, the :scroll:action log, or :memo: job summary for details.
| :x: Errors | Count |
|---|---|
| :warning: binary-file | 1 |
| :x: extra-dictionary-not-found | 1 |
| :x: ignored-expect-variant | 2 |
| :warning: no-newline-at-eof | 1 |
See :x: Event descriptions for more information.
If the flagged items are :exploding_head: false positives
If items relate to a ...
-
binary file (or some other file you wouldn't want to check at all).
Please add a file path to the
excludes.txtfile matching the containing file.File paths are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your files.
^refers to the file's path from the root of the repository, so^README\.md$would exclude README.md (on whichever branch you're using). -
well-formed pattern.
If you can write a pattern that would match it, try adding it to the
patterns.txtfile.Patterns are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your lines.
Note that patterns can't match multiline strings.
Another 1.5 months of waiting for the most useful feature went down the drain.
!!!!!!!!!!!!!!!!!!!!!
Great news, this has been approved from an RAI perspective! I will take a look at the code here to make sure I can add in the feedback from the RAI team at Microsoft and see if we can get this merged. Thank you all for your patience.
Here are the changes I think we'd need to make before we could merge this:
- Remove the 'disable Moderation' button and just disable moderation by default when using a custom model
- Change the Disable / Enable AI button logic to be a drop down and have 'Local model' be an option
- Here's a mock up:
-
- Have the 'Advanced Settings' group disable if 'Local model' is not selected
- If the user selects 'Local model' show different input text warning them about the specific hazards of using a local model
- Sample UX
-
- When local model is enabled modify the main UI to clearly show that it's enabled. Maybe we can highlight it yellow or something to make it clear it's running in that capacity
-
- Ensure that if the local model URL or model name is set to wrong values that we get good error values (Would just want to make sure nothing esoteric or hard to debug / understand shows up if you misspell your endpoint)
- Will need to update the docs with a bit about local models as well. I can own this.
Regarding the change to a drop down (Enabled, OpenAI, Custom):
We need to evaluate how we can reflect this in the gpos in a good way and hopefully without breaking something.
I can imagine a drop down there to that references the currently used registry value with the following numbers: 0=disabled, 1=OpenAI, 2=Custom.
Or we use a new reg value name, mark the existing policy as ourdated and communicate that there is a breaking change.
Policies for custom endpoint We need the following three:
- Endpoint uri
- Endpoint name
- Enforce moderation
The key can't be available as policy because we can't communicate in in a dave way from management to client.
Is there still no official way to change the endpoint for this? I am eagerly awaiting this feature!
When local model is enabled modify the main UI to clearly show that it's enabled
I personally resent the notion the user needs to be reminded in the main (day-to-day usage) UI that they are using it in what is considered a custom fashion. Applications should not throw more excessive warning text at you when you try to make them work better for you.
The appended warning in the paste UI stating "please verify all answers" is close enough to the line above it should be presented as a single line that says something to the effect of: "AI can make mistakes, check info for accuracy.". You have no standardized way to pull the terms/privacy policy of a custom endpoint so youll have to settle for just the text warning. Notice that the UI does not mention a custom model is selected. The user knows that. The user changed that setting. The user is not stupid. The user does not to be reminded they are not using microsoft edge as their default browser.
Furthermore, the warning text that is included in the settings UI seems appropriate save for that it focuses solely on that output from the custom model may be unreliable. This is fine, but you should also mention there is a commensurate risk in transmitting whatever is on the clipboard to whatever custom endpoint is defined as a privacy risk.
(Whether the microsoft terms/privacy policy links still need be presented to the user despite them using a custom model is a legal requirement im not familiar with)
Glad to see theres some movement on this though.
I personally resent the notion the user needs to be reminded in the main (day-to-day usage) UI that they are using it in what is considered a custom fashion. Applications should not throw more excessive warning text at you when you try to make them work better for you.
This isn't about being patronising or excessive, but ensuring that the user is adequately informed when a custom endpoint is being used. Not everyone will be using a custom service as their daily driver. It could be that they use it for certain local-only tasks with private data, and the message would then serve as an important reminder for their peace of mind. It saves them having to check the drop-down to confirm.
In terms of the UI, could I suggest splitting the message into:
"AI can make mistakes. Please verify all answers. Terms | Privacy" (Please verify message always visible no matter the endpoint type.)
and:
"⚠️ You are using a custom model endpoint. More information" (When the custom endpoint is used, with the more info link leading to the updated docs on the types of endpoints available and the corresponding risks).
For anyone interest with this feature, can start with following patch (verified from commit https://github.com/microsoft/PowerToys/commit/da36d410e3c4f35920ccabca07bc75f7286d8c79).
Previous Version:
diff --git a/Directory.Packages.props b/Directory.Packages.props
index a5270e7d7..b6fa9d18f 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -8,7 +8,7 @@
<PackageVersion Include="AdaptiveCards.Templating" Version="2.0.5" />
<PackageVersion Include="Microsoft.Bot.AdaptiveExpressions.Core" Version="4.23.0" />
<PackageVersion Include="Appium.WebDriver" Version="4.4.5" />
- <PackageVersion Include="Azure.AI.OpenAI" Version="1.0.0-beta.17" />
+ <PackageVersion Include="Azure.AI.OpenAI" Version="2.2.0-beta.4" />
<PackageVersion Include="CoenM.ImageSharp.ImageHash" Version="1.3.6" />
<PackageVersion Include="CommunityToolkit.Common" Version="8.4.0" />
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.0" />
@@ -43,7 +43,7 @@
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.8" />
- <PackageVersion Include="Microsoft.SemanticKernel" Version="1.15.0" />
+ <PackageVersion Include="Microsoft.SemanticKernel" Version="1.46.0" />
<PackageVersion Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.2" />
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.2903.40" />
<!-- Package Microsoft.Win32.SystemEvents added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Drawing.Common but the 8.0.1 version wasn't published to nuget. -->
@@ -69,7 +69,7 @@
<PackageVersion Include="NLog" Version="5.2.8" />
<PackageVersion Include="NLog.Extensions.Logging" Version="5.3.8" />
<PackageVersion Include="NLog.Schema" Version="5.2.8" />
- <PackageVersion Include="OpenAI" Version="2.0.0" />
+ <PackageVersion Include="OpenAI" Version="2.2.0-beta.4" />
<PackageVersion Include="ReverseMarkdown" Version="4.1.0" />
<PackageVersion Include="RtfPipe" Version="2.0.7677.4303" />
<PackageVersion Include="ScipBe.Common.Office.OneNote" Version="3.0.1" />
diff --git a/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/AIServiceBatchIntegrationTests.cs b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/AIServiceBatchIntegrationTests.cs
index 3782b057f..1f9765104 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/AIServiceBatchIntegrationTests.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/AIServiceBatchIntegrationTests.cs
@@ -14,9 +14,11 @@ using System.Threading.Tasks;
using AdvancedPaste.Helpers;
using AdvancedPaste.Models;
using AdvancedPaste.Services.OpenAI;
+using AdvancedPaste.Settings;
using AdvancedPaste.UnitTests.Mocks;
using ManagedCommon;
using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
using Windows.ApplicationModel.DataTransfer;
namespace AdvancedPaste.UnitTests.ServicesTests;
@@ -130,10 +132,11 @@ public sealed class AIServiceBatchIntegrationTests
private static async Task<DataPackage> GetOutputDataPackageAsync(BatchTestInput batchTestInput, PasteFormats format)
{
+ Mock<IUserSettings> userSettings = new();
VaultCredentialsProvider credentialsProvider = new();
- PromptModerationService promptModerationService = new(credentialsProvider);
+ PromptModerationService promptModerationService = new(userSettings.Object, credentialsProvider);
NoOpProgress progress = new();
- CustomTextTransformService customTextTransformService = new(credentialsProvider, promptModerationService);
+ CustomTextTransformService customTextTransformService = new(userSettings.Object, credentialsProvider, promptModerationService);
switch (format)
{
@@ -142,7 +145,7 @@ public sealed class AIServiceBatchIntegrationTests
case PasteFormats.KernelQuery:
var clipboardData = DataPackageHelpers.CreateFromText(batchTestInput.Clipboard).GetView();
- KernelService kernelService = new(new NoOpKernelQueryCacheService(), credentialsProvider, promptModerationService, customTextTransformService);
+ KernelService kernelService = new(userSettings.Object, new NoOpKernelQueryCacheService(), credentialsProvider, promptModerationService, customTextTransformService);
return await kernelService.TransformClipboardAsync(batchTestInput.Prompt, clipboardData, isSavedQuery: false, CancellationToken.None, progress);
default:
diff --git a/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/KernelServiceIntegrationTests.cs b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/KernelServiceIntegrationTests.cs
index 998534cf5..cc45be272 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/KernelServiceIntegrationTests.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/KernelServiceIntegrationTests.cs
@@ -12,10 +12,12 @@ using System.Threading.Tasks;
using AdvancedPaste.Helpers;
using AdvancedPaste.Models;
using AdvancedPaste.Services.OpenAI;
+using AdvancedPaste.Settings;
using AdvancedPaste.Telemetry;
using AdvancedPaste.UnitTests.Mocks;
using AdvancedPaste.UnitTests.Utils;
using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
using Windows.ApplicationModel.DataTransfer;
namespace AdvancedPaste.UnitTests.ServicesTests;
@@ -29,14 +31,17 @@ public sealed class KernelServiceIntegrationTests : IDisposable
private const string StandardImageFile = "image_with_text_example.png";
private KernelService _kernelService;
private AdvancedPasteEventListener _eventListener;
+ private Mock<IUserSettings> _userSettings;
[TestInitialize]
public void TestInitialize()
{
+ _userSettings = new();
VaultCredentialsProvider credentialsProvider = new();
- PromptModerationService promptModerationService = new(credentialsProvider);
+ PromptModerationService promptModerationService = new(_userSettings.Object, credentialsProvider);
+ CustomTextTransformService customTextTransformService = new(_userSettings.Object, credentialsProvider, promptModerationService);
- _kernelService = new KernelService(new NoOpKernelQueryCacheService(), credentialsProvider, promptModerationService, new CustomTextTransformService(credentialsProvider, promptModerationService));
+ _kernelService = new KernelService(_userSettings.Object, new NoOpKernelQueryCacheService(), credentialsProvider, promptModerationService, customTextTransformService);
_eventListener = new();
}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/IUserSettings.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/IUserSettings.cs
index 105fe2c0d..94080f76e 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/IUserSettings.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/IUserSettings.cs
@@ -14,6 +14,12 @@ namespace AdvancedPaste.Settings
{
public bool IsAdvancedAIEnabled { get; }
+ public string CustomEndpoint { get; }
+
+ public string CustomModelName { get; }
+
+ public bool DisableModeration { get; }
+
public bool ShowCustomPreview { get; }
public bool CloseAfterLosingFocus { get; }
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs
index 8a25b70f0..962ba9053 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs
@@ -35,6 +35,12 @@ namespace AdvancedPaste.Settings
public bool IsAdvancedAIEnabled { get; private set; }
+ public string CustomEndpoint { get; private set; }
+
+ public string CustomModelName { get; private set; }
+
+ public bool DisableModeration { get; private set; }
+
public bool ShowCustomPreview { get; private set; }
public bool CloseAfterLosingFocus { get; private set; }
@@ -48,6 +54,9 @@ namespace AdvancedPaste.Settings
_settingsUtils = new SettingsUtils(fileSystem);
IsAdvancedAIEnabled = false;
+ CustomEndpoint = string.Empty;
+ CustomModelName = string.Empty;
+ DisableModeration = false;
ShowCustomPreview = true;
CloseAfterLosingFocus = false;
_additionalActions = [];
@@ -99,6 +108,9 @@ namespace AdvancedPaste.Settings
var properties = settings.Properties;
IsAdvancedAIEnabled = properties.IsAdvancedAIEnabled;
+ CustomEndpoint = properties.CustomEndpoint;
+ CustomModelName = properties.CustomModelName;
+ DisableModeration = properties.DisableModeration;
ShowCustomPreview = properties.ShowCustomPreview;
CloseAfterLosingFocus = properties.CloseAfterLosingFocus;
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/CustomTextTransformService.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/CustomTextTransformService.cs
index b6aa156b9..fbae0c033 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/CustomTextTransformService.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/CustomTextTransformService.cs
@@ -3,49 +3,57 @@
// See the LICENSE file in the project root for more information.
using System;
+using System.ClientModel;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
using AdvancedPaste.Models;
+using AdvancedPaste.Settings;
using AdvancedPaste.Telemetry;
-using Azure;
-using Azure.AI.OpenAI;
using ManagedCommon;
using Microsoft.PowerToys.Telemetry;
+using OpenAI;
+using OpenAI.Chat;
namespace AdvancedPaste.Services.OpenAI;
-public sealed class CustomTextTransformService(IAICredentialsProvider aiCredentialsProvider, IPromptModerationService promptModerationService) : ICustomTextTransformService
+public sealed class CustomTextTransformService(IUserSettings userSettings, IAICredentialsProvider aiCredentialsProvider, IPromptModerationService promptModerationService) : ICustomTextTransformService
{
- private const string ModelName = "gpt-3.5-turbo-instruct";
+ private readonly IUserSettings _userSettings = userSettings;
+
+ private string ModelName => string.IsNullOrEmpty(_userSettings.CustomModelName) ? "gpt-3.5-turbo-instruct" : _userSettings.CustomModelName;
private readonly IAICredentialsProvider _aiCredentialsProvider = aiCredentialsProvider;
private readonly IPromptModerationService _promptModerationService = promptModerationService;
- private async Task<Completions> GetAICompletionAsync(string systemInstructions, string userMessage, CancellationToken cancellationToken)
+ private async Task<ChatCompletion> GetAICompletionAsync(string systemInstructions, string userMessage, CancellationToken cancellationToken)
{
var fullPrompt = systemInstructions + "\n\n" + userMessage;
-
await _promptModerationService.ValidateAsync(fullPrompt, cancellationToken);
- OpenAIClient azureAIClient = new(_aiCredentialsProvider.Key);
+ OpenAIClientOptions clientOptions = new();
+ if (!string.IsNullOrEmpty(_userSettings.CustomEndpoint))
+ {
+ clientOptions.Endpoint = new Uri(_userSettings.CustomEndpoint);
+ }
+
+ OpenAIClient openAIClient = new(new ApiKeyCredential(_aiCredentialsProvider.Key), clientOptions);
- var response = await azureAIClient.GetCompletionsAsync(
+ var response = await openAIClient.GetChatClient(ModelName).CompleteChatAsync(
+ [
+ new SystemChatMessage(systemInstructions),
+ new SystemChatMessage(userMessage)
+ ],
new()
{
- DeploymentName = ModelName,
- Prompts =
- {
- fullPrompt,
- },
Temperature = 0.01F,
- MaxTokens = 2000,
+ MaxOutputTokenCount = 2000,
},
cancellationToken);
- if (response.Value.Choices[0].FinishReason == "length")
+ if (response.Value.FinishReason == ChatFinishReason.Length)
{
Logger.LogDebug("Cut off due to length constraints");
}
@@ -85,13 +93,13 @@ Output:
var response = await GetAICompletionAsync(systemInstructions, userMessage, cancellationToken);
var usage = response.Usage;
- AdvancedPasteGenerateCustomFormatEvent telemetryEvent = new(usage.PromptTokens, usage.CompletionTokens, ModelName);
+ AdvancedPasteGenerateCustomFormatEvent telemetryEvent = new(usage.InputTokenCount, usage.OutputTokenCount, ModelName);
PowerToysTelemetry.Log.WriteEvent(telemetryEvent);
var logEvent = new AIServiceFormatEvent(telemetryEvent);
Logger.LogDebug($"{nameof(TransformTextAsync)} complete; {logEvent.ToJsonString()}");
- return response.Choices[0].Text;
+ return response.Content[0].Text;
}
catch (Exception ex)
{
@@ -106,7 +114,7 @@ Output:
}
else
{
- throw new PasteActionException(ErrorHelpers.TranslateErrorText((ex as RequestFailedException)?.Status ?? -1), ex);
+ throw new PasteActionException(ErrorHelpers.TranslateErrorText((ex as ClientResultException)?.Status ?? -1), ex);
}
}
}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/KernelService.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/KernelService.cs
index b19a6d51c..a091a0dad 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/KernelService.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/KernelService.cs
@@ -2,21 +2,25 @@
// 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 AdvancedPaste.Models;
-using Azure.AI.OpenAI;
+using AdvancedPaste.Settings;
+
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.OpenAI;
+using ChatTokenUsage = OpenAI.Chat.ChatTokenUsage;
namespace AdvancedPaste.Services.OpenAI;
-public sealed class KernelService(IKernelQueryCacheService queryCacheService, IAICredentialsProvider aiCredentialsProvider, IPromptModerationService promptModerationService, ICustomTextTransformService customTextTransformService) :
+public sealed class KernelService(IUserSettings userSettings, IKernelQueryCacheService queryCacheService, IAICredentialsProvider aiCredentialsProvider, IPromptModerationService promptModerationService, ICustomTextTransformService customTextTransformService) :
KernelServiceBase(queryCacheService, promptModerationService, customTextTransformService)
{
+ private readonly IUserSettings _userSettings = userSettings;
private readonly IAICredentialsProvider _aiCredentialsProvider = aiCredentialsProvider;
- protected override string ModelName => "gpt-4o";
+ protected override string ModelName => string.IsNullOrEmpty(_userSettings.CustomModelName) ? "gpt-4o" : _userSettings.CustomModelName;
protected override PromptExecutionSettings PromptExecutionSettings =>
new OpenAIPromptExecutionSettings()
@@ -25,10 +29,20 @@ public sealed class KernelService(IKernelQueryCacheService queryCacheService, IA
Temperature = 0.01,
};
- protected override void AddChatCompletionService(IKernelBuilder kernelBuilder) => kernelBuilder.AddOpenAIChatCompletion(ModelName, _aiCredentialsProvider.Key);
+ protected override void AddChatCompletionService(IKernelBuilder kernelBuilder)
+ {
+ if (string.IsNullOrEmpty(_userSettings.CustomEndpoint))
+ {
+ kernelBuilder.AddOpenAIChatCompletion(ModelName, _aiCredentialsProvider.Key);
+ }
+ else
+ {
+ kernelBuilder.AddOpenAIChatCompletion(ModelName, new Uri(_userSettings.CustomEndpoint), _aiCredentialsProvider.Key);
+ }
+ }
protected override AIServiceUsage GetAIServiceUsage(ChatMessageContent chatMessage) =>
- chatMessage.Metadata?.GetValueOrDefault("Usage") is CompletionsUsage completionsUsage
- ? new(PromptTokens: completionsUsage.PromptTokens, CompletionTokens: completionsUsage.CompletionTokens)
+ chatMessage.Metadata?.GetValueOrDefault("Usage") is ChatTokenUsage completionsUsage
+ ? new(PromptTokens: completionsUsage.InputTokenCount, CompletionTokens: completionsUsage.OutputTokenCount)
: AIServiceUsage.None;
}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/PromptModerationService.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/PromptModerationService.cs
index 0ca15e416..a1058874a 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/PromptModerationService.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/PromptModerationService.cs
@@ -2,28 +2,45 @@
// 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.ClientModel;
using System.Threading;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
using AdvancedPaste.Models;
+using AdvancedPaste.Settings;
using ManagedCommon;
+using OpenAI;
using OpenAI.Moderations;
namespace AdvancedPaste.Services.OpenAI;
-public sealed class PromptModerationService(IAICredentialsProvider aiCredentialsProvider) : IPromptModerationService
+public sealed class PromptModerationService(IUserSettings userSettings, IAICredentialsProvider aiCredentialsProvider) : IPromptModerationService
{
+ private readonly IUserSettings _userSettings = userSettings;
+
private const string ModelName = "omni-moderation-latest";
private readonly IAICredentialsProvider _aiCredentialsProvider = aiCredentialsProvider;
public async Task ValidateAsync(string fullPrompt, CancellationToken cancellationToken)
{
+ if (_userSettings.DisableModeration)
+ {
+ Logger.LogDebug($"{nameof(PromptModerationService)}.{nameof(ValidateAsync)} skipped; moderation is disabled");
+ return;
+ }
+
try
{
- ModerationClient moderationClient = new(ModelName, _aiCredentialsProvider.Key);
+ OpenAIClientOptions clientOptions = new();
+ if (!string.IsNullOrEmpty(_userSettings.CustomEndpoint))
+ {
+ clientOptions.Endpoint = new Uri(_userSettings.CustomEndpoint);
+ }
+
+ ModerationClient moderationClient = new(ModelName, new(_aiCredentialsProvider.Key), clientOptions);
var moderationClientResult = await moderationClient.ClassifyTextAsync(fullPrompt, cancellationToken);
var moderationResult = moderationClientResult.Value;
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs b/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs
index 688c3047e..3a0cdac9e 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs
@@ -85,6 +85,12 @@ namespace AdvancedPaste.ViewModels
public bool IsAdvancedAIEnabled => IsCustomAIServiceEnabled && _userSettings.IsAdvancedAIEnabled;
+ public string CustomEndpoint => _userSettings.CustomEndpoint;
+
+ public string CustomModelName => _userSettings.CustomModelName;
+
+ public bool DisableModeration => _userSettings.DisableModeration;
+
public bool ClipboardHasData => AvailableClipboardFormats != ClipboardFormat.None;
public bool ClipboardHasDataForCustomAI => PasteFormat.SupportsClipboardFormats(CustomAIFormat, AvailableClipboardFormats);
@@ -167,6 +173,9 @@ namespace AdvancedPaste.ViewModels
OnPropertyChanged(nameof(ClipboardHasDataForCustomAI));
OnPropertyChanged(nameof(IsCustomAIAvailable));
OnPropertyChanged(nameof(IsAdvancedAIEnabled));
+ OnPropertyChanged(nameof(CustomEndpoint));
+ OnPropertyChanged(nameof(CustomModelName));
+ OnPropertyChanged(nameof(DisableModeration));
EnqueueRefreshPasteFormats();
}
@@ -275,6 +284,9 @@ namespace AdvancedPaste.ViewModels
OnPropertyChanged(nameof(CustomAIUnavailableErrorText));
OnPropertyChanged(nameof(IsCustomAIServiceEnabled));
OnPropertyChanged(nameof(IsAdvancedAIEnabled));
+ OnPropertyChanged(nameof(CustomEndpoint));
+ OnPropertyChanged(nameof(CustomModelName));
+ OnPropertyChanged(nameof(DisableModeration));
OnPropertyChanged(nameof(IsCustomAIAvailable));
});
}
diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteProperties.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteProperties.cs
index d40bd686d..384470a1f 100644
--- a/src/settings-ui/Settings.UI.Library/AdvancedPasteProperties.cs
+++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteProperties.cs
@@ -24,6 +24,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library
CustomActions = new();
AdditionalActions = new();
IsAdvancedAIEnabled = false;
+ CustomEndpoint = string.Empty;
+ CustomModelName = string.Empty;
+ DisableModeration = false;
ShowCustomPreview = true;
CloseAfterLosingFocus = false;
}
@@ -31,6 +34,13 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonConverter(typeof(BoolPropertyJsonConverter))]
public bool IsAdvancedAIEnabled { get; set; }
+ public string CustomEndpoint { get; set; }
+
+ public string CustomModelName { get; set; }
+
+ [JsonConverter(typeof(BoolPropertyJsonConverter))]
+ public bool DisableModeration { get; set; }
+
[JsonConverter(typeof(BoolPropertyJsonConverter))]
public bool ShowCustomPreview { get; set; }
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml
index 7aa817662..0b876a63c 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml
@@ -101,6 +101,26 @@
IsEnabled="{x:Bind ViewModel.IsOpenAIEnabled, Mode=OneWay}">
<ToggleSwitch IsOn="{x:Bind ViewModel.IsAdvancedAIEnabled, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
+ <tkcontrols:SettingsExpander
+ x:Uid="AdvancedPaste_AdvancedSettings"
+ HeaderIcon="{ui:FontIcon Glyph=}"
+ IsEnabled="{x:Bind ViewModel.IsOpenAIEnabled, Mode=OneWay}"
+ IsExpanded="False">
+ <tkcontrols:SettingsExpander.Items>
+ <tkcontrols:SettingsCard
+ x:Uid="AdvancedPaste_AdvancedSettings_CustomEndpoint">
+ <TextBox Text="{x:Bind ViewModel.CustomEndpoint, Mode=TwoWay}" />
+ </tkcontrols:SettingsCard>
+ <tkcontrols:SettingsCard
+ x:Uid="AdvancedPaste_AdvancedSettings_CustomModelName">
+ <TextBox Text="{x:Bind ViewModel.CustomModelName, Mode=TwoWay}"/>
+ </tkcontrols:SettingsCard>
+ <tkcontrols:SettingsCard
+ x:Uid="AdvancedPaste_AdvancedSettings_DisableModeration">
+ <ToggleSwitch IsOn="{x:Bind ViewModel.DisableModeration, Mode=TwoWay}" />
+ </tkcontrols:SettingsCard>
+ </tkcontrols:SettingsExpander.Items>
+ </tkcontrols:SettingsExpander>
</controls:SettingsGroup>
<controls:SettingsGroup x:Uid="AdvancedPaste_BehaviorSettingsGroup" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
diff --git a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
index 38ae16b69..7b39443a4 100644
--- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
+++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
@@ -598,6 +598,30 @@
<data name="AdvancedPaste_Additional_Actions_GroupSettings.Header" xml:space="preserve">
<value>Additional actions</value>
</data>
+ <data name="AdvancedPaste_AdvancedSettings.Header" xml:space="preserve">
+ <value>Advanced settings</value>
+ </data>
+ <data name="AdvancedPaste_AdvancedSettings.Description" xml:space="preserve">
+ <value>These settings allow you to connect to self-hosted models or alternative providers, which are compatible with the OpenAI API specification.</value>
+ </data>
+ <data name="AdvancedPaste_AdvancedSettings_CustomEndpoint.Header" xml:space="preserve">
+ <value>Custom endpoint</value>
+ </data>
+ <data name="AdvancedPaste_AdvancedSettings_CustomEndpoint.Description" xml:space="preserve">
+ <value>Enter the base URL of the OpenAI API-compatible service you wish to use (e.g., https://generativelanguage.googleapis.com/v1beta or https://api.anthropic.com/v1). This will override the default endpoint.</value>
+ </data>
+ <data name="AdvancedPaste_AdvancedSettings_CustomModelName.Header" xml:space="preserve">
+ <value>Custom model name</value>
+ </data>
+ <data name="AdvancedPaste_AdvancedSettings_CustomModelName.Description" xml:space="preserve">
+ <value>Specify the exact model identifier that your custom endpoint expects (e.g., claude-3-7-sonnet, gemini-2.5-flash, my-custom-finetune). Consult your provider's documentation for the correct name.</value>
+ </data>
+ <data name="AdvancedPaste_AdvancedSettings_DisableModeration.Header" xml:space="preserve">
+ <value>Disable moderation</value>
+ </data>
+ <data name="AdvancedPaste_AdvancedSettings_DisableModeration.Description" xml:space="preserve">
+ <value>Disable the built-in moderation checks for the selected model. This is not recommended unless you are using a custom model that has its own moderation capabilities.</value>
+ </data>
<data name="RemapKeysList.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Current Key Remappings</value>
</data>
diff --git a/src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs
index 0fdf2ca94..291dee04d 100644
--- a/src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs
+++ b/src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs
@@ -361,6 +361,47 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
+ public string CustomEndpoint
+ {
+ get => _advancedPasteSettings.Properties.CustomEndpoint;
+ set
+ {
+ if (value != _advancedPasteSettings.Properties.CustomEndpoint)
+ {
+ _advancedPasteSettings.Properties.CustomEndpoint = value;
+ OnPropertyChanged(nameof(CustomEndpoint));
+ NotifySettingsChanged();
+ }
+ }
+ }
+
+ public string CustomModelName
+ {
+ get => _advancedPasteSettings.Properties.CustomModelName;
+ set
+ {
+ if (value != _advancedPasteSettings.Properties.CustomModelName)
+ {
+ _advancedPasteSettings.Properties.CustomModelName = value;
+ OnPropertyChanged(nameof(CustomModelName));
+ NotifySettingsChanged();
+ }
+ }
+ }
+
+ public bool DisableModeration
+ {
+ get => _advancedPasteSettings.Properties.DisableModeration;
+ set
+ {
+ if (value != _advancedPasteSettings.Properties.DisableModeration)
+ {
+ _advancedPasteSettings.Properties.DisableModeration = value;
+ NotifySettingsChanged();
+ }
+ }
+ }
+
public bool ShowCustomPreview
{
get => _advancedPasteSettings.Properties.ShowCustomPreview;
WIP Version (should be applied after the above one):
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Pages/MainPage.xaml b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Pages/MainPage.xaml
index 9c439372c..e36d3dc29 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Pages/MainPage.xaml
+++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Pages/MainPage.xaml
@@ -153,49 +153,60 @@
x:FieldModifier="public"
TabIndex="0">
<controls:PromptBox.Footer>
- <StackPanel Orientation="Horizontal">
- <TextBlock
- Margin="0,0,2,0"
- HorizontalAlignment="Left"
- VerticalAlignment="Center"
- Style="{StaticResource CaptionTextBlockStyle}">
- <Run x:Uid="AIMistakeNote" Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
- </TextBlock>
- <TextBlock
- Margin="4,0,2,0"
- HorizontalAlignment="Left"
- VerticalAlignment="Center"
- Style="{StaticResource CaptionTextBlockStyle}">
- <Hyperlink
- x:Name="TermsHyperlink"
- NavigateUri="https://openai.com/policies/terms-of-use"
- TabIndex="3">
- <Run x:Uid="TermsLink" />
- </Hyperlink>
- <ToolTipService.ToolTip>
- <TextBlock Text="https://openai.com/policies/terms-of-use" />
- </ToolTipService.ToolTip>
- </TextBlock>
- <TextBlock
- Margin="0,0,2,0"
- HorizontalAlignment="Left"
- VerticalAlignment="Center"
- Style="{StaticResource CaptionTextBlockStyle}"
- ToolTipService.ToolTip="">
- <Run x:Uid="AIFooterSeparator" Foreground="{ThemeResource TextFillColorSecondaryBrush}">|</Run>
- </TextBlock>
- <TextBlock
- Margin="0,0,2,0"
- HorizontalAlignment="Left"
- VerticalAlignment="Center"
- Style="{StaticResource CaptionTextBlockStyle}">
- <Hyperlink NavigateUri="https://openai.com/policies/privacy-policy" TabIndex="3">
- <Run x:Uid="PrivacyLink" />
- </Hyperlink>
- <ToolTipService.ToolTip>
- <TextBlock Text="https://openai.com/policies/privacy-policy" />
- </ToolTipService.ToolTip>
- </TextBlock>
+ <StackPanel Orientation="Vertical" Spacing="2">
+ <StackPanel Orientation="Horizontal">
+ <TextBlock
+ Margin="0,0,2,0"
+ HorizontalAlignment="Left"
+ VerticalAlignment="Center"
+ Style="{StaticResource CaptionTextBlockStyle}">
+ <Run x:Uid="AIMistakeNote" Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
+ </TextBlock>
+ <TextBlock
+ Margin="4,0,2,0"
+ HorizontalAlignment="Left"
+ VerticalAlignment="Center"
+ Style="{StaticResource CaptionTextBlockStyle}">
+ <Hyperlink
+ x:Name="TermsHyperlink"
+ NavigateUri="https://openai.com/policies/terms-of-use"
+ TabIndex="3">
+ <Run x:Uid="TermsLink" />
+ </Hyperlink>
+ <ToolTipService.ToolTip>
+ <TextBlock Text="https://openai.com/policies/terms-of-use" />
+ </ToolTipService.ToolTip>
+ </TextBlock>
+ <TextBlock
+ Margin="0,0,2,0"
+ HorizontalAlignment="Left"
+ VerticalAlignment="Center"
+ Style="{StaticResource CaptionTextBlockStyle}"
+ ToolTipService.ToolTip="">
+ <Run x:Uid="AIFooterSeparator" Foreground="{ThemeResource TextFillColorSecondaryBrush}">|</Run>
+ </TextBlock>
+ <TextBlock
+ Margin="0,0,2,0"
+ HorizontalAlignment="Left"
+ VerticalAlignment="Center"
+ Style="{StaticResource CaptionTextBlockStyle}">
+ <Hyperlink NavigateUri="https://openai.com/policies/privacy-policy" TabIndex="3">
+ <Run x:Uid="PrivacyLink" />
+ </Hyperlink>
+ <ToolTipService.ToolTip>
+ <TextBlock Text="https://openai.com/policies/privacy-policy" />
+ </ToolTipService.ToolTip>
+ </TextBlock>
+ </StackPanel>
+ <StackPanel Orientation="Horizontal" Visibility="{x:Bind ViewModel.IsLocalModelMode, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
+ <TextBlock
+ Margin="0,0,2,0"
+ HorizontalAlignment="Left"
+ VerticalAlignment="Center"
+ Style="{StaticResource CaptionTextBlockStyle}">
+ <Run x:Uid="CustomAIMistakeNote"/>
+ </TextBlock>
+ </StackPanel>
</StackPanel>
</controls:PromptBox.Footer>
</controls:PromptBox>
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Pages/MainPage.xaml.cs b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Pages/MainPage.xaml.cs
index 9c4ac5cc7..54f4fe759 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Pages/MainPage.xaml.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Pages/MainPage.xaml.cs
@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
+using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
@@ -22,19 +23,24 @@ using Windows.System;
namespace AdvancedPaste.Pages
{
- public sealed partial class MainPage : Page
+ public sealed partial class MainPage : Page, INotifyPropertyChanged
{
private readonly ObservableCollection<ClipboardItem> clipboardHistory;
private readonly Microsoft.UI.Dispatching.DispatcherQueue _dispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread();
private (VirtualKey Key, DateTime Timestamp) _lastKeyEvent = (VirtualKey.None, DateTime.MinValue);
+ public event PropertyChangedEventHandler PropertyChanged;
+
public OptionsViewModel ViewModel { get; private set; }
+ public bool IsLocalModelModeVisible => ViewModel?.IsLocalModelMode == true;
+
public MainPage()
{
this.InitializeComponent();
ViewModel = App.GetService<OptionsViewModel>();
+ ViewModel.PropertyChanged += ViewModel_PropertyChanged;
clipboardHistory = new ObservableCollection<ClipboardItem>();
@@ -42,6 +48,19 @@ namespace AdvancedPaste.Pages
Clipboard.HistoryChanged += LoadClipboardHistoryEvent;
}
+ private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName == nameof(OptionsViewModel.IsLocalModelMode))
+ {
+ OnPropertyChanged(nameof(IsLocalModelModeVisible));
+ }
+ }
+
+ private void OnPropertyChanged(string propertyName)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+
private void LoadClipboardHistoryEvent(object sender, object e)
{
Task.Run(() =>
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/IUserSettings.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/IUserSettings.cs
index 94080f76e..49d87e71d 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/IUserSettings.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/IUserSettings.cs
@@ -14,6 +14,10 @@ namespace AdvancedPaste.Settings
{
public bool IsAdvancedAIEnabled { get; }
+ public AdvancedPasteAIMode AIMode { get; }
+
+ public bool IsLocalModelMode { get; }
+
public string CustomEndpoint { get; }
public string CustomModelName { get; }
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs
index 962ba9053..cae916d83 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs
@@ -35,6 +35,10 @@ namespace AdvancedPaste.Settings
public bool IsAdvancedAIEnabled { get; private set; }
+ public AdvancedPasteAIMode AIMode { get; private set; }
+
+ public bool IsLocalModelMode => AIMode == AdvancedPasteAIMode.LocalModel;
+
public string CustomEndpoint { get; private set; }
public string CustomModelName { get; private set; }
@@ -54,6 +58,7 @@ namespace AdvancedPaste.Settings
_settingsUtils = new SettingsUtils(fileSystem);
IsAdvancedAIEnabled = false;
+ AIMode = AdvancedPasteAIMode.Disabled;
CustomEndpoint = string.Empty;
CustomModelName = string.Empty;
DisableModeration = false;
@@ -108,6 +113,31 @@ namespace AdvancedPaste.Settings
var properties = settings.Properties;
IsAdvancedAIEnabled = properties.IsAdvancedAIEnabled;
+
+ // Handle backwards compatibility for AIMode
+ if (properties.AIMode == AdvancedPasteAIMode.Disabled)
+ {
+ // Check if user has custom endpoint/model configured (local model mode)
+ if (!string.IsNullOrWhiteSpace(properties.CustomEndpoint) || !string.IsNullOrWhiteSpace(properties.CustomModelName))
+ {
+ AIMode = AdvancedPasteAIMode.LocalModel;
+ }
+
+ // Check if user has OpenAI key configured
+ else if (IsOpenAIKeyConfigured())
+ {
+ AIMode = AdvancedPasteAIMode.OpenAI;
+ }
+ else
+ {
+ AIMode = AdvancedPasteAIMode.Disabled;
+ }
+ }
+ else
+ {
+ AIMode = properties.AIMode;
+ }
+
CustomEndpoint = properties.CustomEndpoint;
CustomModelName = properties.CustomModelName;
DisableModeration = properties.DisableModeration;
@@ -156,6 +186,20 @@ namespace AdvancedPaste.Settings
}
}
+ private static bool IsOpenAIKeyConfigured()
+ {
+ try
+ {
+ var vault = new Windows.Security.Credentials.PasswordVault();
+ vault.Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey");
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
public void Dispose()
{
Dispose(true);
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/CustomTextTransformService.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/CustomTextTransformService.cs
index fbae0c033..ecf1518fc 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/CustomTextTransformService.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/CustomTextTransformService.cs
@@ -23,7 +23,7 @@ public sealed class CustomTextTransformService(IUserSettings userSettings, IAICr
{
private readonly IUserSettings _userSettings = userSettings;
- private string ModelName => string.IsNullOrEmpty(_userSettings.CustomModelName) ? "gpt-3.5-turbo-instruct" : _userSettings.CustomModelName;
+ private string ModelName => string.IsNullOrWhiteSpace(_userSettings.CustomModelName) ? "gpt-3.5-turbo-instruct" : _userSettings.CustomModelName;
private readonly IAICredentialsProvider _aiCredentialsProvider = aiCredentialsProvider;
private readonly IPromptModerationService _promptModerationService = promptModerationService;
@@ -34,9 +34,14 @@ public sealed class CustomTextTransformService(IUserSettings userSettings, IAICr
await _promptModerationService.ValidateAsync(fullPrompt, cancellationToken);
OpenAIClientOptions clientOptions = new();
- if (!string.IsNullOrEmpty(_userSettings.CustomEndpoint))
+ if (!string.IsNullOrWhiteSpace(_userSettings.CustomEndpoint))
{
- clientOptions.Endpoint = new Uri(_userSettings.CustomEndpoint);
+ if (!Uri.TryCreate(_userSettings.CustomEndpoint, UriKind.Absolute, out var endpoint))
+ {
+ throw new ArgumentException($"Invalid custom endpoint URL: '{_userSettings.CustomEndpoint}'. Please ensure the URL includes the protocol (e.g., https://your-server.com/api) and is properly formatted.");
+ }
+
+ clientOptions.Endpoint = endpoint;
}
OpenAIClient openAIClient = new(new ApiKeyCredential(_aiCredentialsProvider.Key), clientOptions);
@@ -44,7 +49,7 @@ public sealed class CustomTextTransformService(IUserSettings userSettings, IAICr
var response = await openAIClient.GetChatClient(ModelName).CompleteChatAsync(
[
new SystemChatMessage(systemInstructions),
- new SystemChatMessage(userMessage)
+ new UserChatMessage(userMessage)
],
new()
{
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/KernelService.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/KernelService.cs
index a091a0dad..7d4d3eed1 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/KernelService.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/KernelService.cs
@@ -20,7 +20,7 @@ public sealed class KernelService(IUserSettings userSettings, IKernelQueryCacheS
private readonly IUserSettings _userSettings = userSettings;
private readonly IAICredentialsProvider _aiCredentialsProvider = aiCredentialsProvider;
- protected override string ModelName => string.IsNullOrEmpty(_userSettings.CustomModelName) ? "gpt-4o" : _userSettings.CustomModelName;
+ protected override string ModelName => string.IsNullOrWhiteSpace(_userSettings.CustomModelName) ? "gpt-4o" : _userSettings.CustomModelName;
protected override PromptExecutionSettings PromptExecutionSettings =>
new OpenAIPromptExecutionSettings()
@@ -31,13 +31,18 @@ public sealed class KernelService(IUserSettings userSettings, IKernelQueryCacheS
protected override void AddChatCompletionService(IKernelBuilder kernelBuilder)
{
- if (string.IsNullOrEmpty(_userSettings.CustomEndpoint))
+ if (string.IsNullOrWhiteSpace(_userSettings.CustomEndpoint))
{
kernelBuilder.AddOpenAIChatCompletion(ModelName, _aiCredentialsProvider.Key);
}
else
{
- kernelBuilder.AddOpenAIChatCompletion(ModelName, new Uri(_userSettings.CustomEndpoint), _aiCredentialsProvider.Key);
+ if (!Uri.TryCreate(_userSettings.CustomEndpoint, UriKind.Absolute, out var endpoint))
+ {
+ throw new ArgumentException($"Invalid custom endpoint URL: '{_userSettings.CustomEndpoint}'. Please ensure the URL includes the protocol (e.g., https://your-server.com/api) and is properly formatted.");
+ }
+
+ kernelBuilder.AddOpenAIChatCompletion(ModelName, endpoint, _aiCredentialsProvider.Key);
}
}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/PromptModerationService.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/PromptModerationService.cs
index a1058874a..43564a6c5 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/PromptModerationService.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/PromptModerationService.cs
@@ -35,9 +35,14 @@ public sealed class PromptModerationService(IUserSettings userSettings, IAICrede
try
{
OpenAIClientOptions clientOptions = new();
- if (!string.IsNullOrEmpty(_userSettings.CustomEndpoint))
+ if (!string.IsNullOrWhiteSpace(_userSettings.CustomEndpoint))
{
- clientOptions.Endpoint = new Uri(_userSettings.CustomEndpoint);
+ if (!Uri.TryCreate(_userSettings.CustomEndpoint, UriKind.Absolute, out var endpoint))
+ {
+ throw new ArgumentException($"Invalid custom endpoint URL: {_userSettings.CustomEndpoint}");
+ }
+
+ clientOptions.Endpoint = endpoint;
}
ModerationClient moderationClient = new(ModelName, new(_aiCredentialsProvider.Key), clientOptions);
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Strings/en-us/Resources.resw b/src/modules/AdvancedPaste/AdvancedPaste/Strings/en-us/Resources.resw
index 604cbf403..03afddea3 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Strings/en-us/Resources.resw
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Strings/en-us/Resources.resw
@@ -120,6 +120,9 @@
<data name="AIMistakeNote.Text" xml:space="preserve">
<value>AI can make mistakes.</value>
</data>
+ <data name="CustomAIMistakeNote.Text" xml:space="preserve">
+ <value>You are using a custom model endpoint please verify all answers.</value>
+ </data>
<data name="ClipboardEmptyWarning" xml:space="preserve">
<value>Clipboard does not contain any usable formats</value>
</data>
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs b/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs
index 3a0cdac9e..ef903f60e 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs
@@ -85,6 +85,8 @@ namespace AdvancedPaste.ViewModels
public bool IsAdvancedAIEnabled => IsCustomAIServiceEnabled && _userSettings.IsAdvancedAIEnabled;
+ public bool IsLocalModelMode => _userSettings.IsLocalModelMode;
+
public string CustomEndpoint => _userSettings.CustomEndpoint;
public string CustomModelName => _userSettings.CustomModelName;
@@ -173,6 +175,7 @@ namespace AdvancedPaste.ViewModels
OnPropertyChanged(nameof(ClipboardHasDataForCustomAI));
OnPropertyChanged(nameof(IsCustomAIAvailable));
OnPropertyChanged(nameof(IsAdvancedAIEnabled));
+ OnPropertyChanged(nameof(IsLocalModelMode));
OnPropertyChanged(nameof(CustomEndpoint));
OnPropertyChanged(nameof(CustomModelName));
OnPropertyChanged(nameof(DisableModeration));
diff --git a/src/modules/cmdpal/Microsoft.Terminal.UI/til_string.h b/src/modules/cmdpal/Microsoft.Terminal.UI/til_string.h
index 5d919ee54..376a5491c 100644
--- a/src/modules/cmdpal/Microsoft.Terminal.UI/til_string.h
+++ b/src/modules/cmdpal/Microsoft.Terminal.UI/til_string.h
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteAIMode.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteAIMode.cs
new file mode 100644
index 000000000..2fd2ab073
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteAIMode.cs
@@ -0,0 +1,13 @@
+// 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
+{
+ public enum AdvancedPasteAIMode
+ {
+ Disabled = 0,
+ OpenAI = 1,
+ LocalModel = 2,
+ }
+}
diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteProperties.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteProperties.cs
index 384470a1f..18cf3f4cb 100644
--- a/src/settings-ui/Settings.UI.Library/AdvancedPasteProperties.cs
+++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteProperties.cs
@@ -23,6 +23,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
PasteAsJsonShortcut = new();
CustomActions = new();
AdditionalActions = new();
+ AIMode = AdvancedPasteAIMode.Disabled;
IsAdvancedAIEnabled = false;
CustomEndpoint = string.Empty;
CustomModelName = string.Empty;
@@ -34,6 +35,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonConverter(typeof(BoolPropertyJsonConverter))]
public bool IsAdvancedAIEnabled { get; set; }
+ public AdvancedPasteAIMode AIMode { get; set; }
+
public string CustomEndpoint { get; set; }
public string CustomModelName { get; set; }
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml
index 0b876a63c..3bd44d35f 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml
@@ -73,21 +73,32 @@
<FontIconSource FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph="" />
</InfoBar.IconSource>
</InfoBar>
+ <InfoBar
+ x:Uid="AdvancedPaste_LocalModelWarning"
+ IsClosable="False"
+ IsOpen="{x:Bind ViewModel.IsLocalModelMode, Mode=OneWay}"
+ Visibility="{x:Bind ViewModel.IsLocalModelMode, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}"
+ Severity="Warning"/>
<tkcontrols:SettingsCard x:Uid="AdvancedPaste_EnableAISettingsCard" IsEnabled="{x:Bind ViewModel.IsOnlineAIModelsDisallowedByGPO, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}">
<tkcontrols:SettingsCard.HeaderIcon>
<PathIcon Data="M128 766q0-42 24-77t65-48l178-57q32-11 61-30t52-42q50-50 71-114l58-179q13-40 48-65t78-26q42 0 77 24t50 65l58 177q21 66 72 117 49 50 117 72l176 58q43 14 69 48t26 80q0 41-25 76t-64 49l-178 58q-66 21-117 72-32 32-51 73t-33 84-26 83-30 73-45 51-71 20q-42 0-77-24t-49-65l-58-178q-8-25-19-47t-28-43q-34-43-77-68t-89-41-89-27-78-29-55-45-21-75zm1149 7q-76-29-145-53t-129-60-104-88-73-138l-57-176-67 176q-18 48-42 89t-60 78q-34 34-76 61t-89 43l-177 57q75 29 144 53t127 60 103 89 73 137l57 176 67-176q37-97 103-168t168-103l177-57zm-125 759q0-31 20-57t49-36l99-32q34-11 53-34t30-51 20-59 20-54 33-41 58-16q32 0 59 19t38 50q6 20 11 40t13 40 17 38 25 34q16 17 39 26t48 18 49 16 44 20 31 32 12 50q0 33-18 60t-51 38q-19 6-39 11t-41 13-39 17-34 25q-24 25-35 62t-24 73-35 61-68 25q-32 0-59-19t-38-50q-6-18-11-39t-13-41-17-40-24-33q-18-17-41-27t-47-17-49-15-43-20-30-33-12-54zm583 4q-43-13-74-30t-55-41-40-55-32-74q-12 41-29 72t-42 55-55 42-71 31q81 23 128 71t71 129q15-43 31-74t40-54 53-40 75-32z" />
</tkcontrols:SettingsCard.HeaderIcon>
- <tkcontrols:SwitchPresenter TargetType="x:Boolean" Value="{x:Bind ViewModel.IsOpenAIEnabled, Mode=OneWay}">
- <tkcontrols:Case Value="True">
- <Button x:Uid="AdvancedPaste_DisableAIButton" Click="AdvancedPaste_DisableAIButton_Click" />
- </tkcontrols:Case>
- <tkcontrols:Case Value="False">
- <Button
- x:Uid="AdvancedPaste_EnableAIButton"
- Click="AdvancedPaste_EnableAIButton_Click"
- Style="{StaticResource AccentButtonStyle}" />
- </tkcontrols:Case>
- </tkcontrols:SwitchPresenter>
+ <ComboBox x:Name="AIModeComboBox"
+ MinWidth="200"
+ ItemsSource="{x:Bind ViewModel.AIModeOptions, Mode=OneWay}"
+ SelectedItem="{x:Bind ViewModel.SelectedAIMode, Mode=TwoWay}"
+ SelectionChanged="AIModeComboBox_SelectionChanged">
+ <ComboBox.ItemTemplate>
+ <DataTemplate>
+ <StackPanel>
+ <TextBlock Text="{Binding DisplayName}" FontWeight="SemiBold"/>
+ <TextBlock Text="{Binding Description}"
+ FontSize="12"
+ Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>
+ </StackPanel>
+ </DataTemplate>
+ </ComboBox.ItemTemplate>
+ </ComboBox>
<tkcontrols:SettingsCard.Description>
<StackPanel Orientation="Vertical">
<TextBlock x:Uid="AdvancedPaste_EnableAISettingsCardDescription" />
@@ -98,13 +109,14 @@
<tkcontrols:SettingsCard
x:Uid="AdvancedPaste_EnableAdvancedAI"
HeaderIcon="{ui:BitmapIcon Source=/Assets/Settings/Icons/SemanticKernel.png}"
- IsEnabled="{x:Bind ViewModel.IsOpenAIEnabled, Mode=OneWay}">
+ IsEnabled="{x:Bind ViewModel.IsAIEnabled, Mode=OneWay}">
<ToggleSwitch IsOn="{x:Bind ViewModel.IsAdvancedAIEnabled, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsExpander
x:Uid="AdvancedPaste_AdvancedSettings"
HeaderIcon="{ui:FontIcon Glyph=}"
- IsEnabled="{x:Bind ViewModel.IsOpenAIEnabled, Mode=OneWay}"
+ IsEnabled="{x:Bind ViewModel.ShowAdvancedSettings, Mode=OneWay}"
+ Visibility="{x:Bind ViewModel.ShowAdvancedSettings, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}"
IsExpanded="False">
<tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard
@@ -115,10 +127,6 @@
x:Uid="AdvancedPaste_AdvancedSettings_CustomModelName">
<TextBox Text="{x:Bind ViewModel.CustomModelName, Mode=TwoWay}"/>
</tkcontrols:SettingsCard>
- <tkcontrols:SettingsCard
- x:Uid="AdvancedPaste_AdvancedSettings_DisableModeration">
- <ToggleSwitch IsOn="{x:Bind ViewModel.DisableModeration, Mode=TwoWay}" />
- </tkcontrols:SettingsCard>
</tkcontrols:SettingsExpander.Items>
</tkcontrols:SettingsExpander>
</controls:SettingsGroup>
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml.cs
index 844226268..cbc399166 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml.cs
@@ -48,26 +48,53 @@ namespace Microsoft.PowerToys.Settings.UI.Views
}
}
- private async void AdvancedPaste_EnableAIButton_Click(object sender, RoutedEventArgs e)
+ private async void AIModeComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
- var resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader;
- EnableAIDialog.PrimaryButtonText = resourceLoader.GetString("EnableAIDialog_SaveBtnText");
- EnableAIDialog.SecondaryButtonText = resourceLoader.GetString("EnableAIDialog_CancelBtnText");
- EnableAIDialog.PrimaryButtonCommand = SaveOpenAIKeyCommand;
+ var comboBox = sender as ComboBox;
+ var selectedItem = comboBox?.SelectedItem as AdvancedPasteViewModel.AIModeItem;
- AdvancedPaste_EnableAIDialogOpenAIApiKey.Text = string.Empty;
+ if (selectedItem == null)
+ {
+ return;
+ }
- await ShowEnableDialogAsync();
- }
+ // Prevent recursion when updating the ViewModel
+ if (selectedItem.Mode == ViewModel.AIMode)
+ {
+ return;
+ }
- private async Task ShowEnableDialogAsync()
- {
- await EnableAIDialog.ShowAsync();
- }
+ // If user selects OpenAI but doesn't have a key, show the setup dialog
+ if (selectedItem.Mode == AdvancedPasteAIMode.OpenAI && !ViewModel.IsOpenAIEnabled)
+ {
+ var resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader;
+ EnableAIDialog.PrimaryButtonText = resourceLoader.GetString("EnableAIDialog_SaveBtnText");
+ EnableAIDialog.SecondaryButtonText = resourceLoader.GetString("EnableAIDialog_CancelBtnText");
+ EnableAIDialog.PrimaryButtonCommand = SaveOpenAIKeyCommand;
+
+ AdvancedPaste_EnableAIDialogOpenAIApiKey.Text = string.Empty;
+
+ var result = await EnableAIDialog.ShowAsync();
+
+ // If user canceled the dialog, revert the selection
+ if (result != ContentDialogResult.Primary || string.IsNullOrEmpty(AdvancedPaste_EnableAIDialogOpenAIApiKey.Text))
+ {
+ // Revert ComboBox selection without triggering change event
+ comboBox.SelectionChanged -= AIModeComboBox_SelectionChanged;
+ comboBox.SelectedItem = ViewModel.SelectedAIMode;
+ comboBox.SelectionChanged += AIModeComboBox_SelectionChanged;
+ return;
+ }
+ }
- private void AdvancedPaste_DisableAIButton_Click(object sender, RoutedEventArgs e)
- {
- ViewModel.DisableAI();
+ // If user selects Disabled and had OpenAI enabled, disable it
+ if (selectedItem.Mode == AdvancedPasteAIMode.Disabled && ViewModel.IsOpenAIEnabled)
+ {
+ ViewModel.DisableAI();
+ }
+
+ // Update the ViewModel (this will handle the actual mode change)
+ ViewModel.AIMode = selectedItem.Mode;
}
private void AdvancedPaste_EnableAIDialogOpenAIApiKey_TextChanged(object sender, TextChangedEventArgs e)
diff --git a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
index 7b39443a4..8fbb628e7 100644
--- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
+++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
@@ -3897,6 +3897,12 @@ Activate by holding the key for the character you want to add an accent to, then
<data name="AdvancedPaste_EnableAdvancedAI.Description" xml:space="preserve">
<value>Add advanced capabilities when using 'Paste with AI' including the power to 'chain' multiple transformations together and work with images and files. This feature may consume more API credits when used.</value>
</data>
+ <data name="AdvancedPaste_LocalModelWarning.Title" xml:space="preserve">
+ <value>Local Model Warning</value>
+ </data>
+ <data name="AdvancedPaste_LocalModelWarning.Message" xml:space="preserve">
+ <value>When using a local model, PowerToys will send your clipboard data to the endpoint you specify. Ensure you trust the endpoint and understand the privacy implications. Moderation is automatically disabled for local models.</value>
+ </data>
<data name="Oobe_AdvancedPaste.Description" xml:space="preserve">
<value>Advanced Paste is a tool to put your clipboard content into any format you need, focused towards developer workflows. It can paste as plain text, markdown, or json directly with the UX or with a direct keystroke invoke. These are fully locally executed. In addition, it has an AI powered option that is 100% opt-in and requires an Open AI key. Note: this will replace the formatted text in your clipboard with the selected format.</value>
</data>
diff --git a/src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs
index 291dee04d..a94e121ab 100644
--- a/src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs
+++ b/src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs
@@ -361,6 +361,71 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
+ public AdvancedPasteAIMode AIMode
+ {
+ get => _advancedPasteSettings.Properties.AIMode;
+ set
+ {
+ if (value != _advancedPasteSettings.Properties.AIMode)
+ {
+ _advancedPasteSettings.Properties.AIMode = value;
+
+ // Auto-disable moderation for local models
+ if (value == AdvancedPasteAIMode.LocalModel)
+ {
+ _advancedPasteSettings.Properties.DisableModeration = true;
+ }
+ else if (value == AdvancedPasteAIMode.OpenAI)
+ {
+ _advancedPasteSettings.Properties.DisableModeration = false;
+ }
+
+ OnPropertyChanged(nameof(AIMode));
+ OnPropertyChanged(nameof(IsAIEnabled));
+ OnPropertyChanged(nameof(IsLocalModelMode));
+ OnPropertyChanged(nameof(ShowAdvancedSettings));
+ OnPropertyChanged(nameof(DisableModeration));
+ OnPropertyChanged(nameof(SelectedAIMode));
+ NotifySettingsChanged();
+ }
+ }
+ }
+
+ public bool IsAIEnabled => AIMode != AdvancedPasteAIMode.Disabled && IsOpenAIEnabled;
+
+ public bool IsLocalModelMode => AIMode == AdvancedPasteAIMode.LocalModel;
+
+ public bool ShowAdvancedSettings => IsLocalModelMode;
+
+ public class AIModeItem
+ {
+ public AdvancedPasteAIMode Mode { get; set; }
+
+ public string DisplayName { get; set; }
+
+ public string Description { get; set; }
+ }
+
+ public AIModeItem[] AIModeOptions { get; } = new[]
+ {
+ new AIModeItem { Mode = AdvancedPasteAIMode.Disabled, DisplayName = "Disabled", Description = "AI features are disabled" },
+ new AIModeItem { Mode = AdvancedPasteAIMode.OpenAI, DisplayName = "OpenAI", Description = "Use OpenAI cloud service" },
+ new AIModeItem { Mode = AdvancedPasteAIMode.LocalModel, DisplayName = "Local model", Description = "Use a local AI model endpoint" },
+ };
+
+ public AIModeItem SelectedAIMode
+ {
+ get => AIModeOptions.FirstOrDefault(x => x.Mode == AIMode) ?? AIModeOptions[0];
+ set
+ {
+ if (value != null && value.Mode != AIMode)
+ {
+ AIMode = value.Mode;
+ OnPropertyChanged(nameof(SelectedAIMode));
+ }
+ }
+ }
+
public string CustomEndpoint
{
get => _advancedPasteSettings.Properties.CustomEndpoint;
@@ -397,6 +462,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
if (value != _advancedPasteSettings.Properties.DisableModeration)
{
_advancedPasteSettings.Properties.DisableModeration = value;
+ OnPropertyChanged(nameof(DisableModeration));
NotifySettingsChanged();
}
}
@@ -501,7 +567,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
PasswordVault vault = new PasswordVault();
PasswordCredential cred = vault.Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey");
vault.Remove(cred);
+ AIMode = AdvancedPasteAIMode.Disabled;
OnPropertyChanged(nameof(IsOpenAIEnabled));
+ OnPropertyChanged(nameof(SelectedAIMode));
NotifySettingsChanged();
}
catch (Exception)
@@ -516,7 +584,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
PasswordVault vault = new();
PasswordCredential cred = new("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey", password);
vault.Add(cred);
+ AIMode = AdvancedPasteAIMode.OpenAI; // Default to OpenAI when enabling
OnPropertyChanged(nameof(IsOpenAIEnabled));
+ OnPropertyChanged(nameof(SelectedAIMode));
IsAdvancedAIEnabled = true; // new users should get Semantic Kernel benefits immediately
NotifySettingsChanged();
}
Hi @sepcnt , why did you close the PR? Will you create a new one? I saw Craig noted that it has passed the RAI review, so it’s expected to be checked in in the next release or the following one
@sepcnt any chance you'd be willing to re-open this PR? I'm looking for exactly the same functionality and it looks like other from the PowerToys community are as well.
Hi @sepcnt , why did you close the PR? Will you create a new one? I saw Craig noted that it has passed the RAI review, so it’s expected to be checked in in the next release or the following one
https://github.com/microsoft/PowerToys/commits/fd1329ec2d23992daf6d800558ca6d78624fdce1/