mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-08-16 01:02:11 +00:00
Added per-provider user-managed API keys for enterprise-configured LLM providers
This commit is contained in:
parent
bb8f6f13f0
commit
84763f3907
@ -4429,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"
|
||||
|
||||
@ -6085,6 +6088,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"
|
||||
|
||||
|
||||
@ -37,12 +37,18 @@
|
||||
<MudIconButton Color="Color.Success" Icon="@Icons.Material.Filled.VerifiedUser" Disabled="true"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
@if (context.IsEnterpriseConfiguration)
|
||||
@if (context.IsEnterpriseConfiguration && !context.AllowUserProvidedAPIKey)
|
||||
{
|
||||
<MudTooltip Text="@T("This provider is managed by your organization.")">
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Business" Disabled="true"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
else if (context.IsEnterpriseConfiguration && context.AllowUserProvidedAPIKey)
|
||||
{
|
||||
<MudTooltip Text="@T("This provider is managed by your organization. You can set your own API key.")">
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Key" OnClick="@(() => this.EditLLMProvider(context))"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTooltip Text="@T("Open Dashboard")">
|
||||
|
||||
@ -55,10 +55,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<ProviderDialog>
|
||||
{
|
||||
{ x => x.DataNum, provider.Num },
|
||||
@ -73,6 +73,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<ProviderDialog>(T("Edit LLM Provider"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
@ -80,16 +81,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<bool>(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<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
|
||||
@ -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 <see cref="Settings.Provider.IsEnterpriseConfiguration"/>.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool IsEnterpriseConfiguration { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string AdditionalJsonApiParameters { get; set; } = string.Empty;
|
||||
@ -170,7 +177,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,
|
||||
@ -234,7 +241,10 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
|
||||
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();
|
||||
@ -259,8 +269,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
|
||||
|
||||
@ -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:<base64-encoded encrypted data>",
|
||||
--
|
||||
-- -- 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"] = "<the model ID>",
|
||||
-- ["DisplayName"] = "<user-friendly name of the model>",
|
||||
|
||||
@ -4431,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"
|
||||
|
||||
@ -6087,6 +6090,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"
|
||||
|
||||
|
||||
@ -21,6 +21,7 @@ namespace AIStudio.Settings;
|
||||
/// <param name="IsSelfHosted">Whether the provider is self-hosted.</param>
|
||||
/// <param name="Hostname">The hostname of the provider. Useful for self-hosted providers.</param>
|
||||
/// <param name="Model">The LLM model to use for chat.</param>
|
||||
/// <param name="AllowUserProvidedAPIKey">When set by a configuration plugin, the user may set their own API key for this otherwise locked, enterprise-managed provider.</param>
|
||||
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
|
||||
{
|
||||
private static readonly ILogger<Provider> LOGGER = Program.LOGGER_FACTORY.CreateLogger<Provider>();
|
||||
|
||||
@ -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<bool>(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<string>(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<string>(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<string>(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})");
|
||||
|
||||
@ -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 Settings.Provider { 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);
|
||||
|
||||
@ -535,3 +535,44 @@ 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.
|
||||
|
||||
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).
|
||||
|
||||
Loading…
Reference in New Issue
Block a user