mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-04 17:27:21 +00:00
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:
1 parent
1f8d72aa45
commit
7d35536c87
29 files changed
+920
-31
No files matched your search
@@ -0,0 +1,11 @@
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
namespace AIStudio.Tools.UserAPIKeys;
|
||||
|
||||
/// <summary>
|
||||
/// The providers of one configuration plugin which wait for the API key of the user. They share one key.
|
||||
/// </summary>
|
||||
/// <param name="ConfigPluginId">The ID of the configuration plugin.</param>
|
||||
/// <param name="Prompt">What the configuration plugin tells the user about obtaining the key, if anything.</param>
|
||||
/// <param name="Targets">The providers which receive the key.</param>
|
||||
public sealed record PendingUserAPIKeyGroup(Guid ConfigPluginId, DataUserAPIKeyPrompt? Prompt, IReadOnlyList<PendingUserAPIKeyTarget> Targets);
|
||||
@@ -0,0 +1,12 @@
|
||||
using AIStudio.Provider;
|
||||
|
||||
namespace AIStudio.Tools.UserAPIKeys;
|
||||
|
||||
/// <summary>
|
||||
/// One provider of a configuration plugin which waits for the API key of the user.
|
||||
/// </summary>
|
||||
/// <param name="Name">The name of the provider as the user knows it.</param>
|
||||
/// <param name="SecretId">Where the API key of the provider is stored.</param>
|
||||
/// <param name="StoreType">The secret store of the provider's kind, which also tells which models the provider offers.</param>
|
||||
/// <param name="CreateProvider">Creates the provider, e.g., to check a key. Called only on demand, since every provider brings an HTTP client of its own.</param>
|
||||
public sealed record PendingUserAPIKeyTarget(string Name, ISecretId SecretId, SecretStoreType StoreType, Func<IProvider> CreateProvider);
|
||||
@@ -0,0 +1,136 @@
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
namespace AIStudio.Tools.UserAPIKeys;
|
||||
|
||||
/// <summary>
|
||||
/// Finds the providers which a configuration plugin marked with "PromptForUserAPIKey" and which have no API key yet,
|
||||
/// and stores the API key the user enters for them.
|
||||
/// </summary>
|
||||
public static class PendingUserAPIKeys
|
||||
{
|
||||
/// <summary>
|
||||
/// Finds all providers which still wait for the API key of the user, grouped by their configuration plugin.
|
||||
/// </summary>
|
||||
/// <param name="settingsManager">The settings manager, which knows the configured providers.</param>
|
||||
/// <param name="rustService">The Rust service, which knows the stored API keys.</param>
|
||||
/// <returns>One group per configuration plugin with at least one waiting provider.</returns>
|
||||
public static async Task<IReadOnlyList<PendingUserAPIKeyGroup>> FindAsync(SettingsManager settingsManager, RustService rustService)
|
||||
{
|
||||
var candidates = new List<(Guid ConfigPluginId, PendingUserAPIKeyTarget Target)>();
|
||||
foreach (var provider in settingsManager.GetAllProviders().Where(provider => IsPrompting(provider)))
|
||||
candidates.Add((provider.EnterpriseConfigurationPluginId, new PendingUserAPIKeyTarget(provider.InstanceName, provider, SecretStoreType.LLM_PROVIDER, () => provider.CreateProvider())));
|
||||
|
||||
foreach (var provider in settingsManager.GetAllEmbeddingProviders().Where(provider => IsPrompting(provider)))
|
||||
candidates.Add((provider.EnterpriseConfigurationPluginId, new PendingUserAPIKeyTarget(provider.Name, provider, SecretStoreType.EMBEDDING_PROVIDER, () => provider.CreateProvider())));
|
||||
|
||||
foreach (var provider in settingsManager.GetAllTranscriptionProviders().Where(provider => IsPrompting(provider)))
|
||||
candidates.Add((provider.EnterpriseConfigurationPluginId, new PendingUserAPIKeyTarget(provider.Name, provider, SecretStoreType.TRANSCRIPTION_PROVIDER, () => provider.CreateProvider())));
|
||||
|
||||
if (candidates.Count == 0)
|
||||
return [];
|
||||
|
||||
var pending = new List<(Guid ConfigPluginId, PendingUserAPIKeyTarget Target)>();
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
var storedKey = await rustService.GetAPIKey(candidate.Target.SecretId, candidate.Target.StoreType, isTrying: true);
|
||||
if (!storedKey.Success)
|
||||
pending.Add(candidate);
|
||||
}
|
||||
|
||||
if (pending.Count == 0)
|
||||
return [];
|
||||
|
||||
var prompts = PluginFactory.GetUserAPIKeyPrompts().ToDictionary(prompt => prompt.EnterpriseConfigurationPluginId);
|
||||
return pending
|
||||
.GroupBy(entry => entry.ConfigPluginId)
|
||||
.Select(group => new PendingUserAPIKeyGroup(group.Key, prompts.GetValueOrDefault(group.Key), group.Select(entry => entry.Target).ToList()))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks the API key the user entered with one of the providers of a group, and stores it for all of them
|
||||
/// unless the provider refused it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The providers of a group share one key, so asking one of them is enough: the first which can list its models.
|
||||
/// When none of them can, or the answer tells nothing about the key, for example because the server is out of
|
||||
/// reach, AI Studio stores the key anyway. Otherwise, an outage would keep the user from finishing the setup.
|
||||
/// </remarks>
|
||||
/// <param name="group">The providers which receive the key.</param>
|
||||
/// <param name="apiKey">The API key the user entered.</param>
|
||||
/// <param name="rustService">The Rust service, which stores the key in the OS keyring.</param>
|
||||
/// <param name="logger">The logger for refused keys and failures to store one.</param>
|
||||
/// <returns>What the provider said about the key, and for which providers storing it failed.</returns>
|
||||
public static async Task<UserAPIKeyStoreResult> StoreAsync(PendingUserAPIKeyGroup group, string apiKey, RustService rustService, ILogger logger)
|
||||
{
|
||||
var check = await CheckAsync(group, apiKey, logger);
|
||||
if (check is UserAPIKeyCheckOutcome.REJECTED)
|
||||
return UserAPIKeyStoreResult.REJECTED;
|
||||
|
||||
var failedProviderNames = new List<string>();
|
||||
foreach (var target in group.Targets)
|
||||
{
|
||||
var storeResponse = await rustService.SetAPIKey(target.SecretId, apiKey, target.StoreType);
|
||||
if (storeResponse.Success)
|
||||
continue;
|
||||
|
||||
logger.LogError("Failed to store the API key for the provider '{ProviderName}': {Issue}", target.Name, storeResponse.Issue);
|
||||
failedProviderNames.Add(target.Name);
|
||||
}
|
||||
|
||||
return new UserAPIKeyStoreResult(check, failedProviderNames);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tells from the models a provider loaded with an API key what the provider says about that key.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only a refused sign-in rejects the key, and only at least one model accepts it. Everything else tells nothing
|
||||
/// about the key: an outage, a rate limit, a used-up quota, or an empty list, which some providers return for a
|
||||
/// kind of model they do not offer, whatever the key. One gap remains: a few providers, such as Perplexity, return
|
||||
/// a fixed list without asking their server, so any key passes there.
|
||||
/// </remarks>
|
||||
/// <param name="result">The models the provider loaded with the key.</param>
|
||||
/// <returns>What the provider says about the key.</returns>
|
||||
public static UserAPIKeyCheckOutcome ClassifyCheck(ModelLoadResult result) => result switch
|
||||
{
|
||||
{ FailureReason: ModelLoadFailureReason.INVALID_OR_MISSING_API_KEY or ModelLoadFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR } => UserAPIKeyCheckOutcome.REJECTED,
|
||||
{ Success: true, Models.Count: > 0 } => UserAPIKeyCheckOutcome.ACCEPTED,
|
||||
_ => UserAPIKeyCheckOutcome.NOT_CHECKED
|
||||
};
|
||||
|
||||
private static async Task<UserAPIKeyCheckOutcome> CheckAsync(PendingUserAPIKeyGroup group, string apiKey, ILogger logger)
|
||||
{
|
||||
//
|
||||
// Every provider instance brings an HTTP client of its own. Hence, we create one only here,
|
||||
// and only until we found a provider which can answer:
|
||||
//
|
||||
foreach (var target in group.Targets)
|
||||
{
|
||||
var provider = target.CreateProvider();
|
||||
if (!provider.HasModelLoadingCapability)
|
||||
continue;
|
||||
|
||||
var outcome = ClassifyCheck(await LoadModelsAsync(provider, target.StoreType, apiKey));
|
||||
if (outcome is UserAPIKeyCheckOutcome.REJECTED)
|
||||
logger.LogWarning("The provider '{ProviderName}' rejected the API key the user entered.", target.Name);
|
||||
|
||||
return outcome;
|
||||
}
|
||||
|
||||
return UserAPIKeyCheckOutcome.NOT_CHECKED;
|
||||
}
|
||||
|
||||
private static Task<ModelLoadResult> LoadModelsAsync(IProvider provider, SecretStoreType storeType, string apiKey) => storeType switch
|
||||
{
|
||||
SecretStoreType.LLM_PROVIDER => provider.GetTextModels(apiKey),
|
||||
SecretStoreType.EMBEDDING_PROVIDER => provider.GetEmbeddingModels(apiKey),
|
||||
SecretStoreType.TRANSCRIPTION_PROVIDER => provider.GetTranscriptionModels(apiKey),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(storeType), storeType, "Only providers wait for the API key of the user.")
|
||||
};
|
||||
|
||||
private static bool IsPrompting<T>(T provider) where T : IConfigurationObject, IUserProvidedAPIKey => provider is { IsEnterpriseConfiguration: true, PromptForUserAPIKey: true };
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace AIStudio.Tools.UserAPIKeys;
|
||||
|
||||
/// <summary>
|
||||
/// What a provider says about an API key the user entered.
|
||||
/// </summary>
|
||||
public enum UserAPIKeyCheckOutcome
|
||||
{
|
||||
/// <summary>
|
||||
/// The provider listed its models with the key, so it accepts the key.
|
||||
/// </summary>
|
||||
ACCEPTED,
|
||||
|
||||
/// <summary>
|
||||
/// The provider refused the key. AI Studio does not store it.
|
||||
/// </summary>
|
||||
REJECTED,
|
||||
|
||||
/// <summary>
|
||||
/// Nothing tells whether the provider accepts the key, e.g., because the server was out of reach or the
|
||||
/// provider cannot list its models. AI Studio stores the key anyway.
|
||||
/// </summary>
|
||||
NOT_CHECKED
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace AIStudio.Tools.UserAPIKeys;
|
||||
|
||||
/// <summary>
|
||||
/// The result of storing the API key of the user for the providers of one configuration plugin.
|
||||
/// </summary>
|
||||
/// <param name="Check">What the provider said about the key. A rejected key was not stored at all.</param>
|
||||
/// <param name="FailedProviderNames">The names of the providers for which the key could not be stored.</param>
|
||||
public sealed record UserAPIKeyStoreResult(UserAPIKeyCheckOutcome Check, IReadOnlyList<string> FailedProviderNames)
|
||||
{
|
||||
/// <summary>
|
||||
/// The result for a key which the provider refused, so that it was not stored.
|
||||
/// </summary>
|
||||
public static readonly UserAPIKeyStoreResult REJECTED = new(UserAPIKeyCheckOutcome.REJECTED, []);
|
||||
}
|
||||
Reference in new issue
Block a user