Implemented host setting for self-hosted providers

This commit is contained in:
Thorsten Sommer 2024-07-04 15:07:44 +02:00
parent 29e1b2e087
commit b05b10af75
No known key found for this signature in database
GPG Key ID: B0B7E2FC074BF1F5
10 changed files with 164 additions and 35 deletions

View File

@ -53,6 +53,7 @@ public partial class Settings : ComponentBase
{ x => x.DataHostname, provider.Hostname }, { x => x.DataHostname, provider.Hostname },
{ x => x.IsSelfHosted, provider.IsSelfHosted }, { x => x.IsSelfHosted, provider.IsSelfHosted },
{ x => x.IsEditing, true }, { x => x.IsEditing, true },
{ x => x.DataHost, provider.Host },
}; };
var dialogReference = await this.DialogService.ShowAsync<ProviderDialog>("Edit Provider", dialogParameters, DialogOptions.FULLSCREEN); var dialogReference = await this.DialogService.ShowAsync<ProviderDialog>("Edit Provider", dialogParameters, DialogOptions.FULLSCREEN);
@ -83,7 +84,7 @@ public partial class Settings : ComponentBase
if (dialogResult.Canceled) if (dialogResult.Canceled)
return; return;
var providerInstance = provider.UsedProvider.CreateProvider(provider.InstanceName, provider.Hostname); var providerInstance = provider.CreateProvider();
var deleteSecretResponse = await this.SettingsManager.DeleteAPIKey(this.JsRuntime, providerInstance); var deleteSecretResponse = await this.SettingsManager.DeleteAPIKey(this.JsRuntime, providerInstance);
if(deleteSecretResponse.Success) if(deleteSecretResponse.Success)
{ {

View File

@ -45,17 +45,15 @@ public static class ExtensionsProvider
/// <summary> /// <summary>
/// Creates a new provider instance based on the provider value. /// Creates a new provider instance based on the provider value.
/// </summary> /// </summary>
/// <param name="provider">The provider value.</param> /// <param name="providerSettings">The provider settings.</param>
/// <param name="instanceName">The used instance name.</param>
/// <param name="hostname">The hostname of the provider.</param>
/// <returns>The provider instance.</returns> /// <returns>The provider instance.</returns>
public static IProvider CreateProvider(this Providers provider, string instanceName, string hostname = "http://localhost:1234") => provider switch public static IProvider CreateProvider(this Settings.Provider providerSettings) => providerSettings.UsedProvider switch
{ {
Providers.OPEN_AI => new ProviderOpenAI { InstanceName = instanceName }, Providers.OPEN_AI => new ProviderOpenAI { InstanceName = providerSettings.InstanceName },
Providers.ANTHROPIC => new ProviderAnthropic { InstanceName = instanceName }, Providers.ANTHROPIC => new ProviderAnthropic { InstanceName = providerSettings.InstanceName },
Providers.MISTRAL => new ProviderMistral { InstanceName = instanceName }, Providers.MISTRAL => new ProviderMistral { InstanceName = providerSettings.InstanceName },
Providers.SELF_HOSTED => new ProviderSelfHosted(hostname) { InstanceName = instanceName }, Providers.SELF_HOSTED => new ProviderSelfHosted(providerSettings) { InstanceName = providerSettings.InstanceName },
_ => new NoProvider(), _ => new NoProvider(),
}; };

View File

@ -0,0 +1,42 @@
namespace AIStudio.Provider.SelfHosted;
public enum Host
{
NONE,
LM_STUDIO,
LLAMACPP,
OLLAMA,
}
public static class HostExtensions
{
public static string Name(this Host host) => host switch
{
Host.NONE => "None",
Host.LM_STUDIO => "LM Studio",
Host.LLAMACPP => "llama.cpp",
Host.OLLAMA => "ollama",
_ => "Unknown",
};
public static string BaseURL(this Host host) => host switch
{
Host.LM_STUDIO => "/v1/",
Host.LLAMACPP => "/v1/",
Host.OLLAMA => "/api/",
_ => "/v1/",
};
public static string ChatURL(this Host host) => host switch
{
Host.LM_STUDIO => "chat/completions",
Host.LLAMACPP => "chat/completions",
Host.OLLAMA => "chat",
_ => "chat/completions",
};
}

View File

@ -8,7 +8,7 @@ using AIStudio.Settings;
namespace AIStudio.Provider.SelfHosted; namespace AIStudio.Provider.SelfHosted;
public sealed class ProviderSelfHosted(string hostname) : BaseProvider($"{hostname}/v1/"), IProvider public sealed class ProviderSelfHosted(Settings.Provider provider) : BaseProvider($"{provider.Hostname}{provider.Host.BaseURL()}"), IProvider
{ {
private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new() private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
{ {
@ -62,7 +62,7 @@ public sealed class ProviderSelfHosted(string hostname) : BaseProvider($"{hostna
}, JSON_SERIALIZER_OPTIONS); }, JSON_SERIALIZER_OPTIONS);
// Build the HTTP post request: // Build the HTTP post request:
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); var request = new HttpRequestMessage(HttpMethod.Post, provider.Host.ChatURL());
// Set the content: // Set the content:
request.Content = new StringContent(providerChatRequest, Encoding.UTF8, "application/json"); request.Content = new StringContent(providerChatRequest, Encoding.UTF8, "application/json");
@ -81,7 +81,7 @@ public sealed class ProviderSelfHosted(string hostname) : BaseProvider($"{hostna
// Read the stream, line by line: // Read the stream, line by line:
while(!streamReader.EndOfStream) while(!streamReader.EndOfStream)
{ {
// Check if the token is cancelled: // Check if the token is canceled:
if(token.IsCancellationRequested) if(token.IsCancellationRequested)
yield break; yield break;

View File

@ -9,7 +9,7 @@ public sealed class Data
/// The version of the settings file. Allows us to upgrade the settings /// The version of the settings file. Allows us to upgrade the settings
/// when a new version is available. /// when a new version is available.
/// </summary> /// </summary>
public Version Version { get; init; } = Version.V2; public Version Version { get; init; } = Version.V3;
/// <summary> /// <summary>
/// List of configured providers. /// List of configured providers.

View File

@ -1,5 +1,8 @@
using AIStudio.Provider; using AIStudio.Provider;
using Model = AIStudio.Provider.Model;
using Host = AIStudio.Provider.SelfHosted.Host;
namespace AIStudio.Settings; namespace AIStudio.Settings;
/// <summary> /// <summary>
@ -12,7 +15,16 @@ namespace AIStudio.Settings;
/// <param name="IsSelfHosted">Whether the provider is self-hosted.</param> /// <param name="IsSelfHosted">Whether the provider is self-hosted.</param>
/// <param name="Hostname">The hostname of the provider. Useful for self-hosted providers.</param> /// <param name="Hostname">The hostname of the provider. Useful for self-hosted providers.</param>
/// <param name="Model">The LLM model to use for chat.</param> /// <param name="Model">The LLM model to use for chat.</param>
public readonly record struct Provider(uint Num, string Id, string InstanceName, Providers UsedProvider, Model Model, bool IsSelfHosted = false, string Hostname = "http://localhost:1234") public readonly record struct Provider(
uint Num,
string Id,
string InstanceName,
Providers UsedProvider,
Model Model,
bool IsSelfHosted = false,
string Hostname = "http://localhost:1234",
Host Host = Host.NONE)
{ {
#region Overrides of ValueType #region Overrides of ValueType

View File

@ -1,4 +1,5 @@
@using AIStudio.Provider @using AIStudio.Provider
@using AIStudio.Provider.SelfHosted
@using MudBlazor @using MudBlazor
<MudDialog> <MudDialog>
@ -41,6 +42,13 @@
Validation="@this.ValidatingHostname" Validation="@this.ValidatingHostname"
/> />
<MudSelect Disabled="@this.IsCloudProvider" @bind-Value="@this.DataHost" Label="Host" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.ValidatingHost">
@foreach (Host host in Enum.GetValues(typeof(Host)))
{
<MudSelectItem Value="@host">@host.Name()</MudSelectItem>
}
</MudSelect>
<MudStack Row="@true" AlignItems="AlignItems.Center"> <MudStack Row="@true" AlignItems="AlignItems.Center">
<MudButton Disabled="@(!this.CanLoadModels)" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.Refresh" OnClick="this.ReloadModels">Load</MudButton> <MudButton Disabled="@(!this.CanLoadModels)" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.Refresh" OnClick="this.ReloadModels">Load</MudButton>
<MudSelect Disabled="@this.IsSelfHostedOrNone" @bind-Value="@this.DataModel" Label="Model" Class="mb-3" OpenIcon="@Icons.Material.Filled.FaceRetouchingNatural" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.ValidatingModel"> <MudSelect Disabled="@this.IsSelfHostedOrNone" @bind-Value="@this.DataModel" Label="Model" Class="mb-3" OpenIcon="@Icons.Material.Filled.FaceRetouchingNatural" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.ValidatingModel">

View File

@ -4,6 +4,8 @@ using AIStudio.Provider;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Host = AIStudio.Provider.SelfHosted.Host;
namespace AIStudio.Settings; namespace AIStudio.Settings;
/// <summary> /// <summary>
@ -44,6 +46,9 @@ public partial class ProviderDialog : ComponentBase
[Parameter] [Parameter]
public bool IsSelfHosted { get; set; } public bool IsSelfHosted { get; set; }
[Parameter]
public Host DataHost { get; set; } = Host.NONE;
/// <summary> /// <summary>
/// The provider to use. /// The provider to use.
/// </summary> /// </summary>
@ -86,6 +91,18 @@ public partial class ProviderDialog : ComponentBase
private readonly List<Model> availableModels = new(); private readonly List<Model> availableModels = new();
private Provider CreateProviderSettings() => new()
{
Num = this.DataNum,
Id = this.DataId,
InstanceName = this.DataInstanceName,
UsedProvider = this.DataProvider,
Model = this.DataModel,
IsSelfHosted = this.DataProvider is Providers.SELF_HOSTED,
Hostname = this.DataHostname,
Host = this.DataHost,
};
#region Overrides of ComponentBase #region Overrides of ComponentBase
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
@ -100,9 +117,23 @@ public partial class ProviderDialog : ComponentBase
if(this.IsEditing) if(this.IsEditing)
{ {
this.dataEditingPreviousInstanceName = this.DataInstanceName.ToLowerInvariant(); this.dataEditingPreviousInstanceName = this.DataInstanceName.ToLowerInvariant();
var provider = this.DataProvider.CreateProvider(this.DataInstanceName);
if(provider is NoProvider) //
// We cannot load the API key nor models for self-hosted providers:
//
if (this.DataProvider is Providers.SELF_HOSTED)
{
await base.OnInitializedAsync();
return; return;
}
var loadedProviderSettings = this.CreateProviderSettings();
var provider = loadedProviderSettings.CreateProvider();
if(provider is NoProvider)
{
await base.OnInitializedAsync();
return;
}
// Load the API key: // Load the API key:
var requestedSecret = await this.SettingsManager.GetAPIKey(this.JsRuntime, provider); var requestedSecret = await this.SettingsManager.GetAPIKey(this.JsRuntime, provider);
@ -111,7 +142,6 @@ public partial class ProviderDialog : ComponentBase
this.dataAPIKey = requestedSecret.Secret; this.dataAPIKey = requestedSecret.Secret;
// Now, we try to load the list of available models: // Now, we try to load the list of available models:
if(this.DataProvider is not Providers.SELF_HOSTED)
await this.ReloadModels(); await this.ReloadModels();
} }
else else
@ -148,19 +178,10 @@ public partial class ProviderDialog : ComponentBase
// Use the data model to store the provider. // Use the data model to store the provider.
// We just return this data to the parent component: // We just return this data to the parent component:
var addedProvider = new Provider var addedProviderSettings = this.CreateProviderSettings();
{
Num = this.DataNum,
Id = this.DataId,
InstanceName = this.DataInstanceName,
UsedProvider = this.DataProvider,
Model = this.DataModel,
IsSelfHosted = this.DataProvider is Providers.SELF_HOSTED,
Hostname = this.DataHostname,
};
// We need to instantiate the provider to store the API key: // We need to instantiate the provider to store the API key:
var provider = this.DataProvider.CreateProvider(this.DataInstanceName); var provider = addedProviderSettings.CreateProvider();
// Store the API key in the OS secure storage: // Store the API key in the OS secure storage:
var storeResponse = await this.SettingsManager.SetAPIKey(this.JsRuntime, provider, this.dataAPIKey); var storeResponse = await this.SettingsManager.SetAPIKey(this.JsRuntime, provider, this.dataAPIKey);
@ -171,7 +192,7 @@ public partial class ProviderDialog : ComponentBase
return; return;
} }
this.MudDialog.Close(DialogResult.Ok(addedProvider)); this.MudDialog.Close(DialogResult.Ok(addedProviderSettings));
} }
private string? ValidatingProvider(Providers provider) private string? ValidatingProvider(Providers provider)
@ -182,6 +203,17 @@ public partial class ProviderDialog : ComponentBase
return null; return null;
} }
private string? ValidatingHost(Host host)
{
if(this.DataProvider is not Providers.SELF_HOSTED)
return null;
if (host == Host.NONE)
return "Please select a host.";
return null;
}
private string? ValidatingModel(Model model) private string? ValidatingModel(Model model)
{ {
if(this.DataProvider is Providers.SELF_HOSTED) if(this.DataProvider is Providers.SELF_HOSTED)
@ -196,7 +228,7 @@ public partial class ProviderDialog : ComponentBase
[GeneratedRegex(@"^[a-zA-Z0-9\-_. ]+$")] [GeneratedRegex(@"^[a-zA-Z0-9\-_. ]+$")]
private static partial Regex InstanceNameRegex(); private static partial Regex InstanceNameRegex();
private static readonly string[] RESERVED_NAMES = { "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9" }; private static readonly string[] RESERVED_NAMES = [ "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9" ];
private string? ValidatingInstanceName(string instanceName) private string? ValidatingInstanceName(string instanceName)
{ {
@ -270,7 +302,8 @@ public partial class ProviderDialog : ComponentBase
private async Task ReloadModels() private async Task ReloadModels()
{ {
var provider = this.DataProvider.CreateProvider("temp"); var currentProviderSettings = this.CreateProviderSettings();
var provider = currentProviderSettings.CreateProvider();
if(provider is NoProvider) if(provider is NoProvider)
return; return;

View File

@ -1,5 +1,7 @@
namespace AIStudio.Settings; namespace AIStudio.Settings;
using Host = AIStudio.Provider.SelfHosted.Host;
public static class SettingsMigrations public static class SettingsMigrations
{ {
public static Data Migrate(Data previousData) public static Data Migrate(Data previousData)
@ -7,7 +9,11 @@ public static class SettingsMigrations
switch (previousData.Version) switch (previousData.Version)
{ {
case Version.V1: case Version.V1:
return MigrateFromV1(previousData); previousData = MigrateV1ToV2(previousData);
return MigrateV2ToV3(previousData);
case Version.V2:
return MigrateV2ToV3(previousData);
default: default:
Console.WriteLine("No migration needed."); Console.WriteLine("No migration needed.");
@ -15,7 +21,7 @@ public static class SettingsMigrations
} }
} }
private static Data MigrateFromV1(Data previousData) private static Data MigrateV1ToV2(Data previousData)
{ {
// //
// Summary: // Summary:
@ -36,4 +42,32 @@ public static class SettingsMigrations
UpdateBehavior = previousData.UpdateBehavior, UpdateBehavior = previousData.UpdateBehavior,
}; };
} }
private static Data MigrateV2ToV3(Data previousData)
{
//
// Summary:
// In v2, self-hosted providers had no host (LM Studio, llama.cpp, ollama, etc.)
//
Console.WriteLine("Migrating from v2 to v3...");
return new()
{
Version = Version.V3,
Providers = previousData.Providers.Select(provider =>
{
if(provider.IsSelfHosted)
return provider with { Host = Host.LM_STUDIO };
return provider with { Host = Host.NONE };
}).ToList(),
EnableSpellchecking = previousData.EnableSpellchecking,
IsSavingEnergy = previousData.IsSavingEnergy,
NextProviderNum = previousData.NextProviderNum,
ShortcutSendBehavior = previousData.ShortcutSendBehavior,
UpdateBehavior = previousData.UpdateBehavior,
};
}
} }

View File

@ -10,4 +10,5 @@ public enum Version
V1, V1,
V2, V2,
V3,
} }