diff --git a/app/Build/Commands/Pdfium.cs b/app/Build/Commands/Pdfium.cs index 88effaed..6593ec28 100644 --- a/app/Build/Commands/Pdfium.cs +++ b/app/Build/Commands/Pdfium.cs @@ -7,6 +7,11 @@ namespace Build.Commands; public static class Pdfium { + private static readonly HttpClient CLIENT = new() + { + Timeout = TimeSpan.FromMinutes(5) + }; + public static async Task InstallAsync(RID rid, string version, bool offline) { Console.Write($"- Installing Pdfium {version} for {rid.ToUserFriendlyName()} ..."); @@ -42,8 +47,7 @@ public static class Pdfium // Download the file: // Console.Write(" downloading ..."); - using var client = new HttpClient(); - using var response = await client.GetAsync(pdfiumUrl, HttpCompletionOption.ResponseHeadersRead); + using var response = await CLIENT.GetAsync(pdfiumUrl, HttpCompletionOption.ResponseHeadersRead); if (!response.IsSuccessStatusCode) { Console.WriteLine($" failed to download Pdfium {version} for {rid.ToUserFriendlyName()} from {pdfiumUrl}"); @@ -61,9 +65,9 @@ public static class Pdfium { await using var downloadStream = await response.Content.ReadAsStreamAsync(); await using var uncompressedStream = new GZipStream(downloadStream, CompressionMode.Decompress); - await using var tarReader = new TarReader(uncompressedStream, false); + await using var tarReader = new TarReader(uncompressedStream); - while (await tarReader.GetNextEntryAsync(false) is { } entry) + while (await tarReader.GetNextEntryAsync() is { } entry) { if (!string.Equals(entry.Name.Replace('\\', '/'), pdfiumLibArchivePath, StringComparison.Ordinal)) continue; diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 6d2a30e1..039f0e8b 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -6760,6 +6760,12 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::OPENAI::PROVIDEROPENAI::T757371511"] = "It -- Model as configured by whisper.cpp UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SELFHOSTED::PROVIDERSELFHOSTED::T3313940770"] = "Model as configured by whisper.cpp" +-- The llama.cpp provider '{0}' does not offer a usable text model. Please check your provider settings. +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SELFHOSTED::PROVIDERSELFHOSTED::T3839908321"] = "The llama.cpp provider '{0}' does not offer a usable text model. Please check your provider settings." + +-- The llama.cpp provider '{0}' offers multiple models. Please open the provider settings and select the model to use. +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SELFHOSTED::PROVIDERSELFHOSTED::T4018006464"] = "The llama.cpp provider '{0}' offers multiple models. Please open the provider settings and select the model to use." + -- Cannot export this chat template because example message {0} is not a text message. UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T1861800849"] = "Cannot export this chat template because example message {0} is not a text message." @@ -7324,6 +7330,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T3928871850"] = "Th -- The configured certificate bundle does not contain usable root CA certificates. UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T599774443"] = "The configured certificate bundle does not contain usable root CA certificates." +-- policy files +UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T632340680"] = "policy files" + -- AI Studio couldn't install Pandoc because the archive was not found. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1059477764"] = "AI Studio couldn't install Pandoc because the archive was not found." diff --git a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor index 4c09da2f..64299132 100644 --- a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor +++ b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor @@ -71,7 +71,7 @@ @* ReSharper restore Asp.Entity *@ } - @if (!this.DataLLMProvider.IsLLMModelSelectionHidden(this.DataHost)) + @if (!this.IsLLMModelSelectionHidden) { diff --git a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs index 0e395324..36600e65 100644 --- a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs @@ -104,6 +104,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId private string dataAPIKeyStorageIssue = string.Empty; private string dataEditingPreviousInstanceName = string.Empty; private string dataLoadingModelsIssue = string.Empty; + private bool usesLegacySystemModelFallback; private bool showExpertSettings; // We get the form reference from Blazor code to validate it manually: @@ -123,6 +124,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId GetUsedInstanceNames = () => this.UsedInstanceNames, GetHost = () => this.DataHost, IsModelProvidedManually = () => this.DataLLMProvider.IsLLMModelProvidedManually(), + IsModelSelectionHidden = () => this.IsLLMModelSelectionHidden, }; } @@ -132,9 +134,9 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId // Determine the model based on the provider and host configuration: Model model; - if (this.DataLLMProvider.IsLLMModelSelectionHidden(this.DataHost)) + if (this.IsLLMModelSelectionHidden) { - // Use system model placeholder for hosts that don't support model selection (e.g., llama.cpp): + // Use system model placeholder for legacy hosts that don't support model selection: model = Model.SYSTEM_MODEL; } else if (this.DataLLMProvider is LLMProviders.FIREWORKS or LLMProviders.HUGGINGFACE) @@ -300,6 +302,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId this.dataManuallyModel = string.Empty; this.availableModels.Clear(); this.dataLoadingModelsIssue = string.Empty; + this.usesLegacySystemModelFallback = false; } private async Task ReloadModels() @@ -321,6 +324,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId this.availableModels.Clear(); this.availableModels.AddRange(orderedModels); + this.UpdateModelSelectionAfterLoading(); } catch (Exception e) { @@ -334,6 +338,34 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId LLMProviders.SELF_HOSTED => T("(Optional) API Key"), _ => T("API Key"), }; + + private bool IsLLMModelSelectionHidden => this.DataLLMProvider.IsLLMModelSelectionHidden(this.DataHost) || + this.DataLLMProvider is LLMProviders.SELF_HOSTED && + this.DataHost is Host.LLAMA_CPP && + this.usesLegacySystemModelFallback; + + private void UpdateModelSelectionAfterLoading() + { + if (this.DataLLMProvider is not LLMProviders.SELF_HOSTED || this.DataHost is not Host.LLAMA_CPP) + return; + + this.usesLegacySystemModelFallback = this.availableModels.Count is 1 && this.availableModels[0].IsSystemModel; + if (this.usesLegacySystemModelFallback) + { + this.DataModel = Model.SYSTEM_MODEL; + return; + } + + var availableModel = this.availableModels.FirstOrDefault(model => + string.Equals(model.Id, this.DataModel.Id, StringComparison.OrdinalIgnoreCase)); + if (availableModel != default) + { + this.DataModel = availableModel; + return; + } + + this.DataModel = this.availableModels.Count is 1 ? this.availableModels[0] : default; + } private void ToggleExpertSettings() => this.showExpertSettings = !this.showExpertSettings; diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index 8367e109..eabbcfd9 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -6762,6 +6762,12 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::OPENAI::PROVIDEROPENAI::T757371511"] = "Ans -- Model as configured by whisper.cpp UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SELFHOSTED::PROVIDERSELFHOSTED::T3313940770"] = "Modell wie in whisper.cpp konfiguriert" +-- The llama.cpp provider '{0}' does not offer a usable text model. Please check your provider settings. +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SELFHOSTED::PROVIDERSELFHOSTED::T3839908321"] = "Der llama.cpp-Anbieter „{0}“ bietet kein verwendbares Textmodell an. Bitte überprüfen Sie Ihre Anbieter-Einstellungen." + +-- The llama.cpp provider '{0}' offers multiple models. Please open the provider settings and select the model to use. +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SELFHOSTED::PROVIDERSELFHOSTED::T4018006464"] = "Der llama.cpp-Anbieter „{0}“ bietet mehrere Modelle an. Bitte öffnen Sie die Anbietereinstellungen und wählen Sie das zu verwendende Modell aus." + -- Cannot export this chat template because example message {0} is not a text message. UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T1861800849"] = "Diese Chatvorlage kann nicht exportiert werden, da die Beispielnachricht {0} keine Textnachricht ist." @@ -7326,6 +7332,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T3928871850"] = "Di -- The configured certificate bundle does not contain usable root CA certificates. UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T599774443"] = "Das konfigurierte Zertifikats-Bundle enthält keine verwendbaren Root-CA-Zertifikate." +-- policy files +UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T632340680"] = "Richtliniendateien" + -- AI Studio couldn't install Pandoc because the archive was not found. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1059477764"] = "AI Studio konnte Pandoc nicht installieren, da das Archiv nicht gefunden wurde." diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index c1104280..6b1fd6f4 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -6762,6 +6762,12 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::OPENAI::PROVIDEROPENAI::T757371511"] = "It -- Model as configured by whisper.cpp UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SELFHOSTED::PROVIDERSELFHOSTED::T3313940770"] = "Model as configured by whisper.cpp" +-- The llama.cpp provider '{0}' does not offer a usable text model. Please check your provider settings. +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SELFHOSTED::PROVIDERSELFHOSTED::T3839908321"] = "The llama.cpp provider '{0}' does not offer a usable text model. Please check your provider settings." + +-- The llama.cpp provider '{0}' offers multiple models. Please open the provider settings and select the model to use. +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SELFHOSTED::PROVIDERSELFHOSTED::T4018006464"] = "The llama.cpp provider '{0}' offers multiple models. Please open the provider settings and select the model to use." + -- Cannot export this chat template because example message {0} is not a text message. UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T1861800849"] = "Cannot export this chat template because example message {0} is not a text message." @@ -7326,6 +7332,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T3928871850"] = "Th -- The configured certificate bundle does not contain usable root CA certificates. UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T599774443"] = "The configured certificate bundle does not contain usable root CA certificates." +-- policy files +UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T632340680"] = "policy files" + -- AI Studio couldn't install Pandoc because the archive was not found. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1059477764"] = "AI Studio couldn't install Pandoc because the archive was not found." diff --git a/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs b/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs index e71cef95..a6867e0f 100644 --- a/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs +++ b/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs @@ -329,14 +329,13 @@ public static class LLMProvidersExtensions /// /// Determines if the model selection should be completely hidden for LLM providers. - /// This is the case when the host does not support model selection (e.g., llama.cpp). + /// This is the case when the host does not support model selection. /// /// The provider. /// The host for self-hosted providers. /// True if model selection should be hidden; otherwise, false. public static bool IsLLMModelSelectionHidden(this LLMProviders provider, Host host) => provider switch { - LLMProviders.SELF_HOSTED => host is Host.LLAMA_CPP, _ => false, }; @@ -416,11 +415,11 @@ public static class LLMProvidersExtensions switch (host) { case Host.NONE: - case Host.LLAMA_CPP: case Host.WHISPER_CPP: default: return false; + case Host.LLAMA_CPP: case Host.OLLAMA: case Host.LM_STUDIO: case Host.VLLM: diff --git a/app/MindWork AI Studio/Provider/Model.cs b/app/MindWork AI Studio/Provider/Model.cs index 0cd43395..f0b64539 100644 --- a/app/MindWork AI Studio/Provider/Model.cs +++ b/app/MindWork AI Studio/Provider/Model.cs @@ -23,7 +23,7 @@ public readonly record struct Model(string Id, string? DisplayName) /// /// Checks if this model is the system-configured placeholder. /// - public bool IsSystemModel => this == SYSTEM_MODEL; + public bool IsSystemModel => string.Equals(this.Id, SYSTEM_MODEL_ID, StringComparison.Ordinal); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(Model).Namespace, nameof(Model)); diff --git a/app/MindWork AI Studio/Provider/SelfHosted/ModelsResponse.cs b/app/MindWork AI Studio/Provider/SelfHosted/ModelsResponse.cs index 8ea8fb57..545c9939 100644 --- a/app/MindWork AI Studio/Provider/SelfHosted/ModelsResponse.cs +++ b/app/MindWork AI Studio/Provider/SelfHosted/ModelsResponse.cs @@ -1,5 +1,7 @@ namespace AIStudio.Provider.SelfHosted; -public readonly record struct ModelsResponse(string Object, Model[] Data); +public readonly record struct ModelsResponse(string? Object, Model[]? Data); -public readonly record struct Model(string Id, string Object, string OwnedBy); \ No newline at end of file +public readonly record struct Model(string Id, string? Object, string? OwnedBy, ModelArchitecture? Architecture); + +public readonly record struct ModelArchitecture(string[]? InputModalities, string[]? OutputModalities); \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs b/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs index 53ee6db8..723a5ab3 100644 --- a/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs +++ b/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs @@ -1,5 +1,6 @@ using System.Net.Http.Headers; using System.Runtime.CompilerServices; +using System.Text.Json; using AIStudio.Chat; using AIStudio.Provider.OpenAI; @@ -23,14 +24,15 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide public override string InstanceName { get; set; } = "Self-hosted"; /// - public override bool HasModelLoadingCapability => host is Host.OLLAMA or Host.LM_STUDIO or Host.VLLM; + public override bool HasModelLoadingCapability => host is Host.OLLAMA or Host.LM_STUDIO or Host.VLLM or Host.LLAMA_CPP; /// public override async IAsyncEnumerable StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) { + var effectiveChatModel = await this.ResolveChatModelForRequest(chatModel, token); await foreach (var content in this.StreamOpenAICompatibleChatCompletion( "self-hosted provider", - chatModel, + effectiveChatModel, chatThread, settingsManager, async (systemPrompt, apiParameters) => @@ -40,13 +42,13 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide // - LM Studio, vLLM, and llama.cpp use the nested image URL format: { "type": "image_url", "image_url": { "url": "data:..." } } var messages = host switch { - Host.OLLAMA => await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.Provider, chatModel), - _ => await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel), + Host.OLLAMA => await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.Provider, effectiveChatModel), + _ => await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, effectiveChatModel), }; return new ChatCompletionAPIRequest { - Model = chatModel.Id, + Model = effectiveChatModel.Id, // Build the messages: // - First of all the system prompt @@ -93,9 +95,7 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide switch (host) { case Host.LLAMA_CPP: - // Right now, llama.cpp only supports one model. - // There is no API to list the model(s). - return ModelLoadResult.FromModels([ new Provider.Model("as configured by llama.cpp", null) ]); + return await this.LoadLlamaCppTextModels(["embed"], [], token, apiKeyProvisional); case Host.LM_STUDIO: case Host.OLLAMA: @@ -188,8 +188,10 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide } var lmStudioModelResponse = await lmStudioResponse.Content.ReadFromJsonAsync(token); - return SuccessfulModelLoadResult(lmStudioModelResponse.Data. - Where(model => !ignorePhrases.Any(ignorePhrase => model.Id.Contains(ignorePhrase, StringComparison.InvariantCulture)) && + var models = lmStudioModelResponse.Data ?? []; + return SuccessfulModelLoadResult(models. + Where(model => !string.IsNullOrWhiteSpace(model.Id) && + !ignorePhrases.Any(ignorePhrase => model.Id.Contains(ignorePhrase, StringComparison.InvariantCulture)) && filterPhrases.All( filter => model.Id.Contains(filter, StringComparison.InvariantCulture))) .Select(n => new Provider.Model(n.Id, null))); } @@ -200,4 +202,127 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide return FailedModelLoadResult(ModelLoadFailureReason.PROVIDER_UNAVAILABLE, e.Message); } } + + private async Task ResolveChatModelForRequest(Provider.Model chatModel, CancellationToken token) + { + if (host is not Host.LLAMA_CPP || !chatModel.IsSystemModel) + return chatModel; + + var modelLoadResult = await this.LoadLlamaCppTextModels(["embed"], [], token); + if (!modelLoadResult.Success) + return chatModel; + + var availableModels = modelLoadResult.Models + .Where(model => !model.IsSystemModel && !string.IsNullOrWhiteSpace(model.Id)) + .ToList(); + + if (modelLoadResult.Models.All(model => !model.IsSystemModel) && availableModels.Count is 0) + { + LOGGER.LogError("The llama.cpp provider '{ProviderInstanceName}' does not offer a usable text model. Please check your provider settings.", this.InstanceName); + throw new ProviderRequestException( + ProviderRequestFailureReason.NONE, + string.Format( + TB("The llama.cpp provider '{0}' does not offer a usable text model. Please check your provider settings."), + this.InstanceName)); + } + + if (availableModels.Count is 1) + return availableModels[0]; + + if (availableModels.Count > 1) + { + LOGGER.LogError( + "The llama.cpp provider '{ProviderInstanceName}' offers {ModelCount} models, but the configured model is the legacy system placeholder. The provider settings must be updated to select a specific model.", + this.InstanceName, + availableModels.Count); + throw new ProviderRequestException( + ProviderRequestFailureReason.NONE, + string.Format( + TB("The llama.cpp provider '{0}' offers multiple models. Please open the provider settings and select the model to use."), + this.InstanceName)); + } + + return chatModel; + } + + private async Task LoadLlamaCppTextModels(string[] ignorePhrases, string[] filterPhrases, CancellationToken token, string? apiKeyProvisional = null) + { + var secretKey = await this.GetModelLoadingSecretKey(SecretStoreType.LLM_PROVIDER, apiKeyProvisional, true); + + try + { + using var request = new HttpRequestMessage(HttpMethod.Get, "models"); + if (!string.IsNullOrWhiteSpace(secretKey)) + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey); + + using var response = await this.HttpClient.SendAsync(request, token); + var responseBody = await response.Content.ReadAsStringAsync(token); + if (!response.IsSuccessStatusCode) + { + if (response.StatusCode is System.Net.HttpStatusCode.NotFound) + return LlamaCppLegacyModelResult(); + + LOGGER.LogError("llama.cpp model loading request failed with status code {ResponseStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", response.StatusCode, response.ReasonPhrase, responseBody); + return FailedModelLoadResult(this.GetModelLoadFailureReason(response, responseBody), $"Status={(int)response.StatusCode} {response.ReasonPhrase}; Body='{responseBody}'"); + } + + try + { + var modelResponse = JsonSerializer.Deserialize(responseBody, JSON_SERIALIZER_OPTIONS); + var responseModels = modelResponse.Data? + .Where(model => !string.IsNullOrWhiteSpace(model.Id)) + .ToList() ?? []; + + if (responseModels.Count is 0) + return LlamaCppLegacyModelResult(); + + var models = responseModels + .Where(model => IsMatchingLlamaCppTextModel(model, ignorePhrases, filterPhrases)) + .Select(model => new Provider.Model(model.Id, null)) + .ToList(); + + return SuccessfulModelLoadResult(models); + } + catch (JsonException e) + { + LOGGER.LogWarning(e, "The llama.cpp model loading response could not be parsed. Falling back to the legacy system-configured model."); + return LlamaCppLegacyModelResult(); + } + } + catch (Exception e) when (this.IsTimeoutException(e, token)) + { + await this.SendTimeoutError("loading the available models"); + LOGGER.LogError(e, "Timed out while loading models from llama.cpp provider '{ProviderInstanceName}'.", this.InstanceName); + return FailedModelLoadResult(ModelLoadFailureReason.PROVIDER_UNAVAILABLE, e.Message); + } + catch (Exception e) + { + LOGGER.LogError(e, "Failed to load models from llama.cpp provider '{ProviderInstanceName}'.", this.InstanceName); + return FailedModelLoadResult(ModelLoadFailureReason.UNKNOWN, e.Message); + } + } + + private static bool IsMatchingLlamaCppTextModel(Model model, string[] ignorePhrases, string[] filterPhrases) + { + if (string.IsNullOrWhiteSpace(model.Id)) + return false; + + if (ignorePhrases.Any(ignorePhrase => model.Id.Contains(ignorePhrase, StringComparison.InvariantCultureIgnoreCase))) + return false; + + if (!filterPhrases.All(filter => model.Id.Contains(filter, StringComparison.InvariantCultureIgnoreCase))) + return false; + + var outputModalities = model.Architecture?.OutputModalities; + if (outputModalities is { Length: > 0 } && + !outputModalities.Any(modality => string.Equals(modality, "text", StringComparison.OrdinalIgnoreCase))) + return false; + + return true; + } + + private static ModelLoadResult LlamaCppLegacyModelResult() + { + return ModelLoadResult.FromModels([ AIStudio.Provider.Model.SYSTEM_MODEL ]); + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Validation/ProviderValidation.cs b/app/MindWork AI Studio/Tools/Validation/ProviderValidation.cs index bb72feb4..5e98efd8 100644 --- a/app/MindWork AI Studio/Tools/Validation/ProviderValidation.cs +++ b/app/MindWork AI Studio/Tools/Validation/ProviderValidation.cs @@ -22,6 +22,8 @@ public sealed class ProviderValidation public Func IsModelProvidedManually { get; init; } = () => false; + public Func IsModelSelectionHidden { get; init; } = () => false; + public string? ValidatingHostname(string hostname) { if(this.GetProvider() != LLMProviders.SELF_HOSTED) @@ -76,9 +78,13 @@ public sealed class ProviderValidation if (this.GetProvider() is LLMProviders.NONE) return null; - // For self-hosted llama.cpp or whisper.cpp, no model selection needed + // For self-hosted whisper.cpp, no model selection needed // (model is loaded at startup): - if (this.GetProvider() is LLMProviders.SELF_HOSTED && this.GetHost() is Host.LLAMA_CPP or Host.WHISPER_CPP) + if (this.GetProvider() is LLMProviders.SELF_HOSTED && this.GetHost() is Host.WHISPER_CPP) + return null; + + // For legacy hosts without model selection, no selection validation is needed: + if (this.IsModelSelectionHidden()) return null; // For manually entered models, this validation doesn't apply: diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.6.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.6.1.md index a367b1a0..3f719b68 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.6.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.6.1.md @@ -5,6 +5,7 @@ - Added support for reading enterprise policy files from a Flatpak provisioning extension. - Added startup path and Linux package type details to the information page to make support easier. - Added the option to search for chats in all workspaces. +- Improved self-hosted llama.cpp providers by loading available models from the server and supporting servers that offer multiple models. Thanks to the GONICUS team for reporting this issue. - Improved workspaces by highlighting the currently open chat in the workspace view. - Improved workspaces by adding a shortcut to start a new chat directly from each workspace row. - Improved workspaces by allowing new workspaces to be created while moving a chat.