Added a home page prompt for missing API keys of enterprise providers (#1056)

Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
Dominic NeuburgandThorsten Sommer authored and GitHub committed 2026-10-19 09:12:12 +02:00
1 parent 1f8d72aa45
commit 7d35536c87
29 files changed
+920 -31

No files matched your search

@@ -0,0 +1,63 @@
using AIStudio.Tools.PluginSystem;
using Lua;
namespace AIStudio.Settings.DataModel;
/// <summary>
/// What a configuration plugin tells the user while AI Studio asks for the API key of the providers
/// which that plugin marked with "PromptForUserAPIKey".
/// </summary>
public sealed record DataUserAPIKeyPrompt : ILivePluginContent
{
private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger<DataUserAPIKeyPrompt>();
/// <summary>
/// The stable ID of the prompt. A configuration plugin has at most one, so this is the plugin's ID.
/// </summary>
public string Id { get; private init; } = string.Empty;
/// <summary>
/// The ID of the enterprise configuration plugin that provides this prompt.
/// </summary>
public Guid EnterpriseConfigurationPluginId { get; private init; } = Guid.Empty;
/// <summary>
/// The optional title shown to the user. When empty, AI Studio shows a title of its own.
/// </summary>
public string Title { get; private init; } = string.Empty;
/// <summary>
/// The Markdown instructions which explain how to obtain the API key.
/// </summary>
public string Markdown { get; private init; } = string.Empty;
public static bool TryParseConfiguration(LuaTable table, Guid configPluginId, out DataUserAPIKeyPrompt prompt)
{
prompt = new DataUserAPIKeyPrompt();
if (!table.TryGetValue("Markdown", out var markdownValue) || !markdownValue.TryRead<string>(out var markdown) || string.IsNullOrWhiteSpace(markdown))
{
LOG.LogWarning("The configured user API key prompt does not contain a valid Markdown field (config plugin id: {ConfigPluginId}).", configPluginId);
return false;
}
var title = string.Empty;
if (table.TryGetValue("Title", out var titleValue))
{
if (titleValue.TryRead<string>(out var titleText))
title = titleText;
else
LOG.LogWarning("The configured user API key prompt does not contain a valid Title field. Falling back to the built-in title (config plugin id: {ConfigPluginId}).", configPluginId);
}
prompt = new DataUserAPIKeyPrompt
{
Id = configPluginId.ToString(),
Title = title.Trim(),
Markdown = AIStudio.Tools.Markdown.RemoveSharedIndentation(markdown),
EnterpriseConfigurationPluginId = configPluginId,
};
return true;
}
}
@@ -29,7 +29,8 @@ public sealed record EmbeddingProvider(
int TokenLimit = 0,
bool AllowUserProvidedAPIKey = false,
string CustomIconDataUrl = "",
HFInferenceProvider HFInferenceProvider = HFInferenceProvider.NONE) : ConfigurationBaseObject, ISecretId, IUserProvidedAPIKey
HFInferenceProvider HFInferenceProvider = HFInferenceProvider.NONE,
bool PromptForUserAPIKey = false) : ConfigurationBaseObject, ISecretId, IUserProvidedAPIKey
{
public const int DEFAULT_TOKEN_LIMIT = 8192;
public const int DEFAULT_EMBEDDING_BATCH_SIZE = 1;
@@ -128,9 +129,7 @@ public sealed record EmbeddingProvider(
embeddingBatchSize = DEFAULT_EMBEDDING_BATCH_SIZE;
}
var allowUserProvidedApiKey = false;
if (table.TryGetValue("AllowUserProvidedAPIKey", out var allowUserProvidedApiKeyValue) && allowUserProvidedApiKeyValue.TryRead<bool>(out var allowUserProvidedApiKeyBool))
allowUserProvidedApiKey = allowUserProvidedApiKeyBool;
var apiKeyOptions = UserProvidedAPIKeyOptions.Read(table);
var hfInferenceProvider = HFInferenceProvider.NONE;
if (table.TryGetValue("HFInferenceProvider", out var hfInferenceProviderValue) && hfInferenceProviderValue.TryRead<string>(out var hfInferenceProviderText))
@@ -166,7 +165,8 @@ public sealed record EmbeddingProvider(
TokenizerPath = tokenizerPath,
EmbeddingBatchSize = embeddingBatchSize,
TokenLimit = tokenLimit,
AllowUserProvidedAPIKey = allowUserProvidedApiKey,
AllowUserProvidedAPIKey = apiKeyOptions.AllowUserProvidedAPIKey,
PromptForUserAPIKey = apiKeyOptions.PromptForUserAPIKey,
CustomIconDataUrl = customIconDataUrl,
HFInferenceProvider = hfInferenceProvider,
};
@@ -174,7 +174,7 @@ public sealed record EmbeddingProvider(
// 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 (apiKeyOptions.AllowUserProvidedAPIKey)
{
if (table.TryGetValue("APIKey", out var ignoredApiKeyValue) && ignoredApiKeyValue.TryRead<string>(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})");
+7 -6
View File
@@ -24,6 +24,7 @@ namespace AIStudio.Settings;
/// <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>
/// <param name="CustomIconDataUrl">The validated custom SVG icon supplied by a configuration plugin.</param>
/// <param name="PromptForUserAPIKey">When set by a configuration plugin, AI Studio asks the user for the API key of this provider on the home page as long as none is stored.</param>
public sealed record Provider(
uint Num,
string Id,
@@ -40,7 +41,8 @@ public sealed record Provider(
[property: JsonConverter(typeof(DataDirectoryRelativePathConverter))] string TokenizerPath = "",
ProviderCapabilityOverrides? CapabilityOverrides = null,
bool AllowUserProvidedAPIKey = false,
string CustomIconDataUrl = "") : ConfigurationBaseObject, ISecretId, IUserProvidedAPIKey
string CustomIconDataUrl = "",
bool PromptForUserAPIKey = false) : ConfigurationBaseObject, ISecretId, IUserProvidedAPIKey
{
private static readonly ILogger<Provider> LOGGER = Program.LOGGER_FACTORY.CreateLogger<Provider>();
@@ -159,9 +161,7 @@ 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;
var apiKeyOptions = UserProvidedAPIKeyOptions.Read(table);
var customIconDataUrl = string.Empty;
if (table.TryGetValue("IconPath", out var iconPathValue))
@@ -188,14 +188,15 @@ public sealed record Provider(
AdditionalJsonApiParameters = additionalJsonApiParameters,
TokenizerPath = tokenizerPath,
CapabilityOverrides = capabilityOverrides,
AllowUserProvidedAPIKey = allowUserProvidedApiKey,
AllowUserProvidedAPIKey = apiKeyOptions.AllowUserProvidedAPIKey,
PromptForUserAPIKey = apiKeyOptions.PromptForUserAPIKey,
CustomIconDataUrl = customIconDataUrl,
};
// 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 (apiKeyOptions.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})");
@@ -24,7 +24,8 @@ public sealed record TranscriptionProvider(
Host Host = Host.NONE,
bool AllowUserProvidedAPIKey = false,
string CustomIconDataUrl = "",
HFInferenceProvider HFInferenceProvider = HFInferenceProvider.NONE) : ConfigurationBaseObject, ISecretId, IUserProvidedAPIKey
HFInferenceProvider HFInferenceProvider = HFInferenceProvider.NONE,
bool PromptForUserAPIKey = false) : ConfigurationBaseObject, ISecretId, IUserProvidedAPIKey
{
private static readonly ILogger<TranscriptionProvider> LOGGER = Program.LOGGER_FACTORY.CreateLogger<TranscriptionProvider>();
@@ -101,9 +102,7 @@ public sealed record TranscriptionProvider(
return false;
}
var allowUserProvidedApiKey = false;
if (table.TryGetValue("AllowUserProvidedAPIKey", out var allowUserProvidedApiKeyValue) && allowUserProvidedApiKeyValue.TryRead<bool>(out var allowUserProvidedApiKeyBool))
allowUserProvidedApiKey = allowUserProvidedApiKeyBool;
var apiKeyOptions = UserProvidedAPIKeyOptions.Read(table);
var hfInferenceProvider = HFInferenceProvider.NONE;
if (table.TryGetValue("HFInferenceProvider", out var hfInferenceProviderValue) && hfInferenceProviderValue.TryRead<string>(out var hfInferenceProviderText))
@@ -136,7 +135,8 @@ public sealed record TranscriptionProvider(
EnterpriseConfigurationPluginId = configPluginId,
Hostname = hostname,
Host = host,
AllowUserProvidedAPIKey = allowUserProvidedApiKey,
AllowUserProvidedAPIKey = apiKeyOptions.AllowUserProvidedAPIKey,
PromptForUserAPIKey = apiKeyOptions.PromptForUserAPIKey,
CustomIconDataUrl = customIconDataUrl,
HFInferenceProvider = hfInferenceProvider,
};
@@ -144,7 +144,7 @@ public sealed record TranscriptionProvider(
// 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 (apiKeyOptions.AllowUserProvidedAPIKey)
{
if (table.TryGetValue("APIKey", out var ignoredApiKeyValue) && ignoredApiKeyValue.TryRead<string>(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})");