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

@@ -76,19 +76,20 @@ public static class ConfigurationSnippetImport
return true;
//
// A user-provided API key is what an organization grants for a provider it manages. A
// provider of the user's own has nothing else, so the flag carries no meaning here:
// A user-provided API key, and asking the user for it, is what an organization grants for a
// provider it manages. A provider of the user's own has nothing else, so neither option carries
// any meaning here:
//
case Settings.Provider provider:
result = new(MakeLocal(provider) with { TokenizerPath = KeepLocalTokenizer(provider.TokenizerPath, hints), AllowUserProvidedAPIKey = false }, ReadAPIKey(table, hints), hints, []);
result = new(MakeLocal(provider) with { TokenizerPath = KeepLocalTokenizer(provider.TokenizerPath, hints), AllowUserProvidedAPIKey = false, PromptForUserAPIKey = false }, ReadAPIKey(table, hints), hints, []);
return true;
case EmbeddingProvider provider:
result = new(MakeLocal(provider) with { TokenizerPath = KeepLocalTokenizer(provider.TokenizerPath, hints), AllowUserProvidedAPIKey = false }, ReadAPIKey(table, hints), hints, []);
result = new(MakeLocal(provider) with { TokenizerPath = KeepLocalTokenizer(provider.TokenizerPath, hints), AllowUserProvidedAPIKey = false, PromptForUserAPIKey = false }, ReadAPIKey(table, hints), hints, []);
return true;
case TranscriptionProvider provider:
result = new(MakeLocal(provider) with { AllowUserProvidedAPIKey = false }, ReadAPIKey(table, hints), hints, []);
result = new(MakeLocal(provider) with { AllowUserProvidedAPIKey = false, PromptForUserAPIKey = false }, ReadAPIKey(table, hints), hints, []);
return true;
//
@@ -3,7 +3,8 @@ namespace AIStudio.Tools.PluginSystem;
/// <summary>
/// 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.
/// the options "AllowUserProvidedAPIKey" and "PromptForUserAPIKey", i.e., LLM, embedding, and
/// transcription providers. UserProvidedAPIKeyOptions reads both options from a plugin.
/// </summary>
public interface IUserProvidedAPIKey
{
@@ -12,4 +13,10 @@ public interface IUserProvidedAPIKey
/// locked, enterprise-managed object.
/// </summary>
public bool AllowUserProvidedAPIKey { get; }
/// <summary>
/// When set by a configuration plugin, AI Studio asks the user for the API key of this object
/// on the home page as long as none is stored. Implies AllowUserProvidedAPIKey.
/// </summary>
public bool PromptForUserAPIKey { get; }
}
@@ -18,6 +18,7 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
private List<DataMandatoryInfo> mandatoryInfos = [];
private List<DataIntroduction> introductions = [];
private List<DataMailboxProvider> mailboxProviders = [];
private List<DataUserAPIKeyPrompt> userAPIKeyPrompts = [];
/// <summary>
/// The list of configuration objects. Configuration objects are, e.g., providers or chat templates.
@@ -42,6 +43,12 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
/// </summary>
public IReadOnlyList<DataMailboxProvider> MailboxProviders => this.mailboxProviders;
/// <summary>
/// What this configuration plugin tells the user while AI Studio asks for the API key of its providers;
/// at most one entry. The prompt is live plugin content and is not persisted to ConfigurationData.
/// </summary>
public IReadOnlyList<DataUserAPIKeyPrompt> UserAPIKeyPrompts => this.userAPIKeyPrompts;
/// <summary>
/// True/false when explicitly configured in the plugin, otherwise null.
/// </summary>
@@ -204,6 +211,7 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
this.mandatoryInfos.Clear();
this.introductions.Clear();
this.mailboxProviders.Clear();
this.userAPIKeyPrompts.Clear();
// Ensure that the main CONFIG table exists and is a valid Lua table:
if (!this.State.Environment["CONFIG"].TryRead<LuaTable>(out var mainTable))
@@ -400,6 +408,9 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
// Handle configured mail servers:
this.TryReadMailboxProviders(mainTable);
// Handle the configured prompt for the API key of the user:
this.TryReadUserAPIKeyPrompt(mainTable);
// Config: preselected provider?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.PreselectedProvider, Guid.Empty, this.Id, settingsTable, dryRun);
@@ -872,4 +883,13 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
LOG.LogWarning("The table 'MAILBOX_PROVIDERS' entry at index {Index} does not contain a valid mail server (config plugin id: {ConfigPluginId}).", i, this.Id);
}
}
private void TryReadUserAPIKeyPrompt(LuaTable mainTable)
{
if (!mainTable.TryGetValue("USER_API_KEY_PROMPT", out var promptValue) || !promptValue.TryRead<LuaTable>(out var promptTable))
return;
if (DataUserAPIKeyPrompt.TryParseConfiguration(promptTable, this.Id, out var prompt))
this.userAPIKeyPrompts.Add(prompt);
}
}
@@ -449,6 +449,15 @@ public static partial class PluginFactory
.ToList();
}
/// <summary>
/// Collects what the running configuration plugins tell the user while AI Studio asks for an API key.
/// </summary>
/// <returns>The prompts of all configuration plugins, at most one per plugin.</returns>
public static IReadOnlyList<DataUserAPIKeyPrompt> GetUserAPIKeyPrompts()
{
return ResolveLivePluginContent<PluginConfiguration, DataUserAPIKeyPrompt>("user API key prompt", plugin => plugin.UserAPIKeyPrompts).ToList();
}
/// <summary>
/// Collects the mail servers which the running configuration plugins offer for new mailboxes.
/// </summary>
@@ -0,0 +1,31 @@
using Lua;
namespace AIStudio.Tools.PluginSystem;
/// <summary>
/// What a configuration plugin allows for the API key of a provider it manages: whether the user may set
/// their own key, and whether AI Studio asks the user for it on the home page.
/// </summary>
/// <param name="AllowUserProvidedAPIKey">The user may set their own API key for the otherwise locked provider.</param>
/// <param name="PromptForUserAPIKey">AI Studio asks the user for the API key on the home page as long as none is stored.</param>
public readonly record struct UserProvidedAPIKeyOptions(bool AllowUserProvidedAPIKey, bool PromptForUserAPIKey)
{
/// <summary>
/// Reads both options from the Lua table of a provider. An option which is missing or not a boolean counts as false.
/// </summary>
/// <remarks>
/// Asking the user for the key only makes sense when the user may set it. Therefore, PromptForUserAPIKey
/// implies AllowUserProvidedAPIKey, and a configuration needs to name only the one option. The implied
/// option also keeps the key of the user in the OS keyring when the configuration is withdrawn.
/// </remarks>
/// <param name="table">The Lua table of the provider.</param>
/// <returns>The options of the provider.</returns>
public static UserProvidedAPIKeyOptions Read(LuaTable table)
{
var promptForUserAPIKey = ReadOption(table, "PromptForUserAPIKey");
var allowUserProvidedAPIKey = promptForUserAPIKey || ReadOption(table, "AllowUserProvidedAPIKey");
return new UserProvidedAPIKeyOptions(allowUserProvidedAPIKey, promptForUserAPIKey);
}
private static bool ReadOption(LuaTable table, string name) => table.TryGetValue(name, out var value) && value.TryRead<bool>(out var isSet) && isSet;
}