diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index d0bdf8c0..cbdb9baf 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -5944,6 +5944,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGMETHODDIALOG::T662524223"] = "A lin -- Cancel UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGMETHODDIALOG::T900713019"] = "Cancel" +-- Hugging Face Inference Provider +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T1085481431"] = "Hugging Face Inference Provider" + -- Failed to store the API key in the operating system. The message was: {0}. Please try again. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T1122745046"] = "Failed to store the API key in the operating system. The message was: {0}. Please try again." @@ -5968,6 +5971,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T1847791252"] = "Up -- Failed to load the API key from the operating system. The message was: {0}. You might ignore this message and provide the API key again. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T1870831108"] = "Failed to load the API key from the operating system. The message was: {0}. You might ignore this message and provide the API key again." +-- Hugging Face offers embeddings through a few of its inference providers only, which is why this list is shorter than the one for chatting. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T194295715"] = "Hugging Face offers embeddings through a few of its inference providers only, which is why this list is shorter than the one for chatting." + -- Model UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T2189814010"] = "Model" @@ -6361,6 +6367,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1870831108"] = "Failed to l -- Speech input UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1874348907"] = "Speech input" +-- Choose which inference provider should answer your requests. When you pick one of the automatic options instead, Hugging Face selects a provider for you and switches to another one when your choice is unavailable. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1889879830"] = "Choose which inference provider should answer your requests. When you pick one of the automatic options instead, Hugging Face selects a provider for you and switches to another one when your choice is unavailable." + -- Please enter a model name. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1936099896"] = "Please enter a model name." @@ -7987,6 +7996,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SINGLEINPUTDIALOG::T4030229154"] = "Your Inp -- Cancel UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SINGLEINPUTDIALOG::T900713019"] = "Cancel" +-- Hugging Face Inference Provider +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T1085481431"] = "Hugging Face Inference Provider" + -- Failed to store the API key in the operating system. The message was: {0}. Please try again. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T1122745046"] = "Failed to store the API key in the operating system. The message was: {0}. Please try again." @@ -8029,6 +8041,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T2810182573"] = -- Instance Name UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T2842060373"] = "Instance Name" +-- Hugging Face transcribes audio through a few of its inference providers only, which is why this list is shorter than the one for chatting. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T3397943774"] = "Hugging Face transcribes audio through a few of its inference providers only, which is why this list is shorter than the one for chatting." + -- Please enter a transcription model name. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T3703662664"] = "Please enter a transcription model name." @@ -9274,6 +9289,21 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCELEVELEXTENSIONS::T3188327965"] = -- Very Low UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCELEVELEXTENSIONS::T786675843"] = "Very Low" +-- Automatic: the cheapest provider +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::HUGGINGFACE::HFINFERENCEPROVIDEREXTENSIONS::T1680748563"] = "Automatic: the cheapest provider" + +-- Automatic: your preferred order +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::HUGGINGFACE::HFINFERENCEPROVIDEREXTENSIONS::T2027398472"] = "Automatic: your preferred order" + +-- Automatic: the fastest provider +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::HUGGINGFACE::HFINFERENCEPROVIDEREXTENSIONS::T997045984"] = "Automatic: the fastest provider" + +-- No Hugging Face inference provider offers the selected model. Please check the model name and whether it is still available on Hugging Face. +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::HUGGINGFACE::PROVIDERHUGGINGFACE::T1055093108"] = "No Hugging Face inference provider offers the selected model. Please check the model name and whether it is still available on Hugging Face." + +-- The Hugging Face inference provider '{0}' does not offer the selected model. Please select another inference provider, or let Hugging Face choose one for you. +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::HUGGINGFACE::PROVIDERHUGGINGFACE::T3314840969"] = "The Hugging Face inference provider '{0}' does not offer the selected model. Please select another inference provider, or let Hugging Face choose one for you." + -- Self-hosted UI_TEXT_CONTENT["AISTUDIO::PROVIDER::LLMPROVIDERSEXTENSIONS::T146444217"] = "Self-hosted" @@ -11197,9 +11227,15 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T3550629491"] -- Please enter an instance name. UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T3999823516"] = "Please enter an instance name." +-- This Hugging Face inference provider does not transcribe audio. Please select another one. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T4142849031"] = "This Hugging Face inference provider does not transcribe audio. Please select another one." + -- Please select an Hugging Face inference provider. UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T497939286"] = "Please select an Hugging Face inference provider." +-- This Hugging Face inference provider does not create embeddings. Please select another one. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T649507886"] = "This Hugging Face inference provider does not create embeddings. Please select another one." + -- Please select a model. UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T818893091"] = "Please select a model." diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs index ba0f4587..2f2110c1 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs @@ -88,6 +88,7 @@ public partial class SettingsPanelEmbeddings : SettingsPanelProviderBase { x => x.IsSelfHosted, embeddingProvider.IsSelfHosted }, { x => x.IsEditing, true }, { x => x.DataHost, embeddingProvider.Host }, + { x => x.HFInferenceProviderId, embeddingProvider.HFInferenceProvider }, { x => x.IsEnterpriseConfiguration, embeddingProvider.IsEnterpriseConfiguration }, }; diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor.cs index 252a95a4..ccfe0e97 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor.cs +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor.cs @@ -86,6 +86,7 @@ public partial class SettingsPanelTranscription : SettingsPanelProviderBase { x => x.IsSelfHosted, transcriptionProvider.IsSelfHosted }, { x => x.IsEditing, true }, { x => x.DataHost, transcriptionProvider.Host }, + { x => x.HFInferenceProviderId, transcriptionProvider.HFInferenceProvider }, { x => x.IsEnterpriseConfiguration, transcriptionProvider.IsEnterpriseConfiguration }, }; diff --git a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor index 5bcead75..b48f9ef5 100644 --- a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor +++ b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor @@ -1,4 +1,5 @@ @using AIStudio.Provider +@using AIStudio.Provider.HuggingFace @using AIStudio.Provider.SelfHosted @inherits MSGComponentBase @@ -64,6 +65,24 @@ } + @if (this.DataLLMProvider.IsHFInstanceProviderNeeded()) + { + + @foreach (HFInferenceProvider inferenceProvider in Enum.GetValues(typeof(HFInferenceProvider))) + { + @if (inferenceProvider.SupportsEmbeddings()) + { + + @inferenceProvider.ToName() + + } + } + + + @T("Hugging Face offers embeddings through a few of its inference providers only, which is why this list is shorter than the one for chatting.") + + } + @if (this.DataLLMProvider.IsEmbeddingModelProvidedManually(this.DataHost)) diff --git a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs index 4bb3c189..f82499d0 100644 --- a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs @@ -1,5 +1,6 @@ using AIStudio.Components; using AIStudio.Provider; +using AIStudio.Provider.HuggingFace; using AIStudio.Settings; using AIStudio.Tools.Services; using AIStudio.Tools.Validation; @@ -68,6 +69,12 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId /// [Parameter] public Model DataModel { get; set; } + + /// + /// The Hugging Face inference provider to use. + /// + [Parameter] + public HFInferenceProvider HFInferenceProviderId { get; set; } = HFInferenceProvider.NONE; /// /// Should the dialog be in editing mode? @@ -151,6 +158,7 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId IsEnterpriseConfiguration = this.IsEnterpriseConfiguration, EnterpriseConfigurationPluginId = Guid.Empty, CustomIconDataUrl = this.DataCustomIconDataUrl, + HFInferenceProvider = this.HFInferenceProviderId, }; } @@ -316,6 +324,22 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId this.dataLoadingModelsIssue = string.Empty; } + /// + /// Resets the model selection when the user picks another Hugging Face inference provider. + /// + /// + /// Each inference provider offers embedding models of its own, so the models loaded for the + /// previous one say nothing about the new one. + /// + /// The inference provider the user chose. + private void OnHFInferenceProviderChanged(HFInferenceProvider selectedInferenceProvider) + { + this.HFInferenceProviderId = selectedInferenceProvider; + this.DataModel = default; + this.availableModels.Clear(); + this.dataLoadingModelsIssue = string.Empty; + } + private async Task ReloadModels() { this.dataLoadingModelsIssue = string.Empty; diff --git a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor index eb97b251..7b6bde33 100644 --- a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor +++ b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor @@ -69,19 +69,20 @@ @if (this.DataLLMProvider.IsHFInstanceProviderNeeded()) { - + @foreach (HFInferenceProvider inferenceProvider in Enum.GetValues(typeof(HFInferenceProvider))) { - - @inferenceProvider.ToName() - - } + @if (inferenceProvider.SupportsChat()) + { + + @inferenceProvider.ToName() + + } + } - @* ReSharper disable Asp.Entity *@ - Please double-check if your model name matches the curl specifications provided by the inference provider. If it doesn't, you might get a Not Found error when trying to use the model. Here's a curl example. + @T("Choose which inference provider should answer your requests. When you pick one of the automatic options instead, Hugging Face selects a provider for you and switches to another one when your choice is unavailable.") - @* ReSharper restore Asp.Entity *@ } @if (!this.IsLLMModelSelectionHidden) @@ -90,7 +91,7 @@ @if (this.DataLLMProvider.IsLLMModelProvidedManually()) { - + @T("Show available models") + @T("Show available models") + + } @T("Load models") diff --git a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs index f491da1c..88908ce2 100644 --- a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs @@ -226,7 +226,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId { this.dataEditingPreviousInstanceName = this.DataInstanceName.ToLowerInvariant(); - // When using Fireworks or Hugging Face, we must copy the model name: + // When using Fireworks, we must copy the model name: if (this.DataLLMProvider.IsLLMModelProvidedManually()) this.dataManuallyModel = this.DataModel.Id; @@ -372,6 +372,24 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId this.usesLegacySystemModelFallback = false; } + /// + /// Resets the model selection when the user picks another Hugging Face inference provider. + /// + /// + /// Which models are on offer depends on the inference provider, so the models loaded for the + /// previous one say nothing about the new one. Keeping them would let the user pick a model + /// their provider does not serve, which the router answers with an error. + /// + /// The inference provider the user chose. + private void OnHFInferenceProviderChanged(HFInferenceProvider selectedInferenceProvider) + { + this.HFInferenceProviderId = selectedInferenceProvider; + this.DataModel = default; + this.capabilityOverrides = new(); + this.availableModels.Clear(); + this.dataLoadingModelsIssue = string.Empty; + } + private void OnHostChanged(Host selectedHost) { // When the host changes, reset the model selection state: @@ -430,6 +448,11 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId this.DataHost is Host.LLAMA_CPP && this.usesLegacySystemModelFallback; + /// + /// The catalog of the provider, where the user can read up on the models before choosing one. + /// + private string ModelsOverviewURL => this.DataLLMProvider.GetModelsOverviewURL(this.HFInferenceProviderId); + private void UpdateModelSelectionAfterLoading() { if (this.DataLLMProvider is not LLMProviders.SELF_HOSTED || this.DataHost is not Host.LLAMA_CPP) diff --git a/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor b/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor index e92889a4..826a2959 100644 --- a/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor +++ b/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor @@ -1,4 +1,5 @@ @using AIStudio.Provider +@using AIStudio.Provider.HuggingFace @using AIStudio.Provider.SelfHosted @inherits MSGComponentBase @@ -64,6 +65,24 @@ } + @if (this.DataLLMProvider.IsHFInstanceProviderNeeded()) + { + + @foreach (HFInferenceProvider inferenceProvider in Enum.GetValues(typeof(HFInferenceProvider))) + { + @if (inferenceProvider.SupportsTranscription()) + { + + @inferenceProvider.ToName() + + } + } + + + @T("Hugging Face transcribes audio through a few of its inference providers only, which is why this list is shorter than the one for chatting.") + + } + @if (!this.DataLLMProvider.IsTranscriptionModelSelectionHidden(this.DataHost)) { diff --git a/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor.cs index 1e1081f3..b596fcdd 100644 --- a/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor.cs @@ -1,5 +1,6 @@ using AIStudio.Components; using AIStudio.Provider; +using AIStudio.Provider.HuggingFace; using AIStudio.Settings; using AIStudio.Tools.Services; using AIStudio.Tools.Validation; @@ -68,6 +69,12 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId /// [Parameter] public Model DataModel { get; set; } + + /// + /// The Hugging Face inference provider to use. + /// + [Parameter] + public HFInferenceProvider HFInferenceProviderId { get; set; } = HFInferenceProvider.NONE; /// /// Should the dialog be in editing mode? @@ -166,6 +173,7 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId IsEnterpriseConfiguration = this.IsEnterpriseConfiguration, EnterpriseConfigurationPluginId = Guid.Empty, CustomIconDataUrl = this.DataCustomIconDataUrl, + HFInferenceProvider = this.HFInferenceProviderId, }; } @@ -331,6 +339,22 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId this.dataLoadingModelsIssue = string.Empty; } + /// + /// Resets the model selection when the user picks another Hugging Face inference provider. + /// + /// + /// Each inference provider offers transcription models of its own, so the models loaded for the + /// previous one say nothing about the new one. + /// + /// The inference provider the user chose. + private void OnHFInferenceProviderChanged(HFInferenceProvider selectedInferenceProvider) + { + this.HFInferenceProviderId = selectedInferenceProvider; + this.DataModel = default; + this.availableModels.Clear(); + this.dataLoadingModelsIssue = string.Empty; + } + private async Task ReloadModels() { this.dataLoadingModelsIssue = string.Empty; diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua index 747ae2ef..5317d830 100644 --- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua +++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua @@ -120,7 +120,15 @@ CONFIG["LLM_PROVIDERS"] = {} -- -- }, -- -- -- Optional: Hugging Face inference provider. Only relevant for UsedLLMProvider = HUGGINGFACE. --- -- Allowed values are: CEREBRAS, NEBIUS_AI_STUDIO, SAMBANOVA, NOVITA, HYPERBOLIC, TOGETHER_AI, FIREWORKS, HF_INFERENCE_API +-- -- Allowed values are: BASETEN, CEREBRAS, COHERE, DEEPINFRA, FEATHERLESS_AI, FIREWORKS, GROQ, +-- -- NOVITA, NSCALE, OVHCLOUD, PUBLIC_AI, SCALEWAY, TOGETHER_AI, ZAI +-- -- Instead of naming a provider, you may let Hugging Face choose one: +-- -- AUTOMATIC (the fastest), CHEAPEST, or PREFERRED (the order configured in your +-- -- Hugging Face account). An automatic choice also fails over to another provider when the +-- -- selected one is unavailable. +-- -- Note: Hugging Face stopped routing HYPERBOLIC, SAMBANOVA, and NEBIUS_AI_STUDIO in July +-- -- 2026, and HF_INFERENCE_API serves no models we can reach. Configurations still naming one +-- -- of them are treated as if no provider was set, and the user is asked to choose again. -- -- ["HFInferenceProvider"] = "NOVITA", -- -- -- Optional: Encrypted API key for cloud providers or secured on-premise models. @@ -171,6 +179,12 @@ CONFIG["TRANSCRIPTION_PROVIDERS"] = {} -- -- above: when both are set, the embedded key is ignored and a warning is logged. -- -- ["AllowUserProvidedAPIKey"] = true, -- +-- -- Optional: Hugging Face inference provider. Only relevant for UsedLLMProvider = HUGGINGFACE. +-- -- Hugging Face transcribes audio through some of its inference providers only, so the choice +-- -- is narrower than for chatting. Allowed values are: DEEPINFRA and TOGETHER_AI. The automatic +-- -- options are not available here, because a transcription request has to name its provider. +-- -- ["HFInferenceProvider"] = "TOGETHER_AI", +-- -- ["Model"] = { -- ["Id"] = "", -- ["DisplayName"] = "", @@ -202,6 +216,12 @@ CONFIG["EMBEDDING_PROVIDERS"] = {} -- -- above: when both are set, the embedded key is ignored and a warning is logged. -- -- ["AllowUserProvidedAPIKey"] = true, -- +-- -- Optional: Hugging Face inference provider. Only relevant for UsedLLMProvider = HUGGINGFACE. +-- -- Hugging Face serves embeddings through some of its inference providers only, so the choice +-- -- is narrower than for chatting. Allowed values are: DEEPINFRA and TOGETHER_AI. The automatic +-- -- options are not available here, because an embedding request has to name its provider. +-- -- ["HFInferenceProvider"] = "TOGETHER_AI", +-- -- ["Model"] = { -- ["Id"] = "", -- ["DisplayName"] = "", 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 f93095be..99ec52b5 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 @@ -5946,6 +5946,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGMETHODDIALOG::T662524223"] = "Ein L -- Cancel UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGMETHODDIALOG::T900713019"] = "Abbrechen" +-- Hugging Face Inference Provider +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T1085481431"] = "Hugging Face-Inferenzanbieter" + -- Failed to store the API key in the operating system. The message was: {0}. Please try again. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T1122745046"] = "Der API-Schlüssel konnte nicht im Betriebssystem gespeichert werden. Die Meldung war: {0}. Bitte versuchen Sie es erneut." @@ -5970,6 +5973,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T1847791252"] = "Ak -- Failed to load the API key from the operating system. The message was: {0}. You might ignore this message and provide the API key again. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T1870831108"] = "Der API-Schlüssel konnte nicht vom Betriebssystem geladen werden. Die Meldung war: {0}. Sie können diese Meldung ignorieren und den API-Schlüssel erneut eingeben." +-- Hugging Face offers embeddings through a few of its inference providers only, which is why this list is shorter than the one for chatting. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T194295715"] = "Hugging Face bietet Einbettungen nur über einige seiner Inferenzanbieter an. Deshalb ist diese Liste kürzer als die für Chats." + -- Model UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T2189814010"] = "Modell" @@ -6363,6 +6369,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1870831108"] = "Der API-Sch -- Speech input UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1874348907"] = "Spracheingabe" +-- Choose which inference provider should answer your requests. When you pick one of the automatic options instead, Hugging Face selects a provider for you and switches to another one when your choice is unavailable. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1889879830"] = "Wählen Sie aus, welcher Inferenzanbieter Ihre Anfragen beantworten soll. Wenn Sie stattdessen eine der automatischen Optionen auswählen, wählt Hugging Face einen Anbieter für Sie aus und wechselt zu einem anderen, wenn der von Ihnen ausgewählte Anbieter nicht verfügbar ist." + -- Please enter a model name. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1936099896"] = "Bitte geben Sie einen Modellnamen ein." @@ -7989,6 +7998,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SINGLEINPUTDIALOG::T4030229154"] = "Ihre Ein -- Cancel UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SINGLEINPUTDIALOG::T900713019"] = "Abbrechen" +-- Hugging Face Inference Provider +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T1085481431"] = "Hugging Face-Inferenzanbieter" + -- Failed to store the API key in the operating system. The message was: {0}. Please try again. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T1122745046"] = "Der API-Schlüssel konnte nicht im Betriebssystem gespeichert werden. Die Meldung lautete: '{0}'. Bitte versuchen Sie es erneut." @@ -8031,6 +8043,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T2810182573"] = -- Instance Name UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T2842060373"] = "Instanzname" +-- Hugging Face transcribes audio through a few of its inference providers only, which is why this list is shorter than the one for chatting. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T3397943774"] = "Hugging Face transkribiert Audio nur über einige seiner Inferenzanbieter. Deshalb ist diese Liste kürzer als die für den Chat." + -- Please enter a transcription model name. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T3703662664"] = "Bitte geben Sie den Namen eines Transkriptionsmodells ein." @@ -9276,6 +9291,21 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCELEVELEXTENSIONS::T3188327965"] = -- Very Low UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCELEVELEXTENSIONS::T786675843"] = "Sehr niedrig" +-- Automatic: the cheapest provider +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::HUGGINGFACE::HFINFERENCEPROVIDEREXTENSIONS::T1680748563"] = "Automatisch: der günstigste Anbieter" + +-- Automatic: your preferred order +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::HUGGINGFACE::HFINFERENCEPROVIDEREXTENSIONS::T2027398472"] = "Automatisch: Ihre bevorzugte Reihenfolge" + +-- Automatic: the fastest provider +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::HUGGINGFACE::HFINFERENCEPROVIDEREXTENSIONS::T997045984"] = "Automatisch: der schnellste Anbieter" + +-- No Hugging Face inference provider offers the selected model. Please check the model name and whether it is still available on Hugging Face. +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::HUGGINGFACE::PROVIDERHUGGINGFACE::T1055093108"] = "Kein Hugging-Face-Inferenzanbieter bietet das ausgewählte Modell an. Bitte prüfen Sie den Modellnamen und ob das Modell auf Hugging Face noch verfügbar ist." + +-- The Hugging Face inference provider '{0}' does not offer the selected model. Please select another inference provider, or let Hugging Face choose one for you. +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::HUGGINGFACE::PROVIDERHUGGINGFACE::T3314840969"] = "Der Hugging Face-Inferenzanbieter „{0}“ bietet das ausgewählte Modell nicht an. Bitte wählen Sie einen anderen Inferenzanbieter aus oder lassen Sie Hugging Face einen Anbieter für Sie auswählen." + -- Self-hosted UI_TEXT_CONTENT["AISTUDIO::PROVIDER::LLMPROVIDERSEXTENSIONS::T146444217"] = "Selbst gehostet" @@ -11199,9 +11229,15 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T3550629491"] -- Please enter an instance name. UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T3999823516"] = "Bitte geben Sie einen Instanznamen ein." +-- This Hugging Face inference provider does not transcribe audio. Please select another one. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T4142849031"] = "Dieser Hugging-Face-Inferenzanbieter transkribiert keine Audiodateien. Bitte wählen Sie einen anderen aus." + -- Please select an Hugging Face inference provider. UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T497939286"] = "Bitte wählen Sie einen Hugging Face-Inferenzanbieter aus." +-- This Hugging Face inference provider does not create embeddings. Please select another one. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T649507886"] = "Dieser Hugging-Face-Inferenzanbieter erstellt keine Einbettungen. Bitte wählen Sie einen anderen aus." + -- Please select a model. UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T818893091"] = "Bitte wählen Sie ein Modell aus." 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 ff2afa5f..2aa2f599 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 @@ -5946,6 +5946,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGMETHODDIALOG::T662524223"] = "A lin -- Cancel UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGMETHODDIALOG::T900713019"] = "Cancel" +-- Hugging Face Inference Provider +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T1085481431"] = "Hugging Face Inference Provider" + -- Failed to store the API key in the operating system. The message was: {0}. Please try again. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T1122745046"] = "Failed to store the API key in the operating system. The message was: {0}. Please try again." @@ -5970,6 +5973,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T1847791252"] = "Up -- Failed to load the API key from the operating system. The message was: {0}. You might ignore this message and provide the API key again. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T1870831108"] = "Failed to load the API key from the operating system. The message was: {0}. You might ignore this message and provide the API key again." +-- Hugging Face offers embeddings through a few of its inference providers only, which is why this list is shorter than the one for chatting. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T194295715"] = "Hugging Face offers embeddings through a few of its inference providers only, which is why this list is shorter than the one for chatting." + -- Model UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T2189814010"] = "Model" @@ -6363,6 +6369,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1870831108"] = "Failed to l -- Speech input UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1874348907"] = "Speech input" +-- Choose which inference provider should answer your requests. When you pick one of the automatic options instead, Hugging Face selects a provider for you and switches to another one when your choice is unavailable. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1889879830"] = "Choose which inference provider should answer your requests. When you pick one of the automatic options instead, Hugging Face selects a provider for you and switches to another one when your choice is unavailable." + -- Please enter a model name. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1936099896"] = "Please enter a model name." @@ -7989,6 +7998,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SINGLEINPUTDIALOG::T4030229154"] = "Your Inp -- Cancel UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SINGLEINPUTDIALOG::T900713019"] = "Cancel" +-- Hugging Face Inference Provider +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T1085481431"] = "Hugging Face Inference Provider" + -- Failed to store the API key in the operating system. The message was: {0}. Please try again. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T1122745046"] = "Failed to store the API key in the operating system. The message was: {0}. Please try again." @@ -8031,6 +8043,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T2810182573"] = -- Instance Name UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T2842060373"] = "Instance Name" +-- Hugging Face transcribes audio through a few of its inference providers only, which is why this list is shorter than the one for chatting. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T3397943774"] = "Hugging Face transcribes audio through a few of its inference providers only, which is why this list is shorter than the one for chatting." + -- Please enter a transcription model name. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T3703662664"] = "Please enter a transcription model name." @@ -9276,6 +9291,21 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCELEVELEXTENSIONS::T3188327965"] = -- Very Low UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCELEVELEXTENSIONS::T786675843"] = "Very Low" +-- Automatic: the cheapest provider +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::HUGGINGFACE::HFINFERENCEPROVIDEREXTENSIONS::T1680748563"] = "Automatic: the cheapest provider" + +-- Automatic: your preferred order +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::HUGGINGFACE::HFINFERENCEPROVIDEREXTENSIONS::T2027398472"] = "Automatic: your preferred order" + +-- Automatic: the fastest provider +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::HUGGINGFACE::HFINFERENCEPROVIDEREXTENSIONS::T997045984"] = "Automatic: the fastest provider" + +-- No Hugging Face inference provider offers the selected model. Please check the model name and whether it is still available on Hugging Face. +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::HUGGINGFACE::PROVIDERHUGGINGFACE::T1055093108"] = "No Hugging Face inference provider offers the selected model. Please check the model name and whether it is still available on Hugging Face." + +-- The Hugging Face inference provider '{0}' does not offer the selected model. Please select another inference provider, or let Hugging Face choose one for you. +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::HUGGINGFACE::PROVIDERHUGGINGFACE::T3314840969"] = "The Hugging Face inference provider '{0}' does not offer the selected model. Please select another inference provider, or let Hugging Face choose one for you." + -- Self-hosted UI_TEXT_CONTENT["AISTUDIO::PROVIDER::LLMPROVIDERSEXTENSIONS::T146444217"] = "Self-hosted" @@ -11199,9 +11229,15 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T3550629491"] -- Please enter an instance name. UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T3999823516"] = "Please enter an instance name." +-- This Hugging Face inference provider does not transcribe audio. Please select another one. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T4142849031"] = "This Hugging Face inference provider does not transcribe audio. Please select another one." + -- Please select an Hugging Face inference provider. UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T497939286"] = "Please select an Hugging Face inference provider." +-- This Hugging Face inference provider does not create embeddings. Please select another one. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T649507886"] = "This Hugging Face inference provider does not create embeddings. Please select another one." + -- Please select a model. UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T818893091"] = "Please select a model." diff --git a/app/MindWork AI Studio/Provider/BaseProvider.cs b/app/MindWork AI Studio/Provider/BaseProvider.cs index f0108a8d..f1c7c010 100644 --- a/app/MindWork AI Studio/Provider/BaseProvider.cs +++ b/app/MindWork AI Studio/Provider/BaseProvider.cs @@ -376,8 +376,13 @@ public abstract class BaseProvider : IProvider, ISecretId errorCode = TryGetString(root, "code"); errorType = TryGetString(root, "type"); - // Services built on FastAPI, such as Helmholtz Blablador, word their errors as "detail": - errorMessage = TryGetString(root, "message") ?? TryGetString(root, "detail"); + // + // Services built on FastAPI, such as Helmholtz Blablador, word their errors as "detail". + // And some providers put the sentence straight into "error" instead of an object, e.g. + // {"error": "Model not supported by provider novita"}. The object form was handled above, + // so reading "error" here can only meet the plain sentence: + // + errorMessage = TryGetString(root, "message") ?? TryGetString(root, "detail") ?? TryGetString(root, "error"); } /// @@ -510,16 +515,35 @@ public abstract class BaseProvider : IProvider, ISecretId if(nextResponse.StatusCode is HttpStatusCode.BadRequest) { + // + // The provider explains the problem in the body, while the reason phrase says no + // more than "Bad Request". We show that explanation and fall back to the phrase + // only when the body carries none: + // + var badRequestMessage = ReadProviderErrorMessage(errorBody); + if (string.IsNullOrWhiteSpace(badRequestMessage)) + badRequestMessage = nextResponse.ReasonPhrase; + + // + // When we recognize what went wrong, we say what it means for the user instead of + // guessing at the message format. The classification happened above already: + // + var classifiedMessage = this.GetProviderRequestFailureUserMessage(providerRequestFailure); + if(!string.IsNullOrWhiteSpace(classifiedMessage)) + { + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, classifiedMessage)); + } + // Check if the error body contains "context" and "token" (case-insensitive), // which indicates that the context window is likely exceeded: - if(errorBody.Contains("context", StringComparison.InvariantCultureIgnoreCase) && + else if(errorBody.Contains("context", StringComparison.InvariantCultureIgnoreCase) && errorBody.Contains("token", StringComparison.InvariantCultureIgnoreCase)) { - await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). The data of the chat, including all file attachments, is probably too large for the selected model and provider. The provider message is: '{2}'"), this.InstanceName, this.Provider, nextResponse.ReasonPhrase))); + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). The data of the chat, including all file attachments, is probably too large for the selected model and provider. The provider message is: '{2}'"), this.InstanceName, this.Provider, badRequestMessage))); } else { - await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). The required message format might be changed. The provider message is: '{2}'"), this.InstanceName, this.Provider, nextResponse.ReasonPhrase))); + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). The required message format might be changed. The provider message is: '{2}'"), this.InstanceName, this.Provider, badRequestMessage))); } this.logger.LogError("Failed request with status code {ResponseStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody); diff --git a/app/MindWork AI Studio/Provider/HuggingFace/HFEndpointKind.cs b/app/MindWork AI Studio/Provider/HuggingFace/HFEndpointKind.cs new file mode 100644 index 00000000..d1c821bd --- /dev/null +++ b/app/MindWork AI Studio/Provider/HuggingFace/HFEndpointKind.cs @@ -0,0 +1,29 @@ +namespace AIStudio.Provider.HuggingFace; + +/// +/// Which of the Hugging Face endpoints a provider instance talks to. +/// +/// +/// Hugging Face serves chatting and everything else from different places. Chat completions go to +/// the router's own OpenAI-compatible endpoint, which accepts the model IDs as the hub writes them +/// and picks an inference provider from a suffix. Embeddings and transcription do not exist there +/// at all and have to be asked of one provider's own route. Because the base URL is fixed when a +/// provider instance is built, the instance has to know from the start which one it is for. +/// +public enum HFEndpointKind +{ + /// + /// The router's own endpoint, which serves chat completions. + /// + CHAT, + + /// + /// The OpenAI-compatible route of one inference provider, which serves embeddings. + /// + EMBEDDING, + + /// + /// The OpenAI-compatible route of one inference provider, which transcribes audio. + /// + TRANSCRIPTION, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/HuggingFace/HFInferenceProvider.cs b/app/MindWork AI Studio/Provider/HuggingFace/HFInferenceProvider.cs index 01b722eb..a7a2bba8 100644 --- a/app/MindWork AI Studio/Provider/HuggingFace/HFInferenceProvider.cs +++ b/app/MindWork AI Studio/Provider/HuggingFace/HFInferenceProvider.cs @@ -3,16 +3,46 @@ /// /// Enum for inference providers that Hugging Face supports. /// +/// +/// Besides the providers themselves, this enum carries the routing strategies Hugging Face offers. +/// They are no providers, but they take the same place: the router picks a provider for us instead +/// of us naming one. +/// +/// NONE must stay the first value: settings are read through the tolerant enum converter, which +/// falls back to the first value whenever it meets a name we no longer know. That is what happens +/// to a configuration naming one of the providers Hugging Face stopped routing in July 2026 +/// (Hyperbolic, SambaNova, Nebius, NVIDIA, Clarifai, Black Forest Labs), and to one naming the +/// Hugging Face Inference API, which serves no model we can reach: it has no chat models at all, +/// and its OpenAI-compatible routes for embeddings and transcription do not exist. Such a provider +/// has to end up on NONE, where the validation asks the user to choose again. Were a routing +/// strategy first, those configurations would silently switch to automatic routing instead. +/// public enum HFInferenceProvider { NONE, - + + // + // Routing strategies. Hugging Face writes them where a provider name would go: + // + AUTOMATIC, + CHEAPEST, + PREFERRED, + + // + // The providers Hugging Face routes: + // + BASETEN, CEREBRAS, - NEBIUS_AI_STUDIO, - SAMBANOVA, - NOVITA, - HYPERBOLIC, - TOGETHER_AI, + COHERE, + DEEPINFRA, + FEATHERLESS_AI, FIREWORKS, - HF_INFERENCE_API, + GROQ, + NOVITA, + NSCALE, + OVHCLOUD, + PUBLIC_AI, + SCALEWAY, + TOGETHER_AI, + ZAI, } \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/HuggingFace/HFInferenceProviderExtensions.cs b/app/MindWork AI Studio/Provider/HuggingFace/HFInferenceProviderExtensions.cs index 0e103938..de57ea88 100644 --- a/app/MindWork AI Studio/Provider/HuggingFace/HFInferenceProviderExtensions.cs +++ b/app/MindWork AI Studio/Provider/HuggingFace/HFInferenceProviderExtensions.cs @@ -1,43 +1,158 @@ -namespace AIStudio.Provider.HuggingFace; +using AIStudio.Tools.PluginSystem; + +namespace AIStudio.Provider.HuggingFace; public static class HFInferenceProviderExtensions { - public static string Endpoints(this HFInferenceProvider provider, Model model) => provider switch - { - HFInferenceProvider.CEREBRAS => "cerebras/v1/", - HFInferenceProvider.NEBIUS_AI_STUDIO => "nebius/v1/", - HFInferenceProvider.SAMBANOVA => "sambanova/v1/", - HFInferenceProvider.NOVITA => "novita/v3/openai/", - HFInferenceProvider.HYPERBOLIC => "hyperbolic/v1/", - HFInferenceProvider.TOGETHER_AI => "together/v1/", - HFInferenceProvider.FIREWORKS => "fireworks-ai/inference/v1/", - HFInferenceProvider.HF_INFERENCE_API => $"hf-inference/models/{model.ToString()}/v1/", - _ => string.Empty, - }; - + private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(HFInferenceProviderExtensions).Namespace, nameof(HFInferenceProviderExtensions)); + + /// + /// The slug Hugging Face uses for this inference provider. + /// + /// The inference provider. + /// The slug, or an empty string for the routing strategies, which name no provider. public static string EndpointsId(this HFInferenceProvider provider) => provider switch { + HFInferenceProvider.BASETEN => "baseten", HFInferenceProvider.CEREBRAS => "cerebras", - HFInferenceProvider.NEBIUS_AI_STUDIO => "nebius", - HFInferenceProvider.SAMBANOVA => "sambanova", + HFInferenceProvider.COHERE => "cohere", + HFInferenceProvider.DEEPINFRA => "deepinfra", + HFInferenceProvider.FEATHERLESS_AI => "featherless-ai", + HFInferenceProvider.FIREWORKS => "fireworks-ai", + HFInferenceProvider.GROQ => "groq", HFInferenceProvider.NOVITA => "novita", - HFInferenceProvider.HYPERBOLIC => "hyperbolic", + HFInferenceProvider.NSCALE => "nscale", + HFInferenceProvider.OVHCLOUD => "ovhcloud", + HFInferenceProvider.PUBLIC_AI => "publicai", + HFInferenceProvider.SCALEWAY => "scaleway", HFInferenceProvider.TOGETHER_AI => "together", - HFInferenceProvider.FIREWORKS => "fireworks", - HFInferenceProvider.HF_INFERENCE_API => "hf-inference", + HFInferenceProvider.ZAI => "zai-org", + _ => string.Empty, }; - + + /// + /// The suffix which tells the router where to send the request. + /// + /// + /// The router serves every provider through one endpoint. Which provider answers is decided by + /// a suffix on the model name, e.g. "google/gemma-4-31B-it:novita". Without a suffix, the router + /// picks the fastest provider itself. + /// + /// The inference provider. + /// The suffix including its colon, or an empty string when the router should choose. + public static string ModelSuffix(this HFInferenceProvider provider) => provider switch + { + HFInferenceProvider.NONE or HFInferenceProvider.AUTOMATIC => string.Empty, + + HFInferenceProvider.CHEAPEST => ":cheapest", + HFInferenceProvider.PREFERRED => ":preferred", + + _ => $":{provider.EndpointsId()}", + }; + + /// + /// Whether this inference provider serves models to chat with. + /// + /// The inference provider. + /// True, when the provider serves chat models. + public static bool SupportsChat(this HFInferenceProvider provider) => provider is not HFInferenceProvider.NONE; + + /// + /// Whether this inference provider creates embeddings for us. + /// + /// + /// Embeddings are a much shorter story than chatting. The router serves them nowhere near its + /// own endpoint, only through the route of a provider, and only two of those answer the + /// OpenAI-compatible form we send. The routing strategies are out by their nature: without a + /// named provider there is no route to address. + /// + /// The inference provider. + /// True, when we can create embeddings through this provider. + public static bool SupportsEmbeddings(this HFInferenceProvider provider) => provider is HFInferenceProvider.TOGETHER_AI or HFInferenceProvider.DEEPINFRA; + + /// + /// Whether this inference provider transcribes audio for us. + /// + /// + /// The same two providers as for embeddings, and for the same reason: transcription lives on a + /// provider's own route, and only these two answer the OpenAI-compatible form there. Others do + /// transcribe for Hugging Face, but not in a shape we could send an audio file to: fal-ai and + /// Replicate both turn the request down with "Model not supported by provider". + /// + /// The inference provider. + /// True, when we can transcribe audio through this provider. + public static bool SupportsTranscription(this HFInferenceProvider provider) => provider is HFInferenceProvider.TOGETHER_AI or HFInferenceProvider.DEEPINFRA; + + /// + /// The base URL of the provider's own OpenAI-compatible route. + /// + /// + /// Only chatting goes through the router's own endpoint. Everything else has to address the + /// provider directly, and they do not agree on where their OpenAI-compatible API sits: DeepInfra + /// keeps it below an additional "openai" segment, and answers the path without it with + /// "Not allowed to POST /v1/embeddings for provider deepinfra". + /// + /// The inference provider. + /// The base URL, or an empty string when the provider has no such route. + public static string ProviderBaseURL(this HFInferenceProvider provider) => provider switch + { + HFInferenceProvider.TOGETHER_AI => "https://router.huggingface.co/together/v1/", + HFInferenceProvider.DEEPINFRA => "https://router.huggingface.co/deepinfra/v1/openai/", + + _ => string.Empty, + }; + + /// + /// Removes the routing suffix from a model, if it carries one. + /// + /// + /// The suffix says where a request goes, not what the model is. Everything asking what a model + /// can do has to look at the bare name: "google/gemma-4-31B-it:novita" is the same model as + /// "google/gemma-4-31B-it", and a name detection which never heard of the suffix would miss it. + /// Model IDs on the hub are written as "org/model" and carry no colon of their own, so the last + /// colon always starts the suffix. + /// + /// The model as it is configured. + /// The model without its routing suffix. + public static Model WithoutRoutingSuffix(this Model model) + { + var separatorIndex = model.Id.LastIndexOf(':'); + return separatorIndex is -1 ? model : model with { Id = model.Id[..separatorIndex] }; + } + + /// + /// The value to filter the Hugging Face model catalog by. + /// + /// The inference provider. + /// The provider slug, or "all" when no particular provider was chosen. + public static string CatalogFilter(this HFInferenceProvider provider) + { + var slug = provider.EndpointsId(); + return string.IsNullOrEmpty(slug) ? "all" : slug; + } + public static string ToName(this HFInferenceProvider provider) => provider switch { + HFInferenceProvider.AUTOMATIC => TB("Automatic: the fastest provider"), + HFInferenceProvider.CHEAPEST => TB("Automatic: the cheapest provider"), + HFInferenceProvider.PREFERRED => TB("Automatic: your preferred order"), + + HFInferenceProvider.BASETEN => "Baseten", HFInferenceProvider.CEREBRAS => "Cerebras", - HFInferenceProvider.NEBIUS_AI_STUDIO => "Nebius AI Studio", - HFInferenceProvider.SAMBANOVA => "Sambanova", - HFInferenceProvider.NOVITA => "Novita", - HFInferenceProvider.HYPERBOLIC => "Hyperbolic", - HFInferenceProvider.TOGETHER_AI => "Together AI", + HFInferenceProvider.COHERE => "Cohere", + HFInferenceProvider.DEEPINFRA => "DeepInfra", + HFInferenceProvider.FEATHERLESS_AI => "Featherless AI", HFInferenceProvider.FIREWORKS => "Fireworks AI", - HFInferenceProvider.HF_INFERENCE_API => "Hugging Face Inference API", + HFInferenceProvider.GROQ => "Groq", + HFInferenceProvider.NOVITA => "Novita", + HFInferenceProvider.NSCALE => "Nscale", + HFInferenceProvider.OVHCLOUD => "OVHcloud", + HFInferenceProvider.PUBLIC_AI => "Public AI", + HFInferenceProvider.SCALEWAY => "Scaleway", + HFInferenceProvider.TOGETHER_AI => "Together AI", + HFInferenceProvider.ZAI => "Z.ai", + _ => string.Empty, }; } \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/HuggingFace/ModelsResponse.cs b/app/MindWork AI Studio/Provider/HuggingFace/ModelsResponse.cs new file mode 100644 index 00000000..79f9847b --- /dev/null +++ b/app/MindWork AI Studio/Provider/HuggingFace/ModelsResponse.cs @@ -0,0 +1,37 @@ +namespace AIStudio.Provider.HuggingFace; + +/// +/// A data model for the response from the model endpoint of the Hugging Face router. +/// +/// +/// The router says more about a model than the OpenAI model list does: which inference providers +/// serve it, and which kinds of input it takes. That is why this provider brings its own data model +/// instead of using the shared one. +/// +/// The models the router knows. +public readonly record struct ModelsResponse(IList Data); + +/// +/// One model as the Hugging Face router describes it. +/// +/// The ID of the model, written as "org/model". +/// The inference providers serving this model. +public readonly record struct HFModel(string Id, IList? Providers); + +/// +/// One inference provider serving a model. +/// +/// The slug of the inference provider, e.g. "novita". +/// Whether the provider currently serves the model. Known value: "live". +public readonly record struct HFModelProvider(string Provider, string Status); + +/// +/// One model as the Hugging Face hub lists it. +/// +/// +/// The hub answers with a plain array of models and describes each of them in far more detail than +/// we need here, from tags to download counts. We only ever ask for the models of one provider and +/// one task, so the ID is all that is left to read. +/// +/// The ID of the model, written as "org/model". +public readonly record struct HubModel(string Id); \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs b/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs index 1c20c646..f954e341 100644 --- a/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs +++ b/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs @@ -1,8 +1,10 @@ -using System.Runtime.CompilerServices; +using System.Net; +using System.Runtime.CompilerServices; using AIStudio.Chat; using AIStudio.Provider.OpenAI; using AIStudio.Settings; +using AIStudio.Tools.PluginSystem; namespace AIStudio.Provider.HuggingFace; @@ -10,11 +12,138 @@ public sealed class ProviderHuggingFace : BaseProvider { private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(); - public ProviderHuggingFace(HFInferenceProvider hfProvider, Model model) : base(LLMProviders.HUGGINGFACE, new Uri($"https://router.huggingface.co/{hfProvider.Endpoints(model)}"), ExternalHttpTrustPolicy.SYSTEM_TRUST_ONLY, LOGGER) + private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ProviderHuggingFace).Namespace, nameof(ProviderHuggingFace)); + + /// + /// The OpenAI-compatible endpoint which serves every inference provider. + /// + /// + /// Hugging Face also keeps a route per provider, such as "/novita/v3/openai/". Those expect the + /// model ID as that provider spells it, which differs from the ID on the hub: Novita knows + /// "google/gemma-4-31B-it" as "google/gemma-4-31b-it", and the router is case-sensitive. Asking + /// for the hub spelling there is answered with "Model not supported by provider novita". This + /// endpoint takes the hub spelling and translates it for us, so it is the one we use. + /// + private const string ROUTER_BASE_URL = "https://router.huggingface.co/v1/"; + + /// + /// Where the models of an inference provider are listed. + /// + /// + /// The router lists the chat models it routes, but nothing else. Which embedding models a + /// provider offers is known to the hub alone, which answers this without a token. The URL is + /// absolute on purpose: it addresses the hub, not the router this provider is built on. + /// + private const string HUB_MODELS_URL = "https://huggingface.co/api/models?limit=100&sort=downloads&direction=-1&inference_provider="; + + private readonly HFInferenceProvider hfProvider; + + public ProviderHuggingFace(HFInferenceProvider hfProvider, HFEndpointKind endpointKind = HFEndpointKind.CHAT) : base(LLMProviders.HUGGINGFACE, new Uri(BuildBaseURL(hfProvider, endpointKind)), ExternalHttpTrustPolicy.SYSTEM_TRUST_ONLY, LOGGER) { - LOGGER.LogInformation($"We use the inference provider '{hfProvider}'. Thus we use the base URL 'https://router.huggingface.co/{hfProvider.Endpoints(model)}'."); + this.hfProvider = hfProvider; + LOGGER.LogInformation($"We use the inference provider '{hfProvider}' for {endpointKind}. Thus, we use the base URL '{BuildBaseURL(hfProvider, endpointKind)}'."); } + /// + /// Determines the base URL for the endpoint this provider instance talks to. + /// + /// + /// A provider which serves no embeddings has no route of its own to offer, and neither have the + /// routing strategies. We still have to hand a URL to the base class, so we fall back to the + /// router. A request sent there is answered with a plain "Not Found", which is the honest + /// outcome: the user selected something we told them we cannot do, and the validation of the + /// dialog says so before it ever comes to a request. + /// + /// The chosen inference provider. + /// Which endpoint this instance is built for. + /// The base URL to use. + private static string BuildBaseURL(HFInferenceProvider hfProvider, HFEndpointKind endpointKind) + { + if (endpointKind is HFEndpointKind.CHAT) + return ROUTER_BASE_URL; + + var providerBaseURL = hfProvider.ProviderBaseURL(); + return string.IsNullOrEmpty(providerBaseURL) ? ROUTER_BASE_URL : providerBaseURL; + } + + /// + /// Builds the model name to send to the router. + /// + /// + /// The router picks the inference provider from a suffix on the model name. When the user wrote + /// a suffix themselves, we keep theirs: appending a second one would name a model nobody knows. + /// + /// The model the user chose. + /// The model name including the provider suffix, when one applies. + private string BuildModelIdentifier(Model model) + { + var modelId = model.Id; + if (string.IsNullOrWhiteSpace(modelId) || modelId.Contains(':')) + return modelId; + + return $"{modelId}{this.hfProvider.ModelSuffix()}"; + } + + /// + /// Recognizes the router's answer for a model the chosen inference provider does not serve. + /// + /// + /// Not every model is available at every inference provider, and the router says so with a bad + /// request. Without this, the user would be told that the message format might have changed, + /// which points them at something they cannot fix and away from the one thing they can: picking + /// another provider. The router words this failure as the error code "model_not_supported", + /// while the providers behind it word it as a sentence of their own. + /// + /// A piece of the failed response: an error code, a message, or the body. + /// True, when this text names an unsupported model. + private static bool IsModelNotSupportedError(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return false; + + return value.Contains("model_not_supported", StringComparison.OrdinalIgnoreCase) || + value.Contains("not supported by provider", StringComparison.OrdinalIgnoreCase) || + value.Contains("not supported by any provider", StringComparison.OrdinalIgnoreCase); + } + + #region Overrides of BaseProvider + + /// + protected override ProviderRequestFailureReason ClassifyProviderRequestFailure(HttpStatusCode statusCode, string responseBody) + { + if (statusCode is HttpStatusCode.BadRequest && IsModelNotSupportedError(responseBody)) + return ProviderRequestFailureReason.MODEL_NOT_SUPPORTED_BY_PROVIDER; + + return base.ClassifyProviderRequestFailure(statusCode, responseBody); + } + + /// + protected override ProviderRequestFailureReason ClassifyProviderRequestFailure(string? errorCode, string? errorType, string? errorMessage, string responseBody) + { + if (IsModelNotSupportedError(errorCode) || IsModelNotSupportedError(errorType) || IsModelNotSupportedError(errorMessage)) + return ProviderRequestFailureReason.MODEL_NOT_SUPPORTED_BY_PROVIDER; + + return base.ClassifyProviderRequestFailure(errorCode, errorType, errorMessage, responseBody); + } + + /// + protected override string GetProviderRequestFailureUserMessage(ProviderRequestFailureReason failureReason) + { + if (failureReason is not ProviderRequestFailureReason.MODEL_NOT_SUPPORTED_BY_PROVIDER) + return base.GetProviderRequestFailureUserMessage(failureReason); + + // + // When Hugging Face chose the provider itself, naming it back to the user would help + // nobody: they never picked it, and no other choice of provider is left to try: + // + if (this.hfProvider is HFInferenceProvider.NONE or HFInferenceProvider.AUTOMATIC or HFInferenceProvider.CHEAPEST or HFInferenceProvider.PREFERRED) + return TB("No Hugging Face inference provider offers the selected model. Please check the model name and whether it is still available on Hugging Face."); + + return string.Format(TB("The Hugging Face inference provider '{0}' does not offer the selected model. Please select another inference provider, or let Hugging Face choose one for you."), this.hfProvider.ToName()); + } + + #endregion + #region Implementation of IProvider /// @@ -24,7 +153,7 @@ public sealed class ProviderHuggingFace : BaseProvider public override string InstanceName { get; set; } = "HuggingFace"; /// - public override bool HasModelLoadingCapability => false; + public override bool HasModelLoadingCapability => true; /// public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) @@ -41,7 +170,7 @@ public sealed class ProviderHuggingFace : BaseProvider return new ChatCompletionAPIRequest { - Model = chatModel.Id, + Model = this.BuildModelIdentifier(chatModel), // Build the messages: // - First of all the system prompt @@ -65,21 +194,66 @@ public sealed class ProviderHuggingFace : BaseProvider #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously /// - public override Task TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default) + public override async Task TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default) { - return Task.FromResult(TranscriptionResult.Failure()); + var requestedSecret = await Program.RUST_SERVICE.GetAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER); + + // + // Note that we send the model as it is: this request goes to the provider's own route, + // where a routing suffix would be part of the name and name nothing: + // + return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, token: token); } /// - public override Task>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) + public override async Task>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) { - return Task.FromResult>>([]); + var requestedSecret = await Program.RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER); + + // + // Note that we send the model as it is: this request goes to the provider's own route, + // where a routing suffix would be part of the name and name nothing: + // + return await this.PerformStandardTextEmbeddingRequest(requestedSecret, embeddingModel, token: token, texts: texts); } /// public override Task GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) { - return Task.FromResult(ModelLoadResult.FromModels([])); + return this.LoadModelsResponse(SecretStoreType.LLM_PROVIDER, "models", this.SelectChatModels, token, apiKeyProvisional); + } + + /// + /// Picks the models the user may chat with through the chosen inference provider. + /// + /// + /// The router reports every model it knows, together with the providers serving it. When the + /// user named a provider, we show what that provider offers and nothing else. Showing more + /// would be a disservice: every model outside that list is answered with a bad request, and the + /// user would only learn about it once they try to chat. + /// + /// The response of the model endpoint. + /// The models to offer. + private IEnumerable SelectChatModels(ModelsResponse response) + { + var chatModels = response.Data.Where(hfModel => new Model(hfModel.Id, null).IsChatModel()); + var providerSlug = this.hfProvider.EndpointsId(); + if (string.IsNullOrEmpty(providerSlug)) + return ToModels(chatModels); + + return ToModels(chatModels.Where(hfModel => IsServedBy(hfModel, providerSlug))); + } + + private static IEnumerable ToModels(IEnumerable hfModels) => hfModels.Select(hfModel => new Model(hfModel.Id, null)); + + private static bool IsServedBy(HFModel hfModel, string providerSlug) + { + if (hfModel.Providers is null) + return false; + + return hfModel.Providers.Any(provider => + string.Equals(provider.Provider, providerSlug, StringComparison.OrdinalIgnoreCase) && + string.Equals(provider.Status, "live", StringComparison.OrdinalIgnoreCase)); } /// @@ -91,14 +265,34 @@ public sealed class ProviderHuggingFace : BaseProvider /// public override Task GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default) { - return Task.FromResult(ModelLoadResult.FromModels([])); + if (!this.hfProvider.SupportsEmbeddings()) + return Task.FromResult(ModelLoadResult.FromModels([])); + + return this.LoadHubModels(SecretStoreType.EMBEDDING_PROVIDER, "feature-extraction", apiKeyProvisional, token); + } + + /// + /// Loads the models one inference provider offers for a task, as the hub lists them. + /// + /// Which stored API key to use. + /// The task to ask for, as the hub names it. + /// An API key which is not stored yet. + /// The cancellation token to use. + /// The models of that provider for that task. + private Task LoadHubModels(SecretStoreType storeType, string pipelineTag, string? apiKeyProvisional, CancellationToken token) + { + var requestURL = $"{HUB_MODELS_URL}{this.hfProvider.EndpointsId()}&pipeline_tag={pipelineTag}"; + return this.LoadModelsResponse>(storeType, requestURL, hubModels => hubModels.Select(hubModel => new Model(hubModel.Id, null)), token, apiKeyProvisional); } /// public override Task GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default) { - return Task.FromResult(ModelLoadResult.FromModels([])); + if (!this.hfProvider.SupportsTranscription()) + return Task.FromResult(ModelLoadResult.FromModels([])); + + return this.LoadHubModels(SecretStoreType.TRANSCRIPTION_PROVIDER, "automatic-speech-recognition", apiKeyProvisional, token); } #endregion -} +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs b/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs index 596a06f6..546845c6 100644 --- a/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs +++ b/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs @@ -206,10 +206,17 @@ public static class LLMProvidersExtensions LLMProviders.FIREWORKS => false, LLMProviders.X => false, LLMProviders.DEEP_SEEK => false, - LLMProviders.HUGGINGFACE => false, LLMProviders.PERPLEXITY => false, LLMProviders.HETZNER => false, + // + // Hugging Face serves embeddings, but not through the router endpoint we chat with: that + // one answers "/v1/embeddings" with a plain "Not Found". They have to be asked of one + // inference provider directly, and only some of them answer the OpenAI-compatible form. + // Which ones is decided by HFInferenceProviderExtensions.SupportsEmbeddings. + // + LLMProviders.HUGGINGFACE => true, + // // Self-hosted providers are treated as a special case anyway. // @@ -246,9 +253,16 @@ public static class LLMProvidersExtensions LLMProviders.ANTHROPIC => false, LLMProviders.X => false, LLMProviders.DEEP_SEEK => false, - LLMProviders.HUGGINGFACE => false, LLMProviders.PERPLEXITY => false, + // + // Hugging Face transcribes audio, but like embeddings, not through the router endpoint we + // chat with: that one answers "/v1/audio/transcriptions" with a plain "Not Found". Only + // some of the inference providers answer the OpenAI-compatible form, which + // HFInferenceProviderExtensions.SupportsTranscription decides. + // + LLMProviders.HUGGINGFACE => true, + // // Self-hosted providers are treated as a special case anyway. // @@ -264,7 +278,7 @@ public static class LLMProvidersExtensions /// The provider instance. public static IProvider CreateProvider(this AIStudio.Settings.Provider providerSettings) { - return providerSettings.UsedLLMProvider.CreateProvider(providerSettings.InstanceName, providerSettings.Host, providerSettings.Hostname, providerSettings.Model, providerSettings.HFInferenceProvider, providerSettings.Id, providerSettings.AdditionalJsonApiParameters, providerSettings.IsEnterpriseConfiguration); + return providerSettings.UsedLLMProvider.CreateProvider(providerSettings.InstanceName, providerSettings.Host, providerSettings.Hostname, providerSettings.HFInferenceProvider, providerSettings.Id, providerSettings.AdditionalJsonApiParameters, providerSettings.IsEnterpriseConfiguration); } /// @@ -274,7 +288,7 @@ public static class LLMProvidersExtensions /// The provider instance. public static IProvider CreateProvider(this EmbeddingProvider embeddingProviderSettings) { - return embeddingProviderSettings.UsedLLMProvider.CreateProvider(embeddingProviderSettings.Name, embeddingProviderSettings.Host, embeddingProviderSettings.Hostname, embeddingProviderSettings.Model, HFInferenceProvider.NONE, configuredProviderId: embeddingProviderSettings.Id, isEnterpriseConfiguration: embeddingProviderSettings.IsEnterpriseConfiguration); + return embeddingProviderSettings.UsedLLMProvider.CreateProvider(embeddingProviderSettings.Name, embeddingProviderSettings.Host, embeddingProviderSettings.Hostname, embeddingProviderSettings.HFInferenceProvider, configuredProviderId: embeddingProviderSettings.Id, isEnterpriseConfiguration: embeddingProviderSettings.IsEnterpriseConfiguration, hfEndpointKind: HFEndpointKind.EMBEDDING); } /// @@ -284,10 +298,10 @@ public static class LLMProvidersExtensions /// The provider instance. public static IProvider CreateProvider(this TranscriptionProvider transcriptionProviderSettings) { - return transcriptionProviderSettings.UsedLLMProvider.CreateProvider(transcriptionProviderSettings.Name, transcriptionProviderSettings.Host, transcriptionProviderSettings.Hostname, transcriptionProviderSettings.Model, HFInferenceProvider.NONE, configuredProviderId: transcriptionProviderSettings.Id, isEnterpriseConfiguration: transcriptionProviderSettings.IsEnterpriseConfiguration); + return transcriptionProviderSettings.UsedLLMProvider.CreateProvider(transcriptionProviderSettings.Name, transcriptionProviderSettings.Host, transcriptionProviderSettings.Hostname, transcriptionProviderSettings.HFInferenceProvider, configuredProviderId: transcriptionProviderSettings.Id, isEnterpriseConfiguration: transcriptionProviderSettings.IsEnterpriseConfiguration, hfEndpointKind: HFEndpointKind.TRANSCRIPTION); } - private static IProvider CreateProvider(this LLMProviders provider, string instanceName, Host host, string hostname, Model model, HFInferenceProvider inferenceProvider, string configuredProviderId = "", string expertProviderApiParameter = "", bool isEnterpriseConfiguration = false) + private static IProvider CreateProvider(this LLMProviders provider, string instanceName, Host host, string hostname, HFInferenceProvider inferenceProvider, string configuredProviderId = "", string expertProviderApiParameter = "", bool isEnterpriseConfiguration = false, HFEndpointKind hfEndpointKind = HFEndpointKind.CHAT) { try { @@ -308,7 +322,7 @@ public static class LLMProvidersExtensions LLMProviders.GROQ => new ProviderGroq { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, LLMProviders.FIREWORKS => new ProviderFireworks { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.HUGGINGFACE => new ProviderHuggingFace(inferenceProvider, model) { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.HUGGINGFACE => new ProviderHuggingFace(inferenceProvider, hfEndpointKind) { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, LLMProviders.SELF_HOSTED => new ProviderSelfHosted(host, hostname) { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, @@ -392,14 +406,13 @@ public static class LLMProvidersExtensions public static string GetModelsOverviewURL(this LLMProviders provider, HFInferenceProvider inferenceProvider) => provider switch { LLMProviders.FIREWORKS => "https://fireworks.ai/models?show=Serverless", - LLMProviders.HUGGINGFACE => $"https://huggingface.co/models?inference_provider={inferenceProvider.EndpointsId()}", + LLMProviders.HUGGINGFACE => $"https://huggingface.co/models?inference_provider={inferenceProvider.CatalogFilter()}", _ => string.Empty, }; public static bool IsLLMModelProvidedManually(this LLMProviders provider) => provider switch { LLMProviders.FIREWORKS => true, - LLMProviders.HUGGINGFACE => true, _ => false, }; diff --git a/app/MindWork AI Studio/Provider/ModelKindExtensions.cs b/app/MindWork AI Studio/Provider/ModelKindExtensions.cs index 997e81ef..c17000aa 100644 --- a/app/MindWork AI Studio/Provider/ModelKindExtensions.cs +++ b/app/MindWork AI Studio/Provider/ModelKindExtensions.cs @@ -46,7 +46,16 @@ public static class ModelKindExtensions private static readonly string[] IMAGE_GENERATION_MARKERS = ["flux", "stable-diffusion", "sdxl", "dall-e", "midjourney", "gpt-image"]; - private static readonly string[] VIDEO_GENERATION_MARKERS = ["sora", "veo-", "kling", "runway"]; + private static readonly string[] VIDEO_GENERATION_MARKERS = ["sora", "veo-", "runway"]; + + // + // Markers which have to stand as a word of their own. "kling" is such a case: taken as a plain + // substring, it also matches the organization "Klingspor", the model "Inkling", and the + // fine-tune "Llama-2-7b-chat-klingon" -- all of them models to chat with, which would vanish + // from the user's list. The video models themselves are named "kling-v1" or "kling-video", + // where the name ends at a separator. + // + private static readonly string[] VIDEO_GENERATION_WORD_MARKERS = ["kling"]; // // Voxtral is marketed as an audio model which understands speech, so one could expect it to work @@ -101,7 +110,7 @@ public static class ModelKindExtensions if (HasAnyMarker(model.Id, IMAGE_GENERATION_MARKERS)) return ModelKind.IMAGE_GENERATION; - if (HasAnyMarker(model.Id, VIDEO_GENERATION_MARKERS)) + if (HasAnyMarker(model.Id, VIDEO_GENERATION_MARKERS) || HasAnyWordMarker(model.Id, VIDEO_GENERATION_WORD_MARKERS)) return ModelKind.VIDEO_GENERATION; if (HasAnyMarker(model.Id, REALTIME_MARKERS)) @@ -158,4 +167,53 @@ public static class ModelKindExtensions return false; } + + /// + /// Checks whether the model name contains one of the markers as a word of its own. + /// + /// + /// A short marker which is also a common syllable cannot be looked for as a plain substring: + /// it would match names which have nothing to do with it, and the model would be sorted into + /// the wrong kind. Such a marker counts only where a name segment begins and ends with it. + /// + /// The ID of the model. + /// The markers to look for. + /// True, when one of the markers stands as a word of its own. + private static bool HasAnyWordMarker(string modelId, string[] markers) + { + foreach (var marker in markers) + { + var searchIndex = 0; + while (searchIndex <= modelId.Length - marker.Length) + { + var markerIndex = modelId.IndexOf(marker, searchIndex, StringComparison.OrdinalIgnoreCase); + if (markerIndex is -1) + break; + + if (IsWholeWord(modelId, marker, markerIndex)) + return true; + + // The same marker may appear again later in the name, so we keep looking: + searchIndex = markerIndex + 1; + } + } + + return false; + } + + private static bool IsWholeWord(string modelId, string marker, int markerIndex) + { + if (markerIndex > 0 && !IsSeparator(modelId[markerIndex - 1])) + return false; + + var endIndex = markerIndex + marker.Length; + return endIndex >= modelId.Length || IsSeparator(modelId[endIndex]); + } + + /// + /// The characters which separate the parts of a model name, such as in "fal-ai/kling-video". + /// + /// The character to check. + /// True, when the character separates two parts of a name. + private static bool IsSeparator(char character) => character is '/' or '-' or '_' or '.' or ' ' or ':'; } \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/ProviderRequestFailureReason.cs b/app/MindWork AI Studio/Provider/ProviderRequestFailureReason.cs index c56fcc4f..4c5a33b4 100644 --- a/app/MindWork AI Studio/Provider/ProviderRequestFailureReason.cs +++ b/app/MindWork AI Studio/Provider/ProviderRequestFailureReason.cs @@ -5,4 +5,13 @@ public enum ProviderRequestFailureReason NONE, INSUFFICIENT_QUOTA, TOO_MANY_REQUESTS, + + /// + /// The provider does not serve the requested model. + /// + /// + /// This applies to gateways which route to other providers: the model exists, but the one + /// meant to answer for it does not offer it. + /// + MODEL_NOT_SUPPORTED_BY_PROVIDER, } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/EmbeddingProvider.cs b/app/MindWork AI Studio/Settings/EmbeddingProvider.cs index 61324df3..64454e27 100644 --- a/app/MindWork AI Studio/Settings/EmbeddingProvider.cs +++ b/app/MindWork AI Studio/Settings/EmbeddingProvider.cs @@ -1,6 +1,7 @@ using System.Text.Json.Serialization; using AIStudio.Provider; +using AIStudio.Provider.HuggingFace; using AIStudio.Tools.PluginSystem; using SharedTools; @@ -22,7 +23,8 @@ public sealed record EmbeddingProvider( string Hostname = "http://localhost:1234", Host Host = Host.NONE, bool AllowUserProvidedAPIKey = false, - string CustomIconDataUrl = "") : ConfigurationBaseObject, ISecretId, IUserProvidedAPIKey + string CustomIconDataUrl = "", + HFInferenceProvider HFInferenceProvider = HFInferenceProvider.NONE) : ConfigurationBaseObject, ISecretId, IUserProvidedAPIKey { private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(); @@ -103,6 +105,16 @@ public sealed record EmbeddingProvider( if (table.TryGetValue("AllowUserProvidedAPIKey", out var allowUserProvidedApiKeyValue) && allowUserProvidedApiKeyValue.TryRead(out var allowUserProvidedApiKeyBool)) allowUserProvidedApiKey = allowUserProvidedApiKeyBool; + var hfInferenceProvider = HFInferenceProvider.NONE; + if (table.TryGetValue("HFInferenceProvider", out var hfInferenceProviderValue) && hfInferenceProviderValue.TryRead(out var hfInferenceProviderText)) + { + if (!Enum.TryParse(hfInferenceProviderText, true, out hfInferenceProvider)) + { + LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid Hugging Face inference provider enum value. (Plugin ID: {configPluginId})"); + hfInferenceProvider = HFInferenceProvider.NONE; + } + } + var customIconDataUrl = string.Empty; if (table.TryGetValue("IconPath", out var iconPathValue)) { @@ -126,6 +138,7 @@ public sealed record EmbeddingProvider( Host = host, AllowUserProvidedAPIKey = allowUserProvidedApiKey, CustomIconDataUrl = customIconDataUrl, + HFInferenceProvider = hfInferenceProvider, }; // Handle an encrypted API key if present. When the user manages their own key for this @@ -192,6 +205,14 @@ public sealed record EmbeddingProvider( /// A Lua configuration section string. 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)) { @@ -205,9 +226,10 @@ public sealed record EmbeddingProvider( ["Id"] = "{{Guid.NewGuid().ToString()}}", ["Name"] = "{{LuaTools.EscapeLuaString(this.Name)}}", ["UsedLLMProvider"] = "{{this.UsedLLMProvider}}", - + ["Host"] = "{{this.Host}}", ["Hostname"] = "{{LuaTools.EscapeLuaString(this.Hostname)}}", + {{hfInferenceProviderLine}} {{apiKeyLine}} ["Model"] = { ["Id"] = "{{LuaTools.EscapeLuaString(this.Model.Id)}}", diff --git a/app/MindWork AI Studio/Settings/ProviderExtensions.OpenRouter.cs b/app/MindWork AI Studio/Settings/ProviderExtensions.Gateway.cs similarity index 68% rename from app/MindWork AI Studio/Settings/ProviderExtensions.OpenRouter.cs rename to app/MindWork AI Studio/Settings/ProviderExtensions.Gateway.cs index bb5e4e7a..c1123381 100644 --- a/app/MindWork AI Studio/Settings/ProviderExtensions.OpenRouter.cs +++ b/app/MindWork AI Studio/Settings/ProviderExtensions.Gateway.cs @@ -4,16 +4,26 @@ namespace AIStudio.Settings; public static partial class ProviderExtensions { - private static List GetModelCapabilitiesOpenRouter(Model model) + /// + /// Determines the capabilities of a model offered through a gateway. + /// + /// + /// A gateway serves the models of many other providers rather than models of its own. OpenRouter, + /// LiteLLM, and the Hugging Face router all work that way, and all three name their models the + /// same: "vendor/model-name". + /// + /// The model as the gateway names it. + /// The capabilities of the model when reached through a gateway. + private static List GetModelCapabilitiesGateway(Model model) { // - // OpenRouter model IDs follow the pattern "vendor/model-name". Examples: + // Model IDs follow the pattern "vendor/model-name". Examples: // - openai/gpt-5.6 // - anthropic/claude-opus-5 // - google/gemini-3.7-flash // - qwen/qwen3.8-flash-next // - // OpenRouter offers the models of all the other providers. Instead of keeping a + // A gateway offers the models of all the other providers. Instead of keeping a // second set of rules here, which would always lag behind, we hand the model // over to the provider implementation which already knows it. The vendor prefix // has to be removed first: some of those implementations match the beginning of @@ -41,26 +51,26 @@ public static partial class ProviderExtensions "perplexity" => GetModelCapabilitiesPerplexity(bareModel), // Everything else is open source: Qwen, Llama, GLM, Kimi, Muse, Hunyuan, - // Nemotron, Grok, and whatever OpenRouter adds next. DeepSeek belongs here + // Nemotron, Grok, and whatever a gateway adds next. DeepSeek belongs here // as well: its own implementation covers the aliases of the DeepSeek - // platform, while OpenRouter uses the names of the open weights. + // platform, while the gateways use the names of the open weights. _ => GetModelCapabilitiesOpenSource(bareModel), }; - return NormalizeForOpenRouter(capabilities); + return NormalizeForGateway(capabilities); } /// - /// Adjusts the capabilities reported by another provider for use through OpenRouter. + /// Adjusts the capabilities reported by another provider for use through a gateway. /// /// The capabilities as reported by the provider implementation. - /// The capabilities as they apply when using the model through OpenRouter. + /// The capabilities as they apply when using the model through a gateway. /// - /// OpenRouter serves every model through its OpenAI-compatible chat completion API. + /// A gateway serves every model through its OpenAI-compatible chat completion API. /// The Responses API is not available there, no matter which API the original /// provider offers. /// - private static List NormalizeForOpenRouter(List capabilities) + private static List NormalizeForGateway(List capabilities) { capabilities.Remove(Capability.RESPONSES_API); if(!capabilities.Contains(Capability.CHAT_COMPLETION_API)) diff --git a/app/MindWork AI Studio/Settings/ProviderExtensions.OpenSource.cs b/app/MindWork AI Studio/Settings/ProviderExtensions.OpenSource.cs index b0f9e5a5..37f3eed7 100644 --- a/app/MindWork AI Studio/Settings/ProviderExtensions.OpenSource.cs +++ b/app/MindWork AI Studio/Settings/ProviderExtensions.OpenSource.cs @@ -462,6 +462,84 @@ public static partial class ProviderExtensions Capability.CHAT_COMPLETION_API, ]; + // + // Google Gemma models. Gemma is the open-weights family, while Gemini is not, which is why + // Gemma is handled here and Gemini in the Google implementation. + // + if (modelName.IndexOf("gemma") is not -1) + { + // + // Every checkpoint of the Gemma 4 generation is multimodal and understands video as + // well; there is no text-only variant. Audio input is limited to the E2B, E4B, and 12B + // checkpoints. The models can think, but only when the request asks them to: their chat + // template keeps the thinking channel closed by default. + // + if (modelName.IndexOf("gemma-4") is not -1 || + modelName.IndexOf("gemma4") is not -1 || + modelName.IndexOf("gemma 4") is not -1) + { + if (modelName.IndexOf("e2b") is not -1 || + modelName.IndexOf("e4b") is not -1 || + modelName.IndexOf("12b") is not -1) + return + [ + Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, + Capability.AUDIO_INPUT, Capability.VIDEO_INPUT, + Capability.TEXT_OUTPUT, + + Capability.OPTIONAL_REASONING, Capability.FUNCTION_CALLING, + Capability.CHAT_COMPLETION_API, + ]; + + return + [ + Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, + Capability.VIDEO_INPUT, + Capability.TEXT_OUTPUT, + + Capability.OPTIONAL_REASONING, Capability.FUNCTION_CALLING, + Capability.CHAT_COMPLETION_API, + ]; + } + + // + // Gemma 3 accepts images from the 4B checkpoint upwards; the 1B one is text-only. This + // generation does not reason. The check for the small checkpoint looks for "-1b" rather + // than "1b", so that a name such as gemma-3-31b does not match it. + // + if (modelName.IndexOf("gemma-3") is not -1 || + modelName.IndexOf("gemma3") is not -1 || + modelName.IndexOf("gemma 3") is not -1) + { + if (modelName.IndexOf("-1b") is not -1) + return + [ + Capability.TEXT_INPUT, Capability.TEXT_OUTPUT, + + Capability.FUNCTION_CALLING, + Capability.CHAT_COMPLETION_API, + ]; + + return + [ + Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, + Capability.TEXT_OUTPUT, + + Capability.FUNCTION_CALLING, + Capability.CHAT_COMPLETION_API, + ]; + } + + // + // The earlier generations take text only and were not built for tool usage: + // + return + [ + Capability.TEXT_INPUT, Capability.TEXT_OUTPUT, + Capability.CHAT_COMPLETION_API, + ]; + } + // // Z AI / GLM models: // diff --git a/app/MindWork AI Studio/Settings/ProviderExtensions.cs b/app/MindWork AI Studio/Settings/ProviderExtensions.cs index 513235fd..c1f6502a 100644 --- a/app/MindWork AI Studio/Settings/ProviderExtensions.cs +++ b/app/MindWork AI Studio/Settings/ProviderExtensions.cs @@ -1,4 +1,5 @@ using AIStudio.Provider; +using AIStudio.Provider.HuggingFace; namespace AIStudio.Settings; @@ -53,21 +54,27 @@ public static partial class ProviderExtensions LLMProviders.DEEP_SEEK => GetModelCapabilitiesDeepSeek(model), LLMProviders.ALIBABA_CLOUD => GetModelCapabilitiesAlibaba(model), LLMProviders.PERPLEXITY => GetModelCapabilitiesPerplexity(model), - LLMProviders.OPEN_ROUTER => GetModelCapabilitiesOpenRouter(model), + LLMProviders.OPEN_ROUTER => GetModelCapabilitiesGateway(model), LLMProviders.HETZNER or LLMProviders.IONOS => GetModelCapabilitiesOpenSource(model), // // LiteLLM is a gateway just like OpenRouter, and it names its models the same way: // "vendor/model", e.g. "anthropic/claude-opus-5" or "azure/gpt-5.6". So we let the - // OpenRouter detection handle it, which resolves the vendor prefix and asks the + // gateway detection handle it, which resolves the vendor prefix and asks the // provider who really knows the model. Everything it cannot place is treated as // an open source model, which is the right fallback for a freely named alias: // - LLMProviders.LITE_LLM => GetModelCapabilitiesOpenRouter(model), + LLMProviders.LITE_LLM => GetModelCapabilitiesGateway(model), - LLMProviders.GROQ => GetModelCapabilitiesOpenSource(model), - LLMProviders.FIREWORKS => GetModelCapabilitiesOpenSource(model), - LLMProviders.HUGGINGFACE => GetModelCapabilitiesOpenSource(model), + LLMProviders.GROQ or LLMProviders.FIREWORKS => GetModelCapabilitiesOpenSource(model), + + // + // Hugging Face names its models the way the hub does, "org/model", which is the same + // shape the other gateways use. So we let the gateway detection resolve the organization + // and ask the provider implementation which really knows the model. The routing suffix + // has to go first: it says which inference provider answers, not what the model is. + // + LLMProviders.HUGGINGFACE => GetModelCapabilitiesGateway(model.WithoutRoutingSuffix()), LLMProviders.HELMHOLTZ => GetModelCapabilitiesOpenSource(model), LLMProviders.GWDG => GetModelCapabilitiesOpenSource(model), diff --git a/app/MindWork AI Studio/Settings/TranscriptionProvider.cs b/app/MindWork AI Studio/Settings/TranscriptionProvider.cs index 7ecc7938..7a15f24a 100644 --- a/app/MindWork AI Studio/Settings/TranscriptionProvider.cs +++ b/app/MindWork AI Studio/Settings/TranscriptionProvider.cs @@ -1,6 +1,7 @@ using System.Text.Json.Serialization; using AIStudio.Provider; +using AIStudio.Provider.HuggingFace; using AIStudio.Tools.PluginSystem; using SharedTools; @@ -22,7 +23,8 @@ public sealed record TranscriptionProvider( string Hostname = "http://localhost:1234", Host Host = Host.NONE, bool AllowUserProvidedAPIKey = false, - string CustomIconDataUrl = "") : ConfigurationBaseObject, ISecretId, IUserProvidedAPIKey + string CustomIconDataUrl = "", + HFInferenceProvider HFInferenceProvider = HFInferenceProvider.NONE) : ConfigurationBaseObject, ISecretId, IUserProvidedAPIKey { private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(); @@ -103,6 +105,16 @@ public sealed record TranscriptionProvider( if (table.TryGetValue("AllowUserProvidedAPIKey", out var allowUserProvidedApiKeyValue) && allowUserProvidedApiKeyValue.TryRead(out var allowUserProvidedApiKeyBool)) allowUserProvidedApiKey = allowUserProvidedApiKeyBool; + var hfInferenceProvider = HFInferenceProvider.NONE; + if (table.TryGetValue("HFInferenceProvider", out var hfInferenceProviderValue) && hfInferenceProviderValue.TryRead(out var hfInferenceProviderText)) + { + if (!Enum.TryParse(hfInferenceProviderText, true, out hfInferenceProvider)) + { + LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid Hugging Face inference provider enum value. (Plugin ID: {configPluginId})"); + hfInferenceProvider = HFInferenceProvider.NONE; + } + } + var customIconDataUrl = string.Empty; if (table.TryGetValue("IconPath", out var iconPathValue)) { @@ -126,6 +138,7 @@ public sealed record TranscriptionProvider( Host = host, AllowUserProvidedAPIKey = allowUserProvidedApiKey, CustomIconDataUrl = customIconDataUrl, + HFInferenceProvider = hfInferenceProvider, }; // Handle an encrypted API key if present. When the user manages their own key for this @@ -192,6 +205,14 @@ public sealed record TranscriptionProvider( /// A Lua configuration section string. 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)) { @@ -208,6 +229,7 @@ public sealed record TranscriptionProvider( ["Host"] = "{{this.Host}}", ["Hostname"] = "{{LuaTools.EscapeLuaString(this.Hostname)}}", + {{hfInferenceProviderLine}} {{apiKeyLine}} ["Model"] = { ["Id"] = "{{LuaTools.EscapeLuaString(this.Model.Id)}}", diff --git a/app/MindWork AI Studio/Tools/Validation/ProviderValidation.cs b/app/MindWork AI Studio/Tools/Validation/ProviderValidation.cs index 4a140614..ae0d5c9d 100644 --- a/app/MindWork AI Studio/Tools/Validation/ProviderValidation.cs +++ b/app/MindWork AI Studio/Tools/Validation/ProviderValidation.cs @@ -126,9 +126,57 @@ public sealed class ProviderValidation if(this.GetProvider() is not LLMProviders.HUGGINGFACE) return null; - if (inferenceProvider is HFInferenceProvider.NONE) + if (!inferenceProvider.SupportsChat()) return TB("Please select an Hugging Face inference provider."); return null; } + + /// + /// Validates the Hugging Face inference provider chosen for embeddings. + /// + /// + /// Far fewer providers create embeddings for us than serve chat models, so a selection which is + /// fine for chatting may not be for embeddings. A provider configured before the choice narrowed + /// is no longer among the options, which would leave the user with an empty field and no reason + /// given. + /// + /// The inference provider to validate. + /// The message to show, or null when the selection is fine. + public string? ValidatingHFInstanceProviderForEmbeddings(HFInferenceProvider inferenceProvider) + { + if(this.GetProvider() is not LLMProviders.HUGGINGFACE) + return null; + + if (inferenceProvider is HFInferenceProvider.NONE) + return TB("Please select an Hugging Face inference provider."); + + if (!inferenceProvider.SupportsEmbeddings()) + return TB("This Hugging Face inference provider does not create embeddings. Please select another one."); + + return null; + } + + /// + /// Validates the Hugging Face inference provider chosen for transcription. + /// + /// + /// As with embeddings, only some of the inference providers transcribe audio for us, so the + /// choice is narrower than it is for chatting. + /// + /// The inference provider to validate. + /// The message to show, or null when the selection is fine. + public string? ValidatingHFInstanceProviderForTranscription(HFInferenceProvider inferenceProvider) + { + if(this.GetProvider() is not LLMProviders.HUGGINGFACE) + return null; + + if (inferenceProvider is HFInferenceProvider.NONE) + return TB("Please select an Hugging Face inference provider."); + + if (!inferenceProvider.SupportsTranscription()) + return TB("This Hugging Face inference provider does not transcribe audio. Please select another one."); + + return null; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md b/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md index 952d3080..fcda497a 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md @@ -7,6 +7,8 @@ - Added the IONOS AI Model Hub as a provider for chats and embeddings. It runs open-source models in Germany, is subject to the GDPR, and IONOS states that your data is not used for training. - Added LiteLLM as a new LLM provider for chats, embeddings, and speech-to-text. LiteLLM is an AI gateway you run yourself in front of models from many providers. Because your gateway decides where your data goes, you set its trust level yourself. Thanks Prodman Devokadev (`prodmanpd`) for this first contribution. - Added speech-to-text for Helmholtz Blablador and GroqCloud, and embeddings for GWDG SAIA. These providers offer these services now, so you can select them when you dictate a message or when you set up a data source. +- Added embeddings and speech-to-text for Hugging Face, so you can now use it to prepare your own documents for retrieval and to dictate your messages. Hugging Face offers both through a few of its inference providers only, which is why you get a shorter list to choose from there than you do for chatting. +- Added a model list for Hugging Face. Until now you had to type the name of the model yourself and hope you got it right, down to its capitalization. AI Studio now loads the models your chosen inference provider actually offers, so you pick one from a list and cannot end up with a model that provider does not serve. - Improved the safety of plugin symbols: AI Studio now shows the symbol of a plugin in isolation, so nothing inside a symbol can reach the rest of the app. - Improved how much memory AI Studio needs. Working with large documents used to grow the app to several gigabytes, and on macOS that memory was never handed back. AI Studio now stays at a fraction of that and returns memory to your system. This matters most on devices with little memory, such as a Raspberry Pi. - Improved the preview for large documents. It now shows you the beginning of your document instead of loading all of it, so the dialog opens right away. Your complete document still goes to the AI. @@ -15,6 +17,8 @@ - Improved which models you get to choose from when chatting: models you cannot chat with are now hidden. This is most noticeable with a gateway such as LiteLLM, which offers you everything its providers have, including video, live speech, and audio models. - Changed the model list of GroqCloud. Models which cannot be used for chatting, such as the speech and the safety models, no longer show up among the chat models. The speech models now appear where they belong, in the settings for speech-to-text. - Changed how plugins your organization rolled out are protected. They can no longer be deleted or edited in AI Studio, which already applied to sharing and replacing them. This also covers plugins staged for a test: such a test now ends by restarting AI Studio or by removing the staged files, instead of through the plugin page. +- Fixed the Hugging Face provider, which had stopped working. Hugging Face changed the way requests are addressed, and AI Studio still used the old way, so chatting failed with a puzzling error about the message format. Chatting works again, and you can now reach far more inference providers, among them Z.ai, Groq, Cohere, DeepInfra, and Baseten. You may also leave the choice to Hugging Face and let it pick the fastest or the cheapest provider for you, which switches to another one when your first choice is unavailable. Should a provider not offer the model you selected, AI Studio now tells you so in plain words instead of reporting a technical problem. The providers Hugging Face no longer runs are gone from the list; if you had picked one of them, AI Studio asks you to choose again. +- Fixed the abilities shown for Google's Gemma models. AI Studio did not recognize them at all and treated every one of them as a text-only model, so images, reasoning, and tool usage stayed hidden even though Gemma 4 handles all three. - Fixed assistants created by the Assistant Builder being named after an internal placeholder, such as "Model decides", when you left the display name empty. The model now picks a fitting name instead. - Fixed AI Studio reading a document to the end even after you closed its preview. Closing the dialog now stops that work immediately. - Fixed AI Studio holding on to finished chats, presentation images, and plugin data. It releases them now, so memory no longer grows the longer you keep the app running.