Added user-managed API keys for enterprise-configured providers (#919)
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions

Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
Dominic NeuburgandThorsten Sommer authored and GitHub committed 2026-08-15 11:20:36 +02:00
1 parent a9a37b6bf5
commit 592c9c76e2
23 files changed
+479 -50

No files matched your search

@@ -4,10 +4,16 @@
<MudDialog>
<DialogContent>
@if (this.IsEnterpriseConfiguration)
{
<MudAlert Severity="Severity.Info" Class="mb-4">
@T("This embedding provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below.")
</MudAlert>
}
<MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues">
<MudStack Row="@true" AlignItems="AlignItems.Center">
@* ReSharper disable once CSharpWarnings::CS8974 *@
<MudSelect @bind-Value="@this.DataLLMProvider" Label="@T("Provider")" Class="mb-3" OpenIcon="@Icons.Material.Filled.AccountBalance" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.providerValidation.ValidatingProvider">
<MudSelect @bind-Value="@this.DataLLMProvider" Label="@T("Provider")" Class="mb-3" OpenIcon="@Icons.Material.Filled.AccountBalance" AdornmentColor="Color.Info" Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingProvider">
@foreach (LLMProviders provider in Enum.GetValues(typeof(LLMProviders)))
{
if (provider.ProvideEmbeddingAPI() || provider is LLMProviders.NONE)
@@ -38,13 +44,14 @@
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Dns"
AdornmentColor="Color.Info"
Disabled="@this.IsEnterpriseConfiguration"
Validation="@this.providerValidation.ValidatingHostname"
UserAttributes="@SPELLCHECK_ATTRIBUTES"/>
}
@if (this.DataLLMProvider.IsHostNeeded())
{
<MudSelect T="Host" Value="@this.DataHost" ValueChanged="@this.OnHostChanged" Label="@T("Host")" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.providerValidation.ValidatingHost">
<MudSelect T="Host" Value="@this.DataHost" ValueChanged="@this.OnHostChanged" Label="@T("Host")" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingHost">
@foreach (Host host in Enum.GetValues(typeof(Host)))
{
if (host.IsEmbeddingSupported())
@@ -69,6 +76,7 @@
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Dns"
AdornmentColor="Color.Info"
Disabled="@this.IsEnterpriseConfiguration"
Validation="@this.ValidateManuallyModel"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
HelperText="@T("Currently, we cannot query the embedding models for the selected provider and/or host. Therefore, please enter the model name manually.")"
@@ -76,7 +84,7 @@
}
else
{
<MudButton Disabled="@(!this.DataLLMProvider.CanLoadModels(this.DataHost, this.dataAPIKey))" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.Refresh" OnClick="@this.ReloadModels">
<MudButton Disabled="@(this.IsEnterpriseConfiguration || !this.DataLLMProvider.CanLoadModels(this.DataHost, this.dataAPIKey))" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.Refresh" OnClick="@this.ReloadModels">
@T("Load")
</MudButton>
@if(this.availableModels.Count is 0)
@@ -87,7 +95,7 @@
}
else
{
<MudSelect Disabled="@this.IsNoneProvider" @bind-Value="@this.DataModel" Label="@T("Model")"
<MudSelect Disabled="@(this.IsEnterpriseConfiguration || this.IsNoneProvider)" @bind-Value="@this.DataModel" Label="@T("Model")"
OpenIcon="@Icons.Material.Filled.FaceRetouchingNatural"
AdornmentColor="Color.Info" Adornment="Adornment.Start"
Validation="@this.providerValidation.ValidatingModel">
@@ -121,6 +129,7 @@
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Lightbulb"
AdornmentColor="Color.Info"
Disabled="@this.IsEnterpriseConfiguration"
Validation="@this.providerValidation.ValidatingInstanceName"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
/>
@@ -68,7 +68,14 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
/// </summary>
[Parameter]
public bool IsEditing { get; init; }
/// <summary>
/// Whether this embedding provider is managed by an enterprise configuration plugin. When true,
/// every field except the API key is locked, matching Settings.EmbeddingProvider.IsEnterpriseConfiguration.
/// </summary>
[Parameter]
public bool IsEnterpriseConfiguration { get; set; }
[Inject]
private RustService RustService { get; init; } = null!;
@@ -85,6 +92,7 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
private bool dataIsValid;
private string[] dataIssues = [];
private string dataAPIKey = string.Empty;
private bool dataHadStoredAPIKeyOnLoad;
private string dataManuallyModel = string.Empty;
private string dataAPIKeyStorageIssue = string.Empty;
private string dataEditingPreviousInstanceName = string.Empty;
@@ -134,7 +142,7 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
IsSelfHosted = this.DataLLMProvider is LLMProviders.SELF_HOSTED,
Hostname = cleanedHostname.EndsWith('/') ? cleanedHostname[..^1] : cleanedHostname,
Host = this.DataHost,
IsEnterpriseConfiguration = false,
IsEnterpriseConfiguration = this.IsEnterpriseConfiguration,
EnterpriseConfigurationPluginId = Guid.Empty,
};
}
@@ -174,11 +182,17 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
// Load the API key:
var requestedSecret = await this.RustService.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER, isTrying: this.DataLLMProvider is LLMProviders.SELF_HOSTED);
if (requestedSecret.Success)
{
this.dataAPIKey = await requestedSecret.Secret.Decrypt(this.encryption);
this.dataHadStoredAPIKeyOnLoad = !string.IsNullOrWhiteSpace(this.dataAPIKey);
}
else
{
this.dataAPIKey = string.Empty;
if (this.DataLLMProvider is not LLMProviders.SELF_HOSTED)
// For an enterprise-managed provider, having no key yet is the expected first-run
// state, not a storage failure -- the user is just about to set their own key:
if (this.DataLLMProvider is not LLMProviders.SELF_HOSTED && !this.IsEnterpriseConfiguration)
{
this.dataAPIKeyStorageIssue = string.Format(T("Failed to load the API key from the operating system. The message was: {0}. You might ignore this message and provide the API key again."), requestedSecret.Issue);
await this.form.Validate();
@@ -203,8 +217,12 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
#region Implementation of ISecretId
public string SecretId => this.DataLLMProvider.ToSecretId();
// Must mirror Settings.EmbeddingProvider.SecretId exactly: when editing an enterprise-managed
// provider, the key has to be stored under the same "ENT::"-prefixed keyring row that the
// app reads from at runtime (see BaseProvider.SecretId). Otherwise, a key entered here would
// silently end up in the wrong keyring row and never be found again.
public string SecretId => this.IsEnterpriseConfiguration ? $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{this.DataLLMProvider.ToSecretId()}" : this.DataLLMProvider.ToSecretId();
public string SecretName => this.DataName;
#endregion
@@ -240,6 +258,22 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
await this.form.Validate();
return;
}
this.dataHadStoredAPIKeyOnLoad = true;
}
else if (this.dataHadStoredAPIKeyOnLoad)
{
// The user cleared a previously stored key. Without this, the old key would simply
// stay in the OS keyring untouched and keep being used:
var deleteResponse = await this.RustService.DeleteAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER);
if (!deleteResponse.Success)
{
this.dataAPIKeyStorageIssue = string.Format(T("Failed to remove the API key from the operating system. The message was: {0}. Please try again."), deleteResponse.Issue);
await this.form.Validate();
return;
}
this.dataHadStoredAPIKeyOnLoad = false;
}
this.MudDialog.Close(DialogResult.Ok(addedProviderSettings));
@@ -4,6 +4,12 @@
@inherits MSGComponentBase
<MudDialog>
<DialogContent>
@if (this.IsEnterpriseConfiguration)
{
<MudAlert Severity="Severity.Info" Class="mb-4">
@T("This provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below.")
</MudAlert>
}
<MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues">
<MudStack Row="@true" AlignItems="AlignItems.Center">
@* ReSharper disable once CSharpWarnings::CS8974 *@
@@ -15,6 +21,7 @@
OpenIcon="@Icons.Material.Filled.AccountBalance"
AdornmentColor="Color.Info"
Adornment="Adornment.Start"
Disabled="@this.IsEnterpriseConfiguration"
Validation="@this.providerValidation.ValidatingProvider">
@foreach (LLMProviders provider in Enum.GetValues(typeof(LLMProviders)))
{
@@ -27,7 +34,7 @@
@T("Create account")
</MudButton>
</MudStack>
@if (this.DataLLMProvider.IsAPIKeyNeeded(this.DataHost))
{
<SecretInputField Secret="@this.dataAPIKey" SecretChanged="@this.OnAPIKeyChanged" Label="@this.APIKeyText" Validation="@this.providerValidation.ValidatingAPIKey"/>
@@ -43,13 +50,14 @@
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Dns"
AdornmentColor="Color.Info"
Disabled="@this.IsEnterpriseConfiguration"
Validation="@this.providerValidation.ValidatingHostname"
UserAttributes="@SPELLCHECK_ATTRIBUTES"/>
}
@if (this.DataLLMProvider.IsHostNeeded())
{
<MudSelect T="Host" Value="@this.DataHost" ValueChanged="@this.OnHostChanged" Label="@T("Host")" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.providerValidation.ValidatingHost">
<MudSelect T="Host" Value="@this.DataHost" ValueChanged="@this.OnHostChanged" Label="@T("Host")" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingHost">
@foreach (Host host in Enum.GetValues(typeof(Host)))
{
@if (host.IsChatSupported())
@@ -64,7 +72,7 @@
@if (this.DataLLMProvider.IsHFInstanceProviderNeeded())
{
<MudSelect @bind-Value="@this.HFInferenceProviderId" Label="@T("Hugging Face Inference Provider")" Class="mb-3" OpenIcon="@Icons.Material.Filled.Dns" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.providerValidation.ValidatingHFInstanceProvider">
<MudSelect @bind-Value="@this.HFInferenceProviderId" Label="@T("Hugging Face Inference Provider")" Class="mb-3" OpenIcon="@Icons.Material.Filled.Dns" AdornmentColor="Color.Info" Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingHFInstanceProvider">
@foreach (HFInferenceProvider inferenceProvider in Enum.GetValues(typeof(HFInferenceProvider)))
{
<MudSelectItem Value="@inferenceProvider">
@@ -95,6 +103,7 @@
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.FaceRetouchingNatural"
AdornmentColor="Color.Info"
Disabled="@this.IsEnterpriseConfiguration"
Validation="@this.ValidateManuallyModel"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
HelperText="@T("Currently, we cannot query the models for the selected provider and/or host. Therefore, please enter the model name manually.")"
@@ -102,7 +111,7 @@
}
else
{
<MudButton Disabled="@(!this.DataLLMProvider.CanLoadModels(this.DataHost, this.dataAPIKey))" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.Refresh" OnClick="@this.ReloadModels">
<MudButton Disabled="@(this.IsEnterpriseConfiguration || !this.DataLLMProvider.CanLoadModels(this.DataHost, this.dataAPIKey))" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.Refresh" OnClick="@this.ReloadModels">
@T("Load models")
</MudButton>
@if(this.availableModels.Count is 0)
@@ -117,7 +126,7 @@
Value="@this.DataModel"
ValueChanged="@(async model => await this.OnModelChanged(model))"
OpenIcon="@Icons.Material.Filled.FaceRetouchingNatural" AdornmentColor="Color.Info"
Adornment="Adornment.Start" Validation="@this.providerValidation.ValidatingModel">
Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingModel">
@foreach (var model in this.availableModels)
{
<MudSelectItem Value="@model">
@@ -157,6 +166,7 @@
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Lightbulb"
AdornmentColor="Color.Info"
Disabled="@this.IsEnterpriseConfiguration"
Validation="@this.providerValidation.ValidatingInstanceName"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
/>
@@ -193,12 +203,13 @@
<MudSwitch T="bool"
Value="@this.IsCapabilityEnabled(capability)"
ValueChanged="@(value => this.OnCapabilitySwitchChanged(capability, value))"
Disabled="@this.IsEnterpriseConfiguration"
Color="Color.Primary" />
<MudButton Variant="Variant.Outlined"
Color="Color.Default"
Size="Size.Small"
StartIcon="@Icons.Material.Filled.RestartAlt"
Disabled="@(!this.HasCapabilityOverride(capability))"
Disabled="@(this.IsEnterpriseConfiguration || !this.HasCapabilityOverride(capability))"
OnClick="@(() => this.ResetCapabilityOverride(capability))">
@T("Reset")
</MudButton>
@@ -216,7 +227,8 @@
Margin="Margin.Dense"
OpenIcon="@Icons.Material.Filled.Psychology"
AdornmentColor="Color.Info"
Adornment="Adornment.Start">
Adornment="Adornment.Start"
Disabled="@this.IsEnterpriseConfiguration">
@foreach (var mode in REASONING_OVERRIDE_MODES)
{
<MudSelectItem Value="@mode">
@@ -229,7 +241,7 @@
<MudJustifiedText Typo="Typo.body2" Class="mb-4">
@string.Format(T("The current model uses the {0}."), this.GetCurrentModelApiLabel())
</MudJustifiedText>
<MudTextField T="string" Label=@T("Additional API parameters") Variant="Variant.Outlined" Lines="4" AutoGrow="true" MaxLines="10" HelperText=@T("""Add the parameters in proper JSON formatting, e.g., "temperature": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though.""") Placeholder="@GetPlaceholderExpertSettings" @bind-Value="@this.AdditionalJsonApiParameters" Immediate="true" Validation="@this.ValidateAdditionalJsonApiParameters" OnBlur="@this.OnInputChangeExpertSettings"/>
<MudTextField T="string" Label=@T("Additional API parameters") Variant="Variant.Outlined" Lines="4" AutoGrow="true" MaxLines="10" HelperText=@T("""Add the parameters in proper JSON formatting, e.g., "temperature": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though.""") Placeholder="@GetPlaceholderExpertSettings" @bind-Value="@this.AdditionalJsonApiParameters" Immediate="true" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.ValidateAdditionalJsonApiParameters" OnBlur="@this.OnInputChangeExpertSettings"/>
</MudCollapse>
</MudStack>
</MudForm>
@@ -90,6 +90,13 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
/// </summary>
[Parameter]
public bool IsEditing { get; init; }
/// <summary>
/// Whether this provider is managed by an enterprise configuration plugin. When true, every
/// field except the API key is locked, matching Settings.Provider.IsEnterpriseConfiguration.
/// </summary>
[Parameter]
public bool IsEnterpriseConfiguration { get; set; }
[Parameter]
public string AdditionalJsonApiParameters { get; set; } = string.Empty;
@@ -129,6 +136,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
private bool dataIsValid;
private string[] dataIssues = [];
private string dataAPIKey = string.Empty;
private bool dataHadStoredAPIKeyOnLoad;
private string dataManuallyModel = string.Empty;
private string dataAPIKeyStorageIssue = string.Empty;
private string dataEditingPreviousInstanceName = string.Empty;
@@ -170,7 +178,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
UsedLLMProvider = this.DataLLMProvider,
Model = this.GetSelectedModel(),
IsSelfHosted = this.DataLLMProvider is LLMProviders.SELF_HOSTED,
IsEnterpriseConfiguration = false,
IsEnterpriseConfiguration = this.IsEnterpriseConfiguration,
Hostname = cleanedHostname.EndsWith('/') ? cleanedHostname[..^1] : cleanedHostname,
Host = this.DataHost,
HFInferenceProvider = this.HFInferenceProviderId,
@@ -228,11 +236,17 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
// Load the API key:
var requestedSecret = await this.RustService.GetAPIKey(this, SecretStoreType.LLM_PROVIDER, isTrying: this.DataLLMProvider is LLMProviders.SELF_HOSTED);
if (requestedSecret.Success)
{
this.dataAPIKey = await requestedSecret.Secret.Decrypt(this.encryption);
this.dataHadStoredAPIKeyOnLoad = !string.IsNullOrWhiteSpace(this.dataAPIKey);
}
else
{
this.dataAPIKey = string.Empty;
if (this.DataLLMProvider is not LLMProviders.SELF_HOSTED)
// For an enterprise-managed provider, having no key yet is the expected first-run
// state, not a storage failure -- the user is just about to set their own key:
if (this.DataLLMProvider is not LLMProviders.SELF_HOSTED && !this.IsEnterpriseConfiguration)
{
this.dataAPIKeyStorageIssue = string.Format(T("Failed to load the API key from the operating system. The message was: {0}. You might ignore this message and provide the API key again."), requestedSecret.Issue);
await this.form.Validate();
@@ -257,8 +271,12 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
#region Implementation of ISecretId
public string SecretId => this.DataLLMProvider.ToSecretId();
// Must mirror Settings.Provider.SecretId exactly: when editing an enterprise-managed
// provider, the key has to be stored under the same "ENT::"-prefixed keyring row that the
// app reads from at runtime (see BaseProvider.SecretId). Otherwise, a key entered here would
// silently end up in the wrong keyring row and never be found again.
public string SecretId => this.IsEnterpriseConfiguration ? $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{this.DataLLMProvider.ToSecretId()}" : this.DataLLMProvider.ToSecretId();
public string SecretName => this.DataInstanceName;
#endregion
@@ -295,6 +313,22 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
await this.form.Validate();
return;
}
this.dataHadStoredAPIKeyOnLoad = true;
}
else if (this.dataHadStoredAPIKeyOnLoad)
{
// The user cleared a previously stored key. Without this, the old key would simply
// stay in the OS keyring untouched and keep being used:
var deleteResponse = await this.RustService.DeleteAPIKey(this, SecretStoreType.LLM_PROVIDER);
if (!deleteResponse.Success)
{
this.dataAPIKeyStorageIssue = string.Format(T("Failed to remove the API key from the operating system. The message was: {0}. Please try again."), deleteResponse.Issue);
await this.form.Validate();
return;
}
this.dataHadStoredAPIKeyOnLoad = false;
}
this.MudDialog.Close(DialogResult.Ok(addedProviderSettings));
@@ -4,10 +4,16 @@
<MudDialog>
<DialogContent>
@if (this.IsEnterpriseConfiguration)
{
<MudAlert Severity="Severity.Info" Class="mb-4">
@T("This transcription provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below.")
</MudAlert>
}
<MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues">
<MudStack Row="@true" AlignItems="AlignItems.Center">
@* ReSharper disable once CSharpWarnings::CS8974 *@
<MudSelect @bind-Value="@this.DataLLMProvider" Label="@T("Provider")" Class="mb-3" OpenIcon="@Icons.Material.Filled.AccountBalance" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.providerValidation.ValidatingProvider">
<MudSelect @bind-Value="@this.DataLLMProvider" Label="@T("Provider")" Class="mb-3" OpenIcon="@Icons.Material.Filled.AccountBalance" AdornmentColor="Color.Info" Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingProvider">
@foreach (LLMProviders provider in Enum.GetValues(typeof(LLMProviders)))
{
if (provider.ProvideTranscriptionAPI() || provider is LLMProviders.NONE)
@@ -38,13 +44,14 @@
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Dns"
AdornmentColor="Color.Info"
Disabled="@this.IsEnterpriseConfiguration"
Validation="@this.providerValidation.ValidatingHostname"
UserAttributes="@SPELLCHECK_ATTRIBUTES"/>
}
@if (this.DataLLMProvider.IsHostNeeded())
{
<MudSelect T="Host" Value="@this.DataHost" ValueChanged="@this.OnHostChanged" Label="@T("Host")" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.providerValidation.ValidatingHost">
<MudSelect T="Host" Value="@this.DataHost" ValueChanged="@this.OnHostChanged" Label="@T("Host")" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingHost">
@foreach (Host host in Enum.GetValues(typeof(Host)))
{
if (host.IsTranscriptionSupported())
@@ -71,6 +78,7 @@
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Dns"
AdornmentColor="Color.Info"
Disabled="@this.IsEnterpriseConfiguration"
Validation="@this.ValidateManuallyModel"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
HelperText="@T("Currently, we cannot query the transcription models for the selected provider and/or host. Therefore, please enter the model name manually.")"
@@ -78,7 +86,7 @@
}
else
{
<MudButton Disabled="@(!this.DataLLMProvider.CanLoadModels(this.DataHost, this.dataAPIKey))" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.Refresh" OnClick="@this.ReloadModels">
<MudButton Disabled="@(this.IsEnterpriseConfiguration || !this.DataLLMProvider.CanLoadModels(this.DataHost, this.dataAPIKey))" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.Refresh" OnClick="@this.ReloadModels">
@T("Load")
</MudButton>
@if(this.availableModels.Count is 0)
@@ -89,7 +97,7 @@
}
else
{
<MudSelect Disabled="@this.IsNoneProvider" @bind-Value="@this.DataModel" Label="@T("Model")"
<MudSelect Disabled="@(this.IsEnterpriseConfiguration || this.IsNoneProvider)" @bind-Value="@this.DataModel" Label="@T("Model")"
OpenIcon="@Icons.Material.Filled.FaceRetouchingNatural"
AdornmentColor="Color.Info" Adornment="Adornment.Start"
Validation="@this.providerValidation.ValidatingModel">
@@ -132,6 +140,7 @@
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Lightbulb"
AdornmentColor="Color.Info"
Disabled="@this.IsEnterpriseConfiguration"
Validation="@this.providerValidation.ValidatingInstanceName"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
/>
@@ -68,7 +68,14 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId
/// </summary>
[Parameter]
public bool IsEditing { get; init; }
/// <summary>
/// Whether this transcription provider is managed by an enterprise configuration plugin. When
/// true, every field except the API key is locked, matching Settings.TranscriptionProvider.IsEnterpriseConfiguration.
/// </summary>
[Parameter]
public bool IsEnterpriseConfiguration { get; set; }
[Inject]
private RustService RustService { get; init; } = null!;
@@ -85,6 +92,7 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId
private bool dataIsValid;
private string[] dataIssues = [];
private string dataAPIKey = string.Empty;
private bool dataHadStoredAPIKeyOnLoad;
private string dataManuallyModel = string.Empty;
private string dataAPIKeyStorageIssue = string.Empty;
private string dataEditingPreviousInstanceName = string.Empty;
@@ -149,7 +157,7 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId
IsSelfHosted = this.DataLLMProvider is LLMProviders.SELF_HOSTED,
Hostname = cleanedHostname.EndsWith('/') ? cleanedHostname[..^1] : cleanedHostname,
Host = this.DataHost,
IsEnterpriseConfiguration = false,
IsEnterpriseConfiguration = this.IsEnterpriseConfiguration,
EnterpriseConfigurationPluginId = Guid.Empty,
};
}
@@ -189,11 +197,17 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId
// Load the API key:
var requestedSecret = await this.RustService.GetAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER, isTrying: this.DataLLMProvider is LLMProviders.SELF_HOSTED);
if (requestedSecret.Success)
{
this.dataAPIKey = await requestedSecret.Secret.Decrypt(this.encryption);
this.dataHadStoredAPIKeyOnLoad = !string.IsNullOrWhiteSpace(this.dataAPIKey);
}
else
{
this.dataAPIKey = string.Empty;
if (this.DataLLMProvider is not LLMProviders.SELF_HOSTED)
// For an enterprise-managed provider, having no key yet is the expected first-run
// state, not a storage failure -- the user is just about to set their own key:
if (this.DataLLMProvider is not LLMProviders.SELF_HOSTED && !this.IsEnterpriseConfiguration)
{
this.dataAPIKeyStorageIssue = string.Format(T("Failed to load the API key from the operating system. The message was: {0}. You might ignore this message and provide the API key again."), requestedSecret.Issue);
await this.form.Validate();
@@ -218,8 +232,12 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId
#region Implementation of ISecretId
public string SecretId => this.DataLLMProvider.ToSecretId();
// Must mirror Settings.TranscriptionProvider.SecretId exactly: when editing an enterprise-managed
// provider, the key has to be stored under the same "ENT::"-prefixed keyring row that the
// app reads from at runtime (see BaseProvider.SecretId). Otherwise, a key entered here would
// silently end up in the wrong keyring row and never be found again.
public string SecretId => this.IsEnterpriseConfiguration ? $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{this.DataLLMProvider.ToSecretId()}" : this.DataLLMProvider.ToSecretId();
public string SecretName => this.DataName;
#endregion
@@ -255,6 +273,22 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId
await this.form.Validate();
return;
}
this.dataHadStoredAPIKeyOnLoad = true;
}
else if (this.dataHadStoredAPIKeyOnLoad)
{
// The user cleared a previously stored key. Without this, the old key would simply
// stay in the OS keyring untouched and keep being used:
var deleteResponse = await this.RustService.DeleteAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER);
if (!deleteResponse.Success)
{
this.dataAPIKeyStorageIssue = string.Format(T("Failed to remove the API key from the operating system. The message was: {0}. Please try again."), deleteResponse.Issue);
await this.form.Validate();
return;
}
this.dataHadStoredAPIKeyOnLoad = false;
}
this.MudDialog.Close(DialogResult.Ok(addedProviderSettings));