2025-02-23 15:05:29 +01:00
using System.Diagnostics.CodeAnalysis ;
2025-01-05 15:11:15 +01:00
using AIStudio.Dialogs ;
using AIStudio.Settings ;
2026-09-09 18:43:37 +02:00
using AIStudio.Tools.Services ;
2025-01-05 15:11:15 +01:00
using Microsoft.AspNetCore.Components ;
using DialogOptions = AIStudio . Dialogs . DialogOptions ;
namespace AIStudio.Components.Settings ;
2026-02-07 22:59:41 +01:00
public partial class SettingsPanelProviders : SettingsPanelProviderBase
2025-01-05 15:11:15 +01:00
{
2026-08-12 21:57:58 +02:00
/// <summary>
/// Groups the table by the used LLM provider. The provider list is already sorted by that
/// provider, so all instances of one LLM provider form a single, coherent group.
/// </summary>
private static readonly TableGroupDefinition < AIStudio . Settings . Provider > GROUP_CONFIG = new ()
{
Expandable = true ,
IsInitiallyExpanded = false ,
2026-08-25 12:46:10 +02:00
Selector = provider => provider . UsedLLMProvider ,
2026-08-12 21:57:58 +02:00
};
2025-01-05 15:11:15 +01:00
[Parameter]
public List < ConfigurationSelectData < string >> AvailableLLMProviders { get ; set ; } = new ();
[Parameter]
public EventCallback < List < ConfigurationSelectData < string >>> AvailableLLMProvidersChanged { get ; set ; }
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync ()
{
await this . UpdateProviders ();
await base . OnInitializedAsync ();
}
#endregion
2026-08-12 20:44:48 +02:00
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed", Justification = "Managing the provider list is the purpose of this settings panel. Reading providers goes through the settings manager, but adding, editing, and removing them stays here on purpose.")]
2025-01-05 15:11:15 +01:00
private async Task AddLLMProvider ()
{
var dialogParameters = new DialogParameters < ProviderDialog >
{
{ x => x . IsEditing , false },
};
2025-04-27 09:06:05 +02:00
var dialogReference = await this . DialogService . ShowAsync < ProviderDialog >( T ( "Add LLM Provider" ), dialogParameters , DialogOptions . FULLSCREEN );
2025-01-05 15:11:15 +01:00
var dialogResult = await dialogReference . Result ;
if ( dialogResult is null || dialogResult . Canceled )
return ;
var addedProvider = ( AIStudio . Settings . Provider ) dialogResult . Data !;
addedProvider = addedProvider with { Num = this . SettingsManager . ConfigurationData . NextProviderNum ++ };
this . SettingsManager . ConfigurationData . Providers . Add ( addedProvider );
await this . UpdateProviders ();
await this . SettingsManager . StoreSettings ();
await this . MessageBus . SendMessage < bool >( this , Event . CONFIGURATION_CHANGED );
}
2026-08-12 20:44:48 +02:00
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed", Justification = "Managing the provider list is the purpose of this settings panel. Reading providers goes through the settings manager, but adding, editing, and removing them stays here on purpose.")]
2025-01-05 15:11:15 +01:00
private async Task EditLLMProvider ( AIStudio . Settings . Provider provider )
{
2025-08-26 10:59:56 +02:00
if ( provider == AIStudio . Settings . Provider . NONE )
return ;
2026-08-15 11:20:36 +02:00
if ( provider . IsEnterpriseConfiguration && ! provider . AllowUserProvidedAPIKey )
2025-08-18 20:40:52 +02:00
return ;
2026-08-15 11:20:36 +02:00
2025-01-05 15:11:15 +01:00
var dialogParameters = new DialogParameters < ProviderDialog >
{
{ x => x . DataNum , provider . Num },
{ x => x . DataId , provider . Id },
{ x => x . DataInstanceName , provider . InstanceName },
{ x => x . DataLLMProvider , provider . UsedLLMProvider },
2026-08-25 12:46:10 +02:00
{ x => x . DataCustomIconDataUrl , provider . CustomIconDataUrl },
2025-01-05 15:11:15 +01:00
{ x => x . DataModel , provider . Model },
{ x => x . DataHostname , provider . Hostname },
{ x => x . IsSelfHosted , provider . IsSelfHosted },
{ x => x . IsEditing , true },
{ x => x . DataHost , provider . Host },
2025-04-20 15:24:43 +02:00
{ x => x . HFInferenceProviderId , provider . HFInferenceProvider },
2025-11-13 18:13:16 +01:00
{ x => x . AdditionalJsonApiParameters , provider . AdditionalJsonApiParameters },
2026-09-09 18:43:37 +02:00
{ x => x . DataTokenizerPath , provider . TokenizerPath },
2026-07-05 12:42:55 +02:00
{ x => x . DataCapabilityOverrides , provider . CapabilityOverrides },
2026-08-15 11:20:36 +02:00
{ x => x . IsEnterpriseConfiguration , provider . IsEnterpriseConfiguration },
2025-01-05 15:11:15 +01:00
};
2025-04-27 09:06:05 +02:00
var dialogReference = await this . DialogService . ShowAsync < ProviderDialog >( T ( "Edit LLM Provider" ), dialogParameters , DialogOptions . FULLSCREEN );
2025-01-05 15:11:15 +01:00
var dialogResult = await dialogReference . Result ;
if ( dialogResult is null || dialogResult . Canceled )
return ;
2026-08-15 11:20:36 +02:00
if ( provider . IsEnterpriseConfiguration )
{
// Only the API key changed, and the dialog already stored it directly. The provider
// object itself is managed by the configuration plugin and must not be overwritten
// with the dialog's copy -- doing so would let the locked-but-technically-editable
// fields drift from what the organization configured.
await this . MessageBus . SendMessage < bool >( this , Event . CONFIGURATION_CHANGED );
return ;
}
2025-01-05 15:11:15 +01:00
var editedProvider = ( AIStudio . Settings . Provider ) dialogResult . Data !;
2026-08-15 11:20:36 +02:00
2025-01-05 15:11:15 +01:00
// Set the provider number if it's not set. This is important for providers
// added before we started saving the provider number.
if ( editedProvider . Num == 0 )
editedProvider = editedProvider with { Num = this . SettingsManager . ConfigurationData . NextProviderNum ++ };
2026-08-15 11:20:36 +02:00
2025-01-05 15:11:15 +01:00
this . SettingsManager . ConfigurationData . Providers [ this . SettingsManager . ConfigurationData . Providers . IndexOf ( provider )] = editedProvider ;
await this . UpdateProviders ();
2026-08-15 11:20:36 +02:00
2025-01-05 15:11:15 +01:00
await this . SettingsManager . StoreSettings ();
await this . MessageBus . SendMessage < bool >( this , Event . CONFIGURATION_CHANGED );
}
2026-08-12 20:44:48 +02:00
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed", Justification = "Managing the provider list is the purpose of this settings panel. Reading providers goes through the settings manager, but adding, editing, and removing them stays here on purpose.")]
2025-01-05 15:11:15 +01:00
private async Task DeleteLLMProvider ( AIStudio . Settings . Provider provider )
{
2025-08-28 18:51:44 +02:00
var dialogParameters = new DialogParameters < ConfirmDialog >
2025-01-05 15:11:15 +01:00
{
2025-08-28 18:51:44 +02:00
{ x => x . Message , string . Format ( T ( "Are you sure you want to delete the provider '{0}'?" ), provider . InstanceName ) },
2025-01-05 15:11:15 +01:00
};
2025-04-27 09:06:05 +02:00
var dialogReference = await this . DialogService . ShowAsync < ConfirmDialog >( T ( "Delete LLM Provider" ), dialogParameters , DialogOptions . FULLSCREEN );
2025-01-05 15:11:15 +01:00
var dialogResult = await dialogReference . Result ;
if ( dialogResult is null || dialogResult . Canceled )
return ;
2026-01-11 16:02:28 +01:00
var deleteSecretResponse = await this . RustService . DeleteAPIKey ( provider , SecretStoreType . LLM_PROVIDER );
2026-09-09 18:43:37 +02:00
//
// Removing the tokenizer is best effort: it leaves an unused file behind when it fails,
// which is not worth bothering the user about while they are deleting the provider. The
// API key is different, though, because a leftover secret is a secret we promised to remove.
//
_ = await this . RustService . DeleteTokenizer ( TokenizerModelId . ForProvider ( provider ));
2025-01-05 15:11:15 +01:00
if ( deleteSecretResponse . Success )
{
this . SettingsManager . ConfigurationData . Providers . Remove ( provider );
await this . SettingsManager . StoreSettings ();
}
2025-04-06 11:53:14 +02:00
else
{
2025-08-28 18:51:44 +02:00
var issueDialogParameters = new DialogParameters < ConfirmDialog >
2025-04-06 11:53:14 +02:00
{
2025-08-28 18:51:44 +02:00
{ x => x . Message , string . Format ( T ( "Couldn't delete the provider '{0}'. The issue: {1}. We can ignore this issue and delete the provider anyway. Do you want to ignore it and delete this provider?" ), provider . InstanceName , deleteSecretResponse . Issue ) },
2025-04-06 11:53:14 +02:00
};
2025-01-05 15:11:15 +01:00
2025-04-27 09:06:05 +02:00
var issueDialogReference = await this . DialogService . ShowAsync < ConfirmDialog >( T ( "Delete LLM Provider" ), issueDialogParameters , DialogOptions . FULLSCREEN );
2025-04-06 11:53:14 +02:00
var issueDialogResult = await issueDialogReference . Result ;
if ( issueDialogResult is null || issueDialogResult . Canceled )
return ;
// Case: The user wants to ignore the issue and delete the provider anyway:
this . SettingsManager . ConfigurationData . Providers . Remove ( provider );
await this . SettingsManager . StoreSettings ();
}
2025-01-05 15:11:15 +01:00
await this . UpdateProviders ();
await this . MessageBus . SendMessage < bool >( this , Event . CONFIGURATION_CHANGED );
}
2026-02-07 22:59:41 +01:00
private async Task ExportLLMProvider ( AIStudio . Settings . Provider provider )
{
2026-02-19 21:31:59 +01:00
if (! this . SettingsManager . ConfigurationData . App . ShowAdminSettings )
return ;
2026-02-07 22:59:41 +01:00
if ( provider == AIStudio . Settings . Provider . NONE )
return ;
await this . ExportProvider ( provider , SecretStoreType . LLM_PROVIDER , provider . ExportAsConfigurationSection );
}
2026-01-18 17:15:18 +01:00
private string GetLLMProviderModelName ( AIStudio . Settings . Provider provider )
2025-01-05 15:11:15 +01:00
{
2026-01-18 17:15:18 +01:00
// For system models, return localized text:
if ( provider . Model . IsSystemModel )
return T ( "Uses the provider-configured model" );
2025-01-05 15:11:15 +01:00
const int MAX_LENGTH = 36 ;
var modelName = provider . Model . ToString ();
return modelName . Length > MAX_LENGTH ? "[...] " + modelName [^ Math . Min ( MAX_LENGTH , modelName . Length )..] : modelName ;
}
private async Task UpdateProviders ()
{
this . AvailableLLMProviders . Clear ();
2026-08-12 20:44:48 +02:00
foreach ( var provider in this . SettingsManager . GetAllProviders ())
2025-01-05 15:11:15 +01:00
this . AvailableLLMProviders . Add ( new ( provider . InstanceName , provider . Id ));
await this . AvailableLLMProvidersChanged . InvokeAsync ( this . AvailableLLMProviders );
}
2026-02-07 22:59:41 +01:00
}