diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
index 66ee77c3..2f40e79e 100644
--- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
+++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
@@ -4294,6 +4294,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T18253
-- Add Embedding Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T190634634"] = "Add Embedding Provider"
+-- This embedding provider is managed by your organization. You can set your own API key.
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1931890418"] = "This embedding provider is managed by your organization. You can set your own API key."
+
-- Add text that should be embedded:
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1992646324"] = "Add text that should be embedded:"
@@ -4426,6 +4429,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T426925
-- This self-hosted provider is trusted for data source security checks.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T485526152"] = "This self-hosted provider is trusted for data source security checks."
+-- This provider is managed by your organization. You can set your own API key.
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T579100747"] = "This provider is managed by your organization. You can set your own API key."
+
-- Open Dashboard
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T78223861"] = "Open Dashboard"
@@ -4480,6 +4486,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T58
-- This transcription provider is trusted by your organization for data source security checks.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T601264181"] = "This transcription provider is trusted by your organization for data source security checks."
+-- This transcription provider is managed by your organization. You can set your own API key.
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T690752279"] = "This transcription provider is managed by your organization. You can set your own API key."
+
-- This transcription provider is managed by your organization.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T756131076"] = "This transcription provider is managed by your organization."
@@ -5776,6 +5785,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T2189814010"] = "Mo
-- (Optional) API Key
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T2331453405"] = "(Optional) API Key"
+-- Failed to remove the API key from the operating system. The message was: {0}. Please try again.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T2439094236"] = "Failed to remove the API key from the operating system. The message was: {0}. Please try again."
+
+-- This embedding provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T2555207324"] = "This embedding provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below."
+
-- Add
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T2646845972"] = "Add"
@@ -6076,6 +6091,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROMPTINGGUIDELINEDIALOG::T4250996615"] = "P
-- Hugging Face Inference Provider
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1085481431"] = "Hugging Face Inference Provider"
+-- This provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1090492389"] = "This provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below."
+
-- Hide Expert Settings
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1108876344"] = "Hide Expert Settings"
@@ -6139,6 +6157,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2189814010"] = "Model"
-- (Optional) API Key
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2331453405"] = "(Optional) API Key"
+-- Failed to remove the API key from the operating system. The message was: {0}. Please try again.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2439094236"] = "Failed to remove the API key from the operating system. The message was: {0}. Please try again."
+
-- Enabled
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2626085950"] = "Enabled"
@@ -7771,6 +7792,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T2189814010"] =
-- (Optional) API Key
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T2331453405"] = "(Optional) API Key"
+-- Failed to remove the API key from the operating system. The message was: {0}. Please try again.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T2439094236"] = "Failed to remove the API key from the operating system. The message was: {0}. Please try again."
+
-- Add
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T2646845972"] = "Add"
@@ -7795,6 +7819,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T504465522"] =
-- Host
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T808120719"] = "Host"
+-- This transcription provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T828088153"] = "This transcription provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below."
+
-- Provider
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T900237532"] = "Provider"
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor
index 71f0e618..a4ad15f7 100644
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor
+++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor
@@ -45,12 +45,21 @@
}
- @if (context.IsEnterpriseConfiguration)
+ @if (context.IsEnterpriseConfiguration && !context.AllowUserProvidedAPIKey)
{
}
+ else if (context.IsEnterpriseConfiguration && context.AllowUserProvidedAPIKey)
+ {
+
+
+
+
+
+
+ }
else
{
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs
index f7b88c14..dd74f949 100644
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs
+++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs
@@ -73,6 +73,9 @@ public partial class SettingsPanelEmbeddings : SettingsPanelProviderBase
private async Task EditEmbeddingProvider(EmbeddingProvider embeddingProvider)
{
+ if (embeddingProvider.IsEnterpriseConfiguration && !embeddingProvider.AllowUserProvidedAPIKey)
+ return;
+
var dialogParameters = new DialogParameters
{
{ x => x.DataNum, embeddingProvider.Num },
@@ -84,6 +87,7 @@ public partial class SettingsPanelEmbeddings : SettingsPanelProviderBase
{ x => x.IsSelfHosted, embeddingProvider.IsSelfHosted },
{ x => x.IsEditing, true },
{ x => x.DataHost, embeddingProvider.Host },
+ { x => x.IsEnterpriseConfiguration, embeddingProvider.IsEnterpriseConfiguration },
};
var dialogReference = await this.DialogService.ShowAsync(T("Edit Embedding Provider"), dialogParameters, DialogOptions.FULLSCREEN);
@@ -91,6 +95,16 @@ public partial class SettingsPanelEmbeddings : SettingsPanelProviderBase
if (dialogResult is null || dialogResult.Canceled)
return;
+ if (embeddingProvider.IsEnterpriseConfiguration)
+ {
+ // Only the API key changed, and the dialog already stored it directly. The provider
+ // object itself is managed by the configuration plugin and must not be overwritten
+ // with the dialog's copy -- doing so would let the locked-but-technically-editable
+ // fields drift from what the organization configured.
+ await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED);
+ return;
+ }
+
var editedEmbeddingProvider = (EmbeddingProvider)dialogResult.Data!;
// Set the provider number if it's not set. This is important for providers
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor
index 426929f0..c7dea430 100644
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor
+++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor
@@ -36,12 +36,18 @@
}
- @if (context.IsEnterpriseConfiguration)
+ @if (context.IsEnterpriseConfiguration && !context.AllowUserProvidedAPIKey)
{
}
+ else if (context.IsEnterpriseConfiguration && context.AllowUserProvidedAPIKey)
+ {
+
+
+
+ }
else
{
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs
index a308ab5a..eef516da 100644
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs
+++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs
@@ -67,10 +67,10 @@ public partial class SettingsPanelProviders : SettingsPanelProviderBase
{
if(provider == AIStudio.Settings.Provider.NONE)
return;
-
- if (provider.IsEnterpriseConfiguration)
+
+ if (provider.IsEnterpriseConfiguration && !provider.AllowUserProvidedAPIKey)
return;
-
+
var dialogParameters = new DialogParameters
{
{ x => x.DataNum, provider.Num },
@@ -85,6 +85,7 @@ public partial class SettingsPanelProviders : SettingsPanelProviderBase
{ x => x.HFInferenceProviderId, provider.HFInferenceProvider },
{ x => x.AdditionalJsonApiParameters, provider.AdditionalJsonApiParameters },
{ x => x.DataCapabilityOverrides, provider.CapabilityOverrides },
+ { x => x.IsEnterpriseConfiguration, provider.IsEnterpriseConfiguration },
};
var dialogReference = await this.DialogService.ShowAsync(T("Edit LLM Provider"), dialogParameters, DialogOptions.FULLSCREEN);
@@ -92,16 +93,26 @@ public partial class SettingsPanelProviders : SettingsPanelProviderBase
if (dialogResult is null || dialogResult.Canceled)
return;
+ if (provider.IsEnterpriseConfiguration)
+ {
+ // Only the API key changed, and the dialog already stored it directly. The provider
+ // object itself is managed by the configuration plugin and must not be overwritten
+ // with the dialog's copy -- doing so would let the locked-but-technically-editable
+ // fields drift from what the organization configured.
+ await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED);
+ return;
+ }
+
var editedProvider = (AIStudio.Settings.Provider)dialogResult.Data!;
-
+
// Set the provider number if it's not set. This is important for providers
// added before we started saving the provider number.
if(editedProvider.Num == 0)
editedProvider = editedProvider with { Num = this.SettingsManager.ConfigurationData.NextProviderNum++ };
-
+
this.SettingsManager.ConfigurationData.Providers[this.SettingsManager.ConfigurationData.Providers.IndexOf(provider)] = editedProvider;
await this.UpdateProviders();
-
+
await this.SettingsManager.StoreSettings();
await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED);
}
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor
index 93d22ba1..40ec1a82 100644
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor
+++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor
@@ -41,12 +41,18 @@
}
- @if (context.IsEnterpriseConfiguration)
+ @if (context.IsEnterpriseConfiguration && !context.AllowUserProvidedAPIKey)
{
}
+ else if (context.IsEnterpriseConfiguration && context.AllowUserProvidedAPIKey)
+ {
+
+
+
+ }
else
{
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor.cs
index 25e3120b..4d8daa15 100644
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor.cs
+++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor.cs
@@ -72,6 +72,9 @@ public partial class SettingsPanelTranscription : SettingsPanelProviderBase
private async Task EditTranscriptionProvider(TranscriptionProvider transcriptionProvider)
{
+ if (transcriptionProvider.IsEnterpriseConfiguration && !transcriptionProvider.AllowUserProvidedAPIKey)
+ return;
+
var dialogParameters = new DialogParameters
{
{ x => x.DataNum, transcriptionProvider.Num },
@@ -83,13 +86,24 @@ public partial class SettingsPanelTranscription : SettingsPanelProviderBase
{ x => x.IsSelfHosted, transcriptionProvider.IsSelfHosted },
{ x => x.IsEditing, true },
{ x => x.DataHost, transcriptionProvider.Host },
+ { x => x.IsEnterpriseConfiguration, transcriptionProvider.IsEnterpriseConfiguration },
};
-
+
var dialogReference = await this.DialogService.ShowAsync(T("Edit Transcription Provider"), dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled)
return;
-
+
+ if (transcriptionProvider.IsEnterpriseConfiguration)
+ {
+ // Only the API key changed, and the dialog already stored it directly. The provider
+ // object itself is managed by the configuration plugin and must not be overwritten
+ // with the dialog's copy -- doing so would let the locked-but-technically-editable
+ // fields drift from what the organization configured.
+ await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED);
+ return;
+ }
+
var editedTranscriptionProvider = (TranscriptionProvider)dialogResult.Data!;
// Set the provider number if it's not set. This is important for providers
diff --git a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor
index 85e6e6ef..1abb680e 100644
--- a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor
+++ b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor
@@ -4,10 +4,16 @@
+ @if (this.IsEnterpriseConfiguration)
+ {
+
+ @T("This embedding provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below.")
+
+ }
@* ReSharper disable once CSharpWarnings::CS8974 *@
-
+
@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())
{
-
+
@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
{
-
+
@T("Load")
@if(this.availableModels.Count is 0)
@@ -87,7 +95,7 @@
}
else
{
-
@@ -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"
/>
diff --git a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs
index 4f7d39ab..f2fb87f6 100644
--- a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs
+++ b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs
@@ -68,7 +68,14 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
///
[Parameter]
public bool IsEditing { get; init; }
-
+
+ ///
+ /// 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.
+ ///
+ [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));
diff --git a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor
index 85795de9..b896837e 100644
--- a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor
+++ b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor
@@ -4,6 +4,12 @@
@inherits MSGComponentBase
+ @if (this.IsEnterpriseConfiguration)
+ {
+
+ @T("This provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below.")
+
+ }
@* 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")
-
+
@if (this.DataLLMProvider.IsAPIKeyNeeded(this.DataHost))
{
@@ -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())
{
-
+
@foreach (Host host in Enum.GetValues(typeof(Host)))
{
@if (host.IsChatSupported())
@@ -64,7 +72,7 @@
@if (this.DataLLMProvider.IsHFInstanceProviderNeeded())
{
-
+
@foreach (HFInferenceProvider inferenceProvider in Enum.GetValues(typeof(HFInferenceProvider)))
{
@@ -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
{
-
+
@T("Load models")
@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)
{
@@ -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 @@
@T("Reset")
@@ -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)
{
@@ -229,7 +241,7 @@
@string.Format(T("The current model uses the {0}."), this.GetCurrentModelApiLabel())
-
+
diff --git a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs
index bd88ba21..ef14b7bb 100644
--- a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs
+++ b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs
@@ -90,6 +90,13 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
///
[Parameter]
public bool IsEditing { get; init; }
+
+ ///
+ /// Whether this provider is managed by an enterprise configuration plugin. When true, every
+ /// field except the API key is locked, matching Settings.Provider.IsEnterpriseConfiguration.
+ ///
+ [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));
diff --git a/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor b/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor
index 78d2dea2..dd7fb33a 100644
--- a/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor
+++ b/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor
@@ -4,10 +4,16 @@
+ @if (this.IsEnterpriseConfiguration)
+ {
+
+ @T("This transcription provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below.")
+
+ }
@* ReSharper disable once CSharpWarnings::CS8974 *@
-
+
@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())
{
-
+
@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
{
-
+
@T("Load")
@if(this.availableModels.Count is 0)
@@ -89,7 +97,7 @@
}
else
{
-
@@ -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"
/>
diff --git a/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor.cs
index b75ff07d..9132c9e6 100644
--- a/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor.cs
+++ b/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor.cs
@@ -68,7 +68,14 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId
///
[Parameter]
public bool IsEditing { get; init; }
-
+
+ ///
+ /// 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.
+ ///
+ [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));
diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua
index f0337abb..453c980c 100644
--- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua
+++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua
@@ -119,6 +119,14 @@ CONFIG["LLM_PROVIDERS"] = {}
-- -- You can export an encrypted API key from an existing provider using the export button in the settings.
-- -- ["APIKey"] = "ENC:v1:",
--
+-- -- Optional: let each user set their own API key for this otherwise locked provider,
+-- -- instead of (or in addition to not) embedding one centrally. Host, model, instance
+-- -- name, and every other field stay locked; only the API key becomes editable.
+-- -- Mutually exclusive with "APIKey" above: when both are set, the embedded key is
+-- -- ignored and a warning is logged. The user's key is preserved in the OS keyring even
+-- -- if this configuration is later withdrawn.
+-- -- ["AllowUserProvidedAPIKey"] = true,
+--
-- ["Model"] = {
-- ["Id"] = "",
-- ["DisplayName"] = "",
@@ -141,6 +149,11 @@ CONFIG["TRANSCRIPTION_PROVIDERS"] = {}
-- -- Optional: Encrypted API key (see LLM_PROVIDERS example for details)
-- -- ["APIKey"] = "ENC:v1:",
--
+-- -- Optional: let each user set their own API key for this otherwise locked transcription
+-- -- provider (see LLM_PROVIDERS example for details). Mutually exclusive with "APIKey"
+-- -- above: when both are set, the embedded key is ignored and a warning is logged.
+-- -- ["AllowUserProvidedAPIKey"] = true,
+--
-- ["Model"] = {
-- ["Id"] = "",
-- ["DisplayName"] = "",
@@ -163,6 +176,11 @@ CONFIG["EMBEDDING_PROVIDERS"] = {}
-- -- Optional: Encrypted API key (see LLM_PROVIDERS example for details)
-- -- ["APIKey"] = "ENC:v1:",
--
+-- -- Optional: let each user set their own API key for this otherwise locked embedding
+-- -- provider (see LLM_PROVIDERS example for details). Mutually exclusive with "APIKey"
+-- -- above: when both are set, the embedded key is ignored and a warning is logged.
+-- -- ["AllowUserProvidedAPIKey"] = true,
+--
-- ["Model"] = {
-- ["Id"] = "",
-- ["DisplayName"] = "",
diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua
index b3b2c0ed..9e567b6e 100644
--- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua
+++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua
@@ -4296,6 +4296,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T18253
-- Add Embedding Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T190634634"] = "Einbettungsanbieter hinzufügen"
+-- This embedding provider is managed by your organization. You can set your own API key.
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1931890418"] = "Dieser Anbieter für Einbettungen wird von Ihrer Organisation verwaltet. Sie können Ihren eigenen API-Schlüssel festlegen."
+
-- Add text that should be embedded:
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1992646324"] = "Text zum Einbetten eingeben:"
@@ -4428,6 +4431,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T426925
-- This self-hosted provider is trusted for data source security checks.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T485526152"] = "Dieser selbstgehostete Anbieter ist für Sicherheitsprüfungen von Datenquellen vertrauenswürdig."
+-- This provider is managed by your organization. You can set your own API key.
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T579100747"] = "Dieser Anbieter wird von Ihrer Organisation verwaltet. Sie können Ihren eigenen API-Schlüssel einrichten."
+
-- Open Dashboard
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T78223861"] = "Dashboard öffnen"
@@ -4482,6 +4488,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T58
-- This transcription provider is trusted by your organization for data source security checks.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T601264181"] = "Ihre Organisation vertraut diesem Anbieter für Transkriptionen bei der Sicherheitsprüfung von Datenquellen."
+-- This transcription provider is managed by your organization. You can set your own API key.
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T690752279"] = "Dieser Anbieter für Transkriptionen wird von Ihrer Organisation verwaltet. Sie können Ihren eigenen API-Schlüssel festlegen."
+
-- This transcription provider is managed by your organization.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T756131076"] = "Dieser Anbieter für Transkriptionen wird von Ihrer Organisation verwaltet."
@@ -5778,6 +5787,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T2189814010"] = "Mo
-- (Optional) API Key
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T2331453405"] = "(Optional) API-Schlüssel"
+-- Failed to remove the API key from the operating system. The message was: {0}. Please try again.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T2439094236"] = "Der API-Schlüssel konnte nicht aus dem Betriebssystem entfernt werden. Die Meldung lautete: {0}. Bitte versuchen Sie es erneut."
+
+-- This embedding provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T2555207324"] = "Dieser Anbieter für Einbettungen wird von Ihrer Organisation verwaltet. Host, Modell und andere Einstellungen sind gesperrt. Unten können Sie Ihren eigenen API-Schlüssel festlegen."
+
-- Add
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T2646845972"] = "Hinzufügen"
@@ -6078,6 +6093,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROMPTINGGUIDELINEDIALOG::T4250996615"] = "P
-- Hugging Face Inference Provider
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1085481431"] = "Hugging Face Inferenz-Anbieter"
+-- This provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1090492389"] = "Dieser Provider wird von Ihrer Organisation verwaltet. Host, Modell und andere Einstellungen sind gesperrt. Sie können Ihren eigenen API-Schlüssel unten festlegen."
+
-- Hide Expert Settings
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1108876344"] = "Experten-Einstellungen ausblenden"
@@ -6141,6 +6159,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2189814010"] = "Modell"
-- (Optional) API Key
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2331453405"] = "(Optional) API-Schlüssel"
+-- Failed to remove the API key from the operating system. The message was: {0}. Please try again.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2439094236"] = "Fehler beim Löschen des API-Schlüssels vom Betriebssystem. Die Nachricht war: {0}. Bitte versuchen Sie es erneut."
+
-- Enabled
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2626085950"] = "Aktiviert"
@@ -7773,6 +7794,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T2189814010"] =
-- (Optional) API Key
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T2331453405"] = "(Optional) API-Schlüssel"
+-- Failed to remove the API key from the operating system. The message was: {0}. Please try again.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T2439094236"] = "Der API-Schlüssel konnte nicht aus dem Betriebssystem entfernt werden. Die Meldung lautete: {0}. Bitte versuchen Sie es erneut."
+
-- Add
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T2646845972"] = "Hinzufügen"
@@ -7797,6 +7821,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T504465522"] =
-- Host
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T808120719"] = "Host"
+-- This transcription provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T828088153"] = "Dieser Anbieter für Transkriptionen wird von Ihrer Organisation verwaltet. Host, Modell und weitere Einstellungen sind gesperrt. Sie können unten Ihren eigenen API-Schlüssel festlegen."
+
-- Provider
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T900237532"] = "Anbieter"
diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua
index fa9d9a8c..932d6199 100644
--- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua
+++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua
@@ -4296,6 +4296,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T18253
-- Add Embedding Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T190634634"] = "Add Embedding Provider"
+-- This embedding provider is managed by your organization. You can set your own API key.
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1931890418"] = "This embedding provider is managed by your organization. You can set your own API key."
+
-- Add text that should be embedded:
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1992646324"] = "Add text that should be embedded:"
@@ -4428,6 +4431,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T426925
-- This self-hosted provider is trusted for data source security checks.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T485526152"] = "This self-hosted provider is trusted for data source security checks."
+-- This provider is managed by your organization. You can set your own API key.
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T579100747"] = "This provider is managed by your organization. You can set your own API key."
+
-- Open Dashboard
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T78223861"] = "Open Dashboard"
@@ -4482,6 +4488,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T58
-- This transcription provider is trusted by your organization for data source security checks.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T601264181"] = "This transcription provider is trusted by your organization for data source security checks."
+-- This transcription provider is managed by your organization. You can set your own API key.
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T690752279"] = "This transcription provider is managed by your organization. You can set your own API key."
+
-- This transcription provider is managed by your organization.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T756131076"] = "This transcription provider is managed by your organization."
@@ -5778,6 +5787,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T2189814010"] = "Mo
-- (Optional) API Key
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T2331453405"] = "(Optional) API Key"
+-- Failed to remove the API key from the operating system. The message was: {0}. Please try again.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T2439094236"] = "Failed to remove the API key from the operating system. The message was: {0}. Please try again."
+
+-- This embedding provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T2555207324"] = "This embedding provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below."
+
-- Add
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T2646845972"] = "Add"
@@ -6078,6 +6093,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROMPTINGGUIDELINEDIALOG::T4250996615"] = "P
-- Hugging Face Inference Provider
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1085481431"] = "Hugging Face Inference Provider"
+-- This provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1090492389"] = "This provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below."
+
-- Hide Expert Settings
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1108876344"] = "Hide Expert Settings"
@@ -6141,6 +6159,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2189814010"] = "Model"
-- (Optional) API Key
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2331453405"] = "(Optional) API Key"
+-- Failed to remove the API key from the operating system. The message was: {0}. Please try again.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2439094236"] = "Failed to remove the API key from the operating system. The message was: {0}. Please try again."
+
-- Enabled
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2626085950"] = "Enabled"
@@ -7773,6 +7794,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T2189814010"] =
-- (Optional) API Key
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T2331453405"] = "(Optional) API Key"
+-- Failed to remove the API key from the operating system. The message was: {0}. Please try again.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T2439094236"] = "Failed to remove the API key from the operating system. The message was: {0}. Please try again."
+
-- Add
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T2646845972"] = "Add"
@@ -7797,6 +7821,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T504465522"] =
-- Host
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T808120719"] = "Host"
+-- This transcription provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T828088153"] = "This transcription provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below."
+
-- Provider
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T900237532"] = "Provider"
diff --git a/app/MindWork AI Studio/Settings/EmbeddingProvider.cs b/app/MindWork AI Studio/Settings/EmbeddingProvider.cs
index 8d153f39..b8306d58 100644
--- a/app/MindWork AI Studio/Settings/EmbeddingProvider.cs
+++ b/app/MindWork AI Studio/Settings/EmbeddingProvider.cs
@@ -20,7 +20,8 @@ public sealed record EmbeddingProvider(
bool IsEnterpriseConfiguration = false,
Guid EnterpriseConfigurationPluginId = default,
string Hostname = "http://localhost:1234",
- Host Host = Host.NONE) : ConfigurationBaseObject, ISecretId
+ Host Host = Host.NONE,
+ bool AllowUserProvidedAPIKey = false) : ConfigurationBaseObject, ISecretId, IUserProvidedAPIKey
{
private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger();
@@ -97,6 +98,10 @@ public sealed record EmbeddingProvider(
return false;
}
+ var allowUserProvidedApiKey = false;
+ if (table.TryGetValue("AllowUserProvidedAPIKey", out var allowUserProvidedApiKeyValue) && allowUserProvidedApiKeyValue.TryRead(out var allowUserProvidedApiKeyBool))
+ allowUserProvidedApiKey = allowUserProvidedApiKeyBool;
+
provider = new EmbeddingProvider
{
Num = 0, // will be set later by the PluginConfigurationObject
@@ -109,10 +114,18 @@ public sealed record EmbeddingProvider(
EnterpriseConfigurationPluginId = configPluginId,
Hostname = hostname,
Host = host,
+ AllowUserProvidedAPIKey = allowUserProvidedApiKey,
};
- // Handle encrypted API key if present:
- if (table.TryGetValue("APIKey", out var apiKeyValue) && apiKeyValue.TryRead(out var apiKeyText) && !string.IsNullOrWhiteSpace(apiKeyText))
+ // Handle an encrypted API key if present. When the user manages their own key for this
+ // embedding provider, we must never enqueue an embedded key: doing so would overwrite the
+ // user's key in the OS keyring on every configuration reload.
+ if (allowUserProvidedApiKey)
+ {
+ if (table.TryGetValue("APIKey", out var ignoredApiKeyValue) && ignoredApiKeyValue.TryRead(out var ignoredApiKeyText) && !string.IsNullOrWhiteSpace(ignoredApiKeyText))
+ LOGGER.LogWarning($"The configured embedding provider {idx} sets both AllowUserProvidedAPIKey and an embedded APIKey. Ignoring the embedded key: the user manages their own key for this provider. (Plugin ID: {configPluginId})");
+ }
+ else if (table.TryGetValue("APIKey", out var apiKeyValue) && apiKeyValue.TryRead(out var apiKeyText) && !string.IsNullOrWhiteSpace(apiKeyText))
{
if (!EnterpriseEncryption.IsEncrypted(apiKeyText))
LOGGER.LogWarning($"The configured embedding provider {idx} contains a plaintext API key. Only encrypted API keys (starting with 'ENC:v1:') are supported. (Plugin ID: {configPluginId})");
diff --git a/app/MindWork AI Studio/Settings/Provider.cs b/app/MindWork AI Studio/Settings/Provider.cs
index 4662b1b1..fdf63a34 100644
--- a/app/MindWork AI Studio/Settings/Provider.cs
+++ b/app/MindWork AI Studio/Settings/Provider.cs
@@ -21,6 +21,7 @@ namespace AIStudio.Settings;
/// Whether the provider is self-hosted.
/// The hostname of the provider. Useful for self-hosted providers.
/// The LLM model to use for chat.
+/// When set by a configuration plugin, the user may set their own API key for this otherwise locked, enterprise-managed provider.
public sealed record Provider(
uint Num,
string Id,
@@ -34,7 +35,8 @@ public sealed record Provider(
Host Host = Host.NONE,
HFInferenceProvider HFInferenceProvider = HFInferenceProvider.NONE,
string AdditionalJsonApiParameters = "",
- ProviderCapabilityOverrides? CapabilityOverrides = null) : ConfigurationBaseObject, ISecretId
+ ProviderCapabilityOverrides? CapabilityOverrides = null,
+ bool AllowUserProvidedAPIKey = false) : ConfigurationBaseObject, ISecretId, IUserProvidedAPIKey
{
private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger();
@@ -155,6 +157,10 @@ public sealed record Provider(
var capabilityOverrides = ProviderCapabilityOverrides.TryParseFromLuaTable(idx, table, configPluginId, LOGGER);
+ var allowUserProvidedApiKey = false;
+ if (table.TryGetValue("AllowUserProvidedAPIKey", out var allowUserProvidedApiKeyValue) && allowUserProvidedApiKeyValue.TryRead(out var allowUserProvidedApiKeyBool))
+ allowUserProvidedApiKey = allowUserProvidedApiKeyBool;
+
provider = new Provider
{
Num = 0, // will be set later by the PluginConfigurationObject
@@ -170,10 +176,18 @@ public sealed record Provider(
HFInferenceProvider = hfInferenceProvider,
AdditionalJsonApiParameters = additionalJsonApiParameters,
CapabilityOverrides = capabilityOverrides,
+ AllowUserProvidedAPIKey = allowUserProvidedApiKey,
};
- // Handle encrypted API key if present:
- if (table.TryGetValue("APIKey", out var apiKeyValue) && apiKeyValue.TryRead(out var apiKeyText) && !string.IsNullOrWhiteSpace(apiKeyText))
+ // Handle an encrypted API key if present. When the user manages their own key for this
+ // provider, we must never enqueue an embedded key: doing so would overwrite the user's
+ // key in the OS keyring on every configuration reload.
+ if (allowUserProvidedApiKey)
+ {
+ if (table.TryGetValue("APIKey", out var ignoredApiKeyValue) && ignoredApiKeyValue.TryRead(out var ignoredApiKeyText) && !string.IsNullOrWhiteSpace(ignoredApiKeyText))
+ LOGGER.LogWarning($"The configured provider {idx} sets both AllowUserProvidedAPIKey and an embedded APIKey. Ignoring the embedded key: the user manages their own key for this provider. (Plugin ID: {configPluginId})");
+ }
+ else if (table.TryGetValue("APIKey", out var apiKeyValue) && apiKeyValue.TryRead(out var apiKeyText) && !string.IsNullOrWhiteSpace(apiKeyText))
{
if (!EnterpriseEncryption.IsEncrypted(apiKeyText))
LOGGER.LogWarning($"The configured provider {idx} contains a plaintext API key. Only encrypted API keys (starting with 'ENC:v1:') are supported. (Plugin ID: {configPluginId})");
diff --git a/app/MindWork AI Studio/Settings/TranscriptionProvider.cs b/app/MindWork AI Studio/Settings/TranscriptionProvider.cs
index 973cd138..5afae679 100644
--- a/app/MindWork AI Studio/Settings/TranscriptionProvider.cs
+++ b/app/MindWork AI Studio/Settings/TranscriptionProvider.cs
@@ -20,7 +20,8 @@ public sealed record TranscriptionProvider(
bool IsEnterpriseConfiguration = false,
Guid EnterpriseConfigurationPluginId = default,
string Hostname = "http://localhost:1234",
- Host Host = Host.NONE) : ConfigurationBaseObject, ISecretId
+ Host Host = Host.NONE,
+ bool AllowUserProvidedAPIKey = false) : ConfigurationBaseObject, ISecretId, IUserProvidedAPIKey
{
private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger();
@@ -97,6 +98,10 @@ public sealed record TranscriptionProvider(
return false;
}
+ var allowUserProvidedApiKey = false;
+ if (table.TryGetValue("AllowUserProvidedAPIKey", out var allowUserProvidedApiKeyValue) && allowUserProvidedApiKeyValue.TryRead(out var allowUserProvidedApiKeyBool))
+ allowUserProvidedApiKey = allowUserProvidedApiKeyBool;
+
provider = new TranscriptionProvider
{
Num = 0, // will be set later by the PluginConfigurationObject
@@ -109,10 +114,18 @@ public sealed record TranscriptionProvider(
EnterpriseConfigurationPluginId = configPluginId,
Hostname = hostname,
Host = host,
+ AllowUserProvidedAPIKey = allowUserProvidedApiKey,
};
- // Handle encrypted API key if present:
- if (table.TryGetValue("APIKey", out var apiKeyValue) && apiKeyValue.TryRead(out var apiKeyText) && !string.IsNullOrWhiteSpace(apiKeyText))
+ // Handle an encrypted API key if present. When the user manages their own key for this
+ // transcription provider, we must never enqueue an embedded key: doing so would overwrite
+ // the user's key in the OS keyring on every configuration reload.
+ if (allowUserProvidedApiKey)
+ {
+ if (table.TryGetValue("APIKey", out var ignoredApiKeyValue) && ignoredApiKeyValue.TryRead(out var ignoredApiKeyText) && !string.IsNullOrWhiteSpace(ignoredApiKeyText))
+ LOGGER.LogWarning($"The configured transcription provider {idx} sets both AllowUserProvidedAPIKey and an embedded APIKey. Ignoring the embedded key: the user manages their own key for this provider. (Plugin ID: {configPluginId})");
+ }
+ else if (table.TryGetValue("APIKey", out var apiKeyValue) && apiKeyValue.TryRead(out var apiKeyText) && !string.IsNullOrWhiteSpace(apiKeyText))
{
if (!EnterpriseEncryption.IsEncrypted(apiKeyText))
LOGGER.LogWarning($"The configured transcription provider {idx} contains a plaintext API key. Only encrypted API keys (starting with 'ENC:v1:') are supported. (Plugin ID: {configPluginId})");
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/IUserProvidedAPIKey.cs b/app/MindWork AI Studio/Tools/PluginSystem/IUserProvidedAPIKey.cs
new file mode 100644
index 00000000..db0cb054
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/IUserProvidedAPIKey.cs
@@ -0,0 +1,15 @@
+namespace AIStudio.Tools.PluginSystem;
+
+///
+/// Represents a configuration object whose API key is managed by the user, although the object
+/// itself is managed by a configuration plugin. Implemented by all provider kinds which support
+/// the "AllowUserProvidedAPIKey" option, i.e., LLM, embedding, and transcription providers.
+///
+public interface IUserProvidedAPIKey
+{
+ ///
+ /// When set by a configuration plugin, the user may set their own API key for this otherwise
+ /// locked, enterprise-managed object.
+ ///
+ public bool AllowUserProvidedAPIKey { get; }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs
index 40f45617..07b4e3e8 100644
--- a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs
@@ -407,6 +407,13 @@ public sealed record PluginConfigurationObject
else
LOG.LogWarning($"Failed to delete secret for removed enterprise object '{item.Name}' from the OS keyring: {deleteResult.Issue}");
}
+ else if(item is IUserProvidedAPIKey { AllowUserProvidedAPIKey: true })
+ {
+ // The user manages their own key for this provider. Keep it in the OS keyring
+ // in case the organization's configuration comes back later, instead of forcing
+ // the user to re-enter it:
+ LOG.LogInformation($"Preserving the user-provided API key for removed enterprise provider '{item.Name}' in the OS keyring.");
+ }
else if(secretStoreType is not null && item is ISecretId secretId)
{
var deleteResult = await RustService.DeleteAPIKey(secretId, secretStoreType.Value);
diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md b/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md
index b1c44de3..c6b09e24 100644
--- a/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md
+++ b/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md
@@ -1 +1,3 @@
# v26.8.2, build 252 (2026-08-xx xx:xx UTC)
+- Added providers for which you bring your own API key. Until now, a provider your organization configured had to come with a shared API key, which meant your IT department needed one key for everybody. Such a provider can now be handed out without a key, so that everyone signs in with their own, for example, a personal OpenAI or Anthropic account. This works for chat, embedding, and transcription providers alike. The provider stays managed by your organization: the host, the model, the instance name, and everything else remain fixed, and the only thing you can edit is the API key. In the settings, these providers carry a key icon instead of the usual lock, so you can see at a glance where you have to add your key; for embedding providers, the test button stays available, so you can check your key right after entering it. Your key is stored on your device in the operating system's credential store, and it stays there even when your organization withdraws the provider later, so it is still in place should the same provider return. For IT departments: the new `AllowUserProvidedAPIKey` option does this, and it works for `LLM_PROVIDERS`, `EMBEDDING_PROVIDERS`, and `TRANSCRIPTION_PROVIDERS` alike.
+- Fixed a removed API key staying in the operating system's credential store. When you cleared the API key of a provider, the previous key remained stored and was still used. It is now removed together with your change.
\ No newline at end of file
diff --git a/documentation/Enterprise IT.md b/documentation/Enterprise IT.md
index 8f874ac4..b517f581 100644
--- a/documentation/Enterprise IT.md
+++ b/documentation/Enterprise IT.md
@@ -535,3 +535,77 @@ CONFIG["LLM_PROVIDERS"][#CONFIG["LLM_PROVIDERS"]+1] = {
```
The API key will be automatically decrypted when the configuration is loaded and stored securely in the operating system's credential store (Windows Credential Manager / macOS Keychain).
+
+## Letting users provide their own API key
+
+Sometimes you want to hand out a preconfigured provider -- a fixed host, model, and instance name
+-- without embedding a shared API key for it. Each user then brings their own key, for example
+their personal OpenAI or Anthropic account, while everything else about the provider stays exactly
+as your organization configured it.
+
+Set `AllowUserProvidedAPIKey` on the provider:
+
+```lua
+CONFIG["LLM_PROVIDERS"][#CONFIG["LLM_PROVIDERS"]+1] = {
+ ["Id"] = "9072b77d-ca81-40da-be6a-861da525ef7b",
+ ["InstanceName"] = "Corporate OpenAI GPT-4",
+ ["UsedLLMProvider"] = "OPEN_AI",
+ ["Host"] = "NONE",
+ ["Hostname"] = "",
+ ["AllowUserProvidedAPIKey"] = true,
+ ["AdditionalJsonApiParameters"] = "",
+ ["Model"] = {
+ ["Id"] = "gpt-4",
+ ["DisplayName"] = "GPT-4",
+ }
+}
+```
+
+With `AllowUserProvidedAPIKey` set, the provider still shows up as managed by your organization,
+and users still cannot change the host, model, instance name, or any other field. The settings
+page shows a key icon instead of the usual lock icon for this provider; opening it only offers the
+API key field, with everything else disabled.
+
+The flag works the same way for embedding and transcription providers:
+
+```lua
+CONFIG["EMBEDDING_PROVIDERS"][#CONFIG["EMBEDDING_PROVIDERS"]+1] = {
+ ["Id"] = "3f0a4e8c-1d6b-4a91-8f2e-7c5d9b0a4e13",
+ ["Name"] = "Corporate Embeddings",
+ ["UsedLLMProvider"] = "OPEN_AI",
+ ["Host"] = "NONE",
+ ["Hostname"] = "",
+ ["AllowUserProvidedAPIKey"] = true,
+ ["Model"] = {
+ ["Id"] = "text-embedding-3-large",
+ ["DisplayName"] = "Text Embedding 3 Large",
+ }
+}
+
+CONFIG["TRANSCRIPTION_PROVIDERS"][#CONFIG["TRANSCRIPTION_PROVIDERS"]+1] = {
+ ["Id"] = "b1c7d24f-5e83-4a06-9d1b-2f8e6a3c7d50",
+ ["Name"] = "Corporate Transcription",
+ ["UsedLLMProvider"] = "OPEN_AI",
+ ["Host"] = "NONE",
+ ["Hostname"] = "",
+ ["AllowUserProvidedAPIKey"] = true,
+ ["Model"] = {
+ ["Id"] = "whisper-1",
+ ["DisplayName"] = "Whisper",
+ }
+}
+```
+
+For embedding providers, the settings page keeps the test button available next to the key icon, so
+users can verify their own key right after entering it.
+
+This is mutually exclusive with an embedded `APIKey` on the same provider: if both are present,
+AI Studio ignores the embedded key and logs a warning, because the whole point of the flag is that
+each user manages their own key. Combine the two across different providers if you need it -- one
+provider with a shared, embedded key and another with `AllowUserProvidedAPIKey` -- but not on the
+same provider.
+
+The user's key follows the same "withdrawing a configuration" philosophy as everything else in this
+document: if your configuration stops offering this provider, AI Studio removes the provider from
+the settings but leaves the user's key in the OS keyring rather than deleting it, in case the same
+provider comes back later. See [Withdrawing a configuration](#withdrawing-a-configuration).