2024-12-03 15:24:40 +01:00
using System.Text.Json.Serialization ;
2024-04-19 21:25:44 +02:00
using AIStudio.Provider ;
2025-04-11 14:31:10 +02:00
using AIStudio.Provider.HuggingFace ;
2025-08-26 10:59:56 +02:00
using AIStudio.Tools.PluginSystem ;
2026-05-22 15:46:03 +02:00
using SharedTools ;
2025-08-26 10:59:56 +02:00
2024-07-16 10:28:13 +02:00
using Host = AIStudio . Provider . SelfHosted . Host ;
2026-05-22 15:46:03 +02:00
using LuaTable = Lua . LuaTable ;
2024-07-16 10:28:13 +02:00
2024-04-19 21:25:44 +02:00
namespace AIStudio.Settings ;
2024-05-04 10:55:00 +02:00
/// <summary>
/// Data model for configured providers.
/// </summary>
2024-05-19 20:28:25 +02:00
/// <param name="Num">The provider's number.</param>
2024-05-04 10:55:00 +02:00
/// <param name="Id">The provider's ID.</param>
/// <param name="InstanceName">The provider's instance name. Useful for multiple instances of the same provider, e.g., to distinguish between different OpenAI API keys.</param>
2024-09-13 21:50:00 +02:00
/// <param name="UsedLLMProvider">The provider used.</param>
2024-07-03 20:31:04 +02:00
/// <param name="IsSelfHosted">Whether the provider is self-hosted.</param>
/// <param name="Hostname">The hostname of the provider. Useful for self-hosted providers.</param>
2024-05-19 16:10:37 +02:00
/// <param name="Model">The LLM model to use for chat.</param>
2026-08-15 11:20:36 +02:00
/// <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>
2026-08-25 12:46:10 +02:00
/// <param name="CustomIconDataUrl">The validated custom SVG icon supplied by a configuration plugin.</param>
2025-08-26 10:59:56 +02:00
public sealed record Provider (
2024-07-16 10:28:13 +02:00
uint Num ,
string Id ,
string InstanceName ,
2024-09-13 21:50:00 +02:00
LLMProviders UsedLLMProvider ,
2024-07-16 10:28:13 +02:00
Model Model ,
bool IsSelfHosted = false ,
2025-06-01 21:14:21 +02:00
bool IsEnterpriseConfiguration = false ,
Guid EnterpriseConfigurationPluginId = default ,
2024-07-16 10:28:13 +02:00
string Hostname = "http://localhost:1234" ,
2025-04-11 14:31:10 +02:00
Host Host = Host . NONE ,
2025-11-13 18:13:16 +01:00
HFInferenceProvider HFInferenceProvider = HFInferenceProvider . NONE ,
2026-07-05 12:42:55 +02:00
string AdditionalJsonApiParameters = "" ,
2026-09-09 18:43:37 +02:00
string TokenizerPath = "" ,
2026-08-15 11:20:36 +02:00
ProviderCapabilityOverrides ? CapabilityOverrides = null ,
2026-08-25 12:46:10 +02:00
bool AllowUserProvidedAPIKey = false ,
string CustomIconDataUrl = "" ) : ConfigurationBaseObject , ISecretId , IUserProvidedAPIKey
2024-05-04 10:55:00 +02:00
{
2025-08-26 10:59:56 +02:00
private static readonly ILogger < Provider > LOGGER = Program . LOGGER_FACTORY . CreateLogger < Provider >();
public static readonly Provider NONE = new ();
2026-09-09 18:43:37 +02:00
public Provider () : this ( 0 , Guid . Empty . ToString (), string . Empty , LLMProviders . NONE , default , false , false , Guid . Empty )
2025-08-26 10:59:56 +02:00
{
}
2024-05-04 10:55:00 +02:00
#region Overrides of ValueType
/// <summary>
/// Returns a string that represents the current provider in a human-readable format.
/// We use this to display the provider in the chat UI.
/// </summary>
/// <returns>A string that represents the current provider in a human-readable format.</returns>
public override string ToString ()
{
2024-07-03 20:31:04 +02:00
if ( this . IsSelfHosted )
2024-09-13 21:50:00 +02:00
return $"{this.InstanceName} ({this.UsedLLMProvider.ToName()}, {this.Host}, {this.Hostname}, {this.Model})" ;
2024-07-03 20:31:04 +02:00
2024-09-13 21:50:00 +02:00
return $"{this.InstanceName} ({this.UsedLLMProvider.ToName()}, {this.Model})" ;
2024-05-04 10:55:00 +02:00
}
#endregion
2024-12-03 15:24:40 +01:00
#region Implementation of ISecretId
/// <inheritdoc />
[JsonIgnore]
2026-06-20 15:55:09 +02:00
public string SecretId => this . IsEnterpriseConfiguration ? $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{this.UsedLLMProvider.ToSecretId()}" : this . UsedLLMProvider . ToSecretId ();
2024-12-03 15:24:40 +01:00
/// <inheritdoc />
[JsonIgnore]
public string SecretName => this . InstanceName ;
#endregion
2025-08-26 10:59:56 +02:00
#region Implementation of IConfigurationObject
public override string Name
{
get => this . InstanceName ;
init => this . InstanceName = value ;
}
#endregion
2026-08-25 12:46:10 +02:00
public static bool TryParseProviderTable ( int idx , LuaTable table , Guid configPluginId , string pluginPath , out ConfigurationBaseObject provider )
2025-08-26 10:59:56 +02:00
{
provider = NONE ;
if (! table . TryGetValue ( "Id" , out var idValue ) || ! idValue . TryRead < string >( out var idText ) || ! Guid . TryParse ( idText , out var id ))
{
2026-02-19 20:43:47 +01:00
LOGGER . LogWarning ( $"The configured provider {idx} does not contain a valid ID. The ID must be a valid GUID. (Plugin ID: {configPluginId})" );
2025-08-26 10:59:56 +02:00
return false ;
}
if (! table . TryGetValue ( "InstanceName" , out var instanceNameValue ) || ! instanceNameValue . TryRead < string >( out var instanceName ))
{
2026-02-19 20:43:47 +01:00
LOGGER . LogWarning ( $"The configured provider {idx} does not contain a valid instance name. (Plugin ID: {configPluginId})" );
2025-08-26 10:59:56 +02:00
return false ;
}
if (! table . TryGetValue ( "UsedLLMProvider" , out var usedLLMProviderValue ) || ! usedLLMProviderValue . TryRead < string >( out var usedLLMProviderText ) || ! Enum . TryParse < LLMProviders >( usedLLMProviderText , true , out var usedLLMProvider ))
{
2026-02-19 20:43:47 +01:00
LOGGER . LogWarning ( $"The configured provider {idx} does not contain a valid LLM provider enum value. (Plugin ID: {configPluginId})" );
2025-08-26 10:59:56 +02:00
return false ;
}
if (! table . TryGetValue ( "Host" , out var hostValue ) || ! hostValue . TryRead < string >( out var hostText ) || ! Enum . TryParse < Host >( hostText , true , out var host ))
{
2026-02-19 20:43:47 +01:00
LOGGER . LogWarning ( $"The configured provider {idx} does not contain a valid host enum value. (Plugin ID: {configPluginId})" );
2025-08-26 10:59:56 +02:00
return false ;
}
if (! table . TryGetValue ( "Hostname" , out var hostnameValue ) || ! hostnameValue . TryRead < string >( out var hostname ))
{
2026-02-19 20:43:47 +01:00
LOGGER . LogWarning ( $"The configured provider {idx} does not contain a valid hostname. (Plugin ID: {configPluginId})" );
2025-08-26 10:59:56 +02:00
return false ;
}
2026-02-07 22:59:41 +01:00
var hfInferenceProvider = HFInferenceProvider . NONE ;
if ( table . TryGetValue ( "HFInferenceProvider" , out var hfInferenceProviderValue ) && hfInferenceProviderValue . TryRead < string >( out var hfInferenceProviderText ))
{
if (! Enum . TryParse ( hfInferenceProviderText , true , out hfInferenceProvider ))
{
2026-02-19 20:43:47 +01:00
LOGGER . LogWarning ( $"The configured provider {idx} does not contain a valid Hugging Face inference provider enum value. (Plugin ID: {configPluginId})" );
2026-02-07 22:59:41 +01:00
hfInferenceProvider = HFInferenceProvider . NONE ;
}
}
2025-08-26 10:59:56 +02:00
if (! table . TryGetValue ( "Model" , out var modelValue ) || ! modelValue . TryRead < LuaTable >( out var modelTable ))
{
2026-02-19 20:43:47 +01:00
LOGGER . LogWarning ( $"The configured provider {idx} does not contain a valid model table. (Plugin ID: {configPluginId})" );
2025-08-26 10:59:56 +02:00
return false ;
}
2026-02-19 20:43:47 +01:00
if (! TryReadModelTable ( idx , modelTable , configPluginId , out var model ))
2025-08-26 10:59:56 +02:00
{
2026-02-19 20:43:47 +01:00
LOGGER . LogWarning ( $"The configured provider {idx} does not contain a valid model configuration. (Plugin ID: {configPluginId})" );
2025-08-26 10:59:56 +02:00
return false ;
}
2025-11-13 18:13:16 +01:00
if (! table . TryGetValue ( "AdditionalJsonApiParameters" , out var additionalJsonApiParametersValue ) || ! additionalJsonApiParametersValue . TryRead < string >( out var additionalJsonApiParameters ))
{
2025-11-17 13:13:19 +01:00
// In this case, no reason exists to reject this provider, though.
2026-02-19 20:43:47 +01:00
LOGGER . LogWarning ( $"The configured provider {idx} does not contain valid additional JSON API parameters. (Plugin ID: {configPluginId})" );
2025-11-17 13:13:19 +01:00
additionalJsonApiParameters = string . Empty ;
2025-11-13 18:13:16 +01:00
}
2025-08-26 10:59:56 +02:00
2026-09-09 18:43:37 +02:00
var tokenizerPath = string . Empty ;
if ( table . TryGetValue ( "TokenizerPath" , out var tokenizerPathValue ) && ! tokenizerPathValue . TryRead < string >( out tokenizerPath ))
{
LOGGER . LogWarning ( $"The configured provider {idx} does not contain a valid tokenizer path. (Plugin ID: {configPluginId})" );
tokenizerPath = string . Empty ;
}
2026-07-05 12:42:55 +02:00
var capabilityOverrides = ProviderCapabilityOverrides . TryParseFromLuaTable ( idx , table , configPluginId , LOGGER );
2026-08-15 11:20:36 +02:00
var allowUserProvidedApiKey = false ;
if ( table . TryGetValue ( "AllowUserProvidedAPIKey" , out var allowUserProvidedApiKeyValue ) && allowUserProvidedApiKeyValue . TryRead < bool >( out var allowUserProvidedApiKeyBool ))
allowUserProvidedApiKey = allowUserProvidedApiKeyBool ;
2026-08-25 12:46:10 +02:00
var customIconDataUrl = string . Empty ;
if ( table . TryGetValue ( "IconPath" , out var iconPathValue ))
{
if (! iconPathValue . TryRead < string >( out var iconPath ))
LOGGER . LogWarning ( $"The configured provider {idx} does not contain a valid icon path. Falling back to the built-in provider icon. (Plugin ID: {configPluginId})" );
else if (! PluginIconFile . TryLoadDataUrl ( iconPath , pluginPath , out customIconDataUrl , out var iconIssue ))
LOGGER . LogWarning ( $"The configured provider {idx} contains an invalid icon path. Falling back to the built-in provider icon. Issue: {iconIssue} (Plugin ID: {configPluginId})" );
}
2025-08-26 10:59:56 +02:00
provider = new Provider
{
2026-02-01 14:50:19 +01:00
Num = 0 , // will be set later by the PluginConfigurationObject
2025-08-26 10:59:56 +02:00
Id = id . ToString (),
InstanceName = instanceName ,
UsedLLMProvider = usedLLMProvider ,
Model = model ,
IsSelfHosted = usedLLMProvider is LLMProviders . SELF_HOSTED ,
IsEnterpriseConfiguration = true ,
EnterpriseConfigurationPluginId = configPluginId ,
Hostname = hostname ,
2025-11-13 18:13:16 +01:00
Host = host ,
2026-02-07 22:59:41 +01:00
HFInferenceProvider = hfInferenceProvider ,
2025-11-13 18:13:16 +01:00
AdditionalJsonApiParameters = additionalJsonApiParameters ,
2026-09-09 18:43:37 +02:00
TokenizerPath = tokenizerPath ,
2026-07-05 12:42:55 +02:00
CapabilityOverrides = capabilityOverrides ,
2026-08-15 11:20:36 +02:00
AllowUserProvidedAPIKey = allowUserProvidedApiKey ,
2026-08-25 12:46:10 +02:00
CustomIconDataUrl = customIconDataUrl ,
2025-08-26 10:59:56 +02:00
};
2026-02-07 22:59:41 +01:00
2026-08-15 11:20:36 +02:00
// 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 ))
2026-02-07 22:59:41 +01:00
{
if (! EnterpriseEncryption . IsEncrypted ( apiKeyText ))
2026-02-19 20:43:47 +01:00
LOGGER . LogWarning ( $"The configured provider {idx} contains a plaintext API key. Only encrypted API keys (starting with 'ENC:v1:') are supported. (Plugin ID: {configPluginId})" );
2026-02-07 22:59:41 +01:00
else
{
var encryption = PluginFactory . EnterpriseEncryption ;
if ( encryption ?. IsAvailable == true )
{
if ( encryption . TryDecrypt ( apiKeyText , out var decryptedApiKey ))
{
// Queue the API key for storage in the OS keyring:
PendingEnterpriseApiKeys . Add ( new (
2026-06-20 15:55:09 +02:00
$"{ISecretId.ENTERPRISE_KEY_PREFIX}::{usedLLMProvider.ToSecretId()}" ,
2026-02-07 22:59:41 +01:00
instanceName ,
decryptedApiKey ,
SecretStoreType . LLM_PROVIDER ));
2026-02-19 20:43:47 +01:00
LOGGER . LogDebug ( $"Successfully decrypted API key for provider {idx}. It will be stored in the OS keyring. (Plugin ID: {configPluginId})" );
2026-02-07 22:59:41 +01:00
}
else
2026-02-19 20:43:47 +01:00
LOGGER . LogWarning ( $"Failed to decrypt API key for provider {idx}. The encryption secret may be incorrect. (Plugin ID: {configPluginId})" );
2026-02-07 22:59:41 +01:00
}
else
2026-02-19 20:43:47 +01:00
LOGGER . LogWarning ( $"The configured provider {idx} contains an encrypted API key, but no encryption secret is configured. (Plugin ID: {configPluginId})" );
2026-02-07 22:59:41 +01:00
}
}
2025-08-26 10:59:56 +02:00
return true ;
}
2026-02-19 20:43:47 +01:00
private static bool TryReadModelTable ( int idx , LuaTable table , Guid configPluginId , out Model model )
2025-08-26 10:59:56 +02:00
{
model = default ;
if (! table . TryGetValue ( "Id" , out var idValue ) || ! idValue . TryRead < string >( out var id ))
{
2026-02-19 20:43:47 +01:00
LOGGER . LogWarning ( $"The configured provider {idx} does not contain a valid model ID. (Plugin ID: {configPluginId})" );
2025-08-26 10:59:56 +02:00
return false ;
}
if (! table . TryGetValue ( "DisplayName" , out var displayNameValue ) || ! displayNameValue . TryRead < string >( out var displayName ))
{
2026-02-19 20:43:47 +01:00
LOGGER . LogWarning ( $"The configured provider {idx} does not contain a valid model display name. (Plugin ID: {configPluginId})" );
2025-08-26 10:59:56 +02:00
return false ;
}
model = new ( id , displayName );
return true ;
}
2026-02-07 22:59:41 +01:00
/// <summary>
/// Exports the provider configuration as a Lua configuration section.
/// </summary>
/// <param name="encryptedApiKey">Optional encrypted API key to include in the export.</param>
/// <returns>A Lua configuration section string.</returns>
public string ExportAsConfigurationSection ( string? encryptedApiKey = null )
{
var hfInferenceProviderLine = string . Empty ;
if ( this . HFInferenceProvider is not HFInferenceProvider . NONE )
{
hfInferenceProviderLine = $"""
[" HFInferenceProvider "] = " { this . HFInferenceProvider } ",
""";
}
var apiKeyLine = string . Empty ;
if (! string . IsNullOrWhiteSpace ( encryptedApiKey ))
{
apiKeyLine = $"""
[" APIKey "] = " { LuaTools . EscapeLuaString ( encryptedApiKey )} ",
""";
}
2026-07-05 12:42:55 +02:00
var capabilityOverridesLine = this . CapabilityOverrides ?. ExportAsLuaTable ( " " ) ?? string . Empty ;
2026-02-07 22:59:41 +01:00
return $ $"""
CONFIG[" LLM_PROVIDERS "][#CONFIG[" LLM_PROVIDERS "]+1] = {
["Id"] = "{{Guid.NewGuid().ToString()}}" ,
["InstanceName"] = "{{LuaTools.EscapeLuaString(this.InstanceName)}}" ,
["UsedLLMProvider"] = "{{this.UsedLLMProvider}}" ,
2026-09-09 18:43:37 +02:00
["TokenizerPath"] = "{{this.TokenizerPath}}" ,
2026-02-07 22:59:41 +01:00
["Host"] = "{{this.Host}}" ,
["Hostname"] = "{{LuaTools.EscapeLuaString(this.Hostname)}}" ,
{{ hfInferenceProviderLine }}
{{ apiKeyLine }}
["AdditionalJsonApiParameters"] = "{{LuaTools.EscapeLuaString(this.AdditionalJsonApiParameters)}}" ,
2026-07-05 12:42:55 +02:00
{{ capabilityOverridesLine }}
2026-02-07 22:59:41 +01:00
["Model"] = {
["Id"] = "{{LuaTools.EscapeLuaString(this.Model.Id)}}" ,
2026-03-12 11:44:53 +01:00
["DisplayName"] = "{{LuaTools.EscapeLuaString(this.Model.DisplayName ?? this.Model.Id)}}" ,
2026-02-07 22:59:41 +01:00
},
}
""";
}
}