using AIStudio.Provider; using AIStudio.Settings; using AIStudio.Tools.PluginSystem; using AIStudio.Tools.Services; namespace AIStudio.Tools.UserAPIKeys; /// /// 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. /// public static class PendingUserAPIKeys { /// /// Finds all providers which still wait for the API key of the user, grouped by their configuration plugin. /// /// The settings manager, which knows the configured providers. /// The Rust service, which knows the stored API keys. /// One group per configuration plugin with at least one waiting provider. public static async Task> FindAsync(SettingsManager settingsManager, RustService rustService) { // // A provider the organization prohibited is left out, even when a configuration of the same // organization asks for its key: the key could be used for nothing, and checking it would // contact the provider. // var candidates = new List<(Guid ConfigPluginId, PendingUserAPIKeyTarget Target)>(); foreach (var provider in settingsManager.GetAllProviders().Where(provider => IsPrompting(provider) && !settingsManager.IsProviderProhibited(provider.UsedLLMProvider))) candidates.Add((provider.EnterpriseConfigurationPluginId, new PendingUserAPIKeyTarget(provider.InstanceName, provider, SecretStoreType.LLM_PROVIDER, () => provider.CreateProvider()))); foreach (var provider in settingsManager.GetAllEmbeddingProviders().Where(provider => IsPrompting(provider) && !settingsManager.IsProviderProhibited(provider.UsedLLMProvider))) candidates.Add((provider.EnterpriseConfigurationPluginId, new PendingUserAPIKeyTarget(provider.Name, provider, SecretStoreType.EMBEDDING_PROVIDER, () => provider.CreateProvider()))); foreach (var provider in settingsManager.GetAllTranscriptionProviders().Where(provider => IsPrompting(provider) && !settingsManager.IsProviderProhibited(provider.UsedLLMProvider))) 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(); } /// /// 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. /// /// /// 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. /// /// The providers which receive the key. /// The API key the user entered. /// The Rust service, which stores the key in the OS keyring. /// The logger for refused keys and failures to store one. /// What the provider said about the key, and for which providers storing it failed. public static async Task 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(); 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); } /// /// Tells from the models a provider loaded with an API key what the provider says about that key. /// /// /// 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. /// /// The models the provider loaded with the key. /// What the provider says about the key. 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 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 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 provider) where T : IConfigurationObject, IUserProvidedAPIKey => provider is { IsEnterpriseConfiguration: true, PromptForUserAPIKey: true }; }