From d2f36febc83dae7bb9a9cfd80b80f139bfc55ad3 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 30 Aug 2026 14:57:37 +0200 Subject: [PATCH 01/10] Switch Hugging Face provider to the unified inference router --- .../Assistants/I18N/allTexts.lua | 12 +++ .../Dialogs/ProviderDialog.razor | 15 +-- .../Plugins/configuration/plugin.lua | 10 +- .../plugin.lua | 12 +++ .../plugin.lua | 12 +++ .../HuggingFace/HFInferenceProvider.cs | 41 ++++++-- .../HFInferenceProviderExtensions.cs | 97 ++++++++++++++----- .../HuggingFace/ProviderHuggingFace.cs | 39 +++++++- .../Provider/LLMProvidersExtensions.cs | 12 +-- 9 files changed, 202 insertions(+), 48 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index d0bdf8c0a..a88361640 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -6361,6 +6361,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." @@ -9274,6 +9277,15 @@ 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" + -- Self-hosted UI_TEXT_CONTENT["AISTUDIO::PROVIDER::LLMPROVIDERSEXTENSIONS::T146444217"] = "Self-hosted" diff --git a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor index eb97b2518..aaa891c66 100644 --- a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor +++ b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor @@ -72,16 +72,17 @@ @foreach (HFInferenceProvider inferenceProvider in Enum.GetValues(typeof(HFInferenceProvider))) { - - @inferenceProvider.ToName() - - } + @if (inferenceProvider is not HFInferenceProvider.NONE) + { + + @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) diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua index 747ae2efa..4ca2d7ec8 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, +-- -- HF_INFERENCE_API, 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. 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. 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 f93095be7..b508f517e 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 @@ -6363,6 +6363,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." @@ -9276,6 +9279,15 @@ 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" + -- Self-hosted UI_TEXT_CONTENT["AISTUDIO::PROVIDER::LLMPROVIDERSEXTENSIONS::T146444217"] = "Selbst gehostet" 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 ff2afa5f5..23d72267a 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 @@ -6363,6 +6363,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." @@ -9276,6 +9279,15 @@ 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" + -- Self-hosted UI_TEXT_CONTENT["AISTUDIO::PROVIDER::LLMPROVIDERSEXTENSIONS::T146444217"] = "Self-hosted" diff --git a/app/MindWork AI Studio/Provider/HuggingFace/HFInferenceProvider.cs b/app/MindWork AI Studio/Provider/HuggingFace/HFInferenceProvider.cs index 01b722eba..81195ade8 100644 --- a/app/MindWork AI Studio/Provider/HuggingFace/HFInferenceProvider.cs +++ b/app/MindWork AI Studio/Provider/HuggingFace/HFInferenceProvider.cs @@ -3,16 +3,45 @@ /// /// 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). 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, + GROQ, HF_INFERENCE_API, + 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 0e103938d..fa268d3d2 100644 --- a/app/MindWork AI Studio/Provider/HuggingFace/HFInferenceProviderExtensions.cs +++ b/app/MindWork AI Studio/Provider/HuggingFace/HFInferenceProviderExtensions.cs @@ -1,43 +1,90 @@ -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.HF_INFERENCE_API => "hf-inference", 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()}", + }; + + /// + /// 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.GROQ => "Groq", HFInferenceProvider.HF_INFERENCE_API => "Hugging Face Inference API", + 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/ProviderHuggingFace.cs b/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs index 1c20c6469..3f8aa012e 100644 --- a/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs +++ b/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs @@ -10,9 +10,42 @@ 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) + /// + /// 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/"; + + private readonly HFInferenceProvider hfProvider; + + public ProviderHuggingFace(HFInferenceProvider hfProvider) : base(LLMProviders.HUGGINGFACE, new Uri(ROUTER_BASE_URL), ExternalHttpTrustPolicy.SYSTEM_TRUST_ONLY, LOGGER) + { + this.hfProvider = hfProvider; + LOGGER.LogInformation($"We use the inference provider '{hfProvider}'. Thus, we address the models as '{hfProvider.ModelSuffix()}'."); + } + + /// + /// 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) { - LOGGER.LogInformation($"We use the inference provider '{hfProvider}'. Thus we use the base URL 'https://router.huggingface.co/{hfProvider.Endpoints(model)}'."); + var modelId = model.Id; + if (string.IsNullOrWhiteSpace(modelId) || modelId.Contains(':')) + return modelId; + + return $"{modelId}{this.hfProvider.ModelSuffix()}"; } #region Implementation of IProvider @@ -41,7 +74,7 @@ public override async IAsyncEnumerable StreamChatCompletion( return new ChatCompletionAPIRequest { - Model = chatModel.Id, + Model = this.BuildModelIdentifier(chatModel), // Build the messages: // - First of all the system prompt diff --git a/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs b/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs index 596a06f68..c4006e665 100644 --- a/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs +++ b/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs @@ -264,7 +264,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 +274,7 @@ public static IProvider CreateProvider(this AIStudio.Settings.Provider providerS /// 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, HFInferenceProvider.NONE, configuredProviderId: embeddingProviderSettings.Id, isEnterpriseConfiguration: embeddingProviderSettings.IsEnterpriseConfiguration); } /// @@ -284,10 +284,10 @@ public static IProvider CreateProvider(this EmbeddingProvider embeddingProviderS /// 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, HFInferenceProvider.NONE, configuredProviderId: transcriptionProviderSettings.Id, isEnterpriseConfiguration: transcriptionProviderSettings.IsEnterpriseConfiguration); } - 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) { try { @@ -308,7 +308,7 @@ private static IProvider CreateProvider(this LLMProviders provider, string insta 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) { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, LLMProviders.SELF_HOSTED => new ProviderSelfHosted(host, hostname) { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, @@ -392,7 +392,7 @@ private static IProvider CreateProvider(this LLMProviders provider, string insta 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, }; From 8e6be06df02bcaeb4a28d36204997367c15235ab Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 30 Aug 2026 15:04:40 +0200 Subject: [PATCH 02/10] Fix model capabilities for Hugging Face models --- .../HFInferenceProviderExtensions.cs | 18 +++++ .../Settings/ProviderExtensions.OpenSource.cs | 78 +++++++++++++++++++ .../Settings/ProviderExtensions.cs | 13 +++- 3 files changed, 106 insertions(+), 3 deletions(-) diff --git a/app/MindWork AI Studio/Provider/HuggingFace/HFInferenceProviderExtensions.cs b/app/MindWork AI Studio/Provider/HuggingFace/HFInferenceProviderExtensions.cs index fa268d3d2..a414116ba 100644 --- a/app/MindWork AI Studio/Provider/HuggingFace/HFInferenceProviderExtensions.cs +++ b/app/MindWork AI Studio/Provider/HuggingFace/HFInferenceProviderExtensions.cs @@ -52,6 +52,24 @@ public static class HFInferenceProviderExtensions _ => $":{provider.EndpointsId()}", }; + /// + /// 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. /// diff --git a/app/MindWork AI Studio/Settings/ProviderExtensions.OpenSource.cs b/app/MindWork AI Studio/Settings/ProviderExtensions.OpenSource.cs index b0f9e5a5a..37f3eed7f 100644 --- a/app/MindWork AI Studio/Settings/ProviderExtensions.OpenSource.cs +++ b/app/MindWork AI Studio/Settings/ProviderExtensions.OpenSource.cs @@ -462,6 +462,84 @@ private static List GetModelCapabilitiesOpenSource(Model model) 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 513235fdc..b6666d4e4 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; @@ -65,9 +66,15 @@ public static List GetModelCapabilities(this LLMProviders provider, // LLMProviders.LITE_LLM => GetModelCapabilitiesOpenRouter(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 OpenRouter uses. So we let the OpenRouter 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 => GetModelCapabilitiesOpenRouter(model.WithoutRoutingSuffix()), LLMProviders.HELMHOLTZ => GetModelCapabilitiesOpenSource(model), LLMProviders.GWDG => GetModelCapabilitiesOpenSource(model), From c5c47e256336ee78548df185bcb04a6e4d0f904f Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 30 Aug 2026 15:07:57 +0200 Subject: [PATCH 03/10] Rename OpenRouter capability helpers to gateway helpers --- ...outer.cs => ProviderExtensions.Gateway.cs} | 30 ++++++++++++------- .../Settings/ProviderExtensions.cs | 10 +++---- 2 files changed, 25 insertions(+), 15 deletions(-) rename app/MindWork AI Studio/Settings/{ProviderExtensions.OpenRouter.cs => ProviderExtensions.Gateway.cs} (68%) 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 bb5e4e7a1..c11233818 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 @@ private static List GetModelCapabilitiesOpenRouter(Model model) "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.cs b/app/MindWork AI Studio/Settings/ProviderExtensions.cs index b6666d4e4..c1f6502ac 100644 --- a/app/MindWork AI Studio/Settings/ProviderExtensions.cs +++ b/app/MindWork AI Studio/Settings/ProviderExtensions.cs @@ -54,27 +54,27 @@ public static List GetModelCapabilities(this LLMProviders provider, 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 or LLMProviders.FIREWORKS => GetModelCapabilitiesOpenSource(model), // // Hugging Face names its models the way the hub does, "org/model", which is the same - // shape OpenRouter uses. So we let the OpenRouter detection resolve the organization + // 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 => GetModelCapabilitiesOpenRouter(model.WithoutRoutingSuffix()), + LLMProviders.HUGGINGFACE => GetModelCapabilitiesGateway(model.WithoutRoutingSuffix()), LLMProviders.HELMHOLTZ => GetModelCapabilitiesOpenSource(model), LLMProviders.GWDG => GetModelCapabilitiesOpenSource(model), From d54a8be1b256fa70fec29f500d17f3f8a830da54 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 30 Aug 2026 15:12:41 +0200 Subject: [PATCH 04/10] Explain Hugging Face model routing failures to the user --- .../Assistants/I18N/allTexts.lua | 6 ++ .../plugin.lua | 6 ++ .../plugin.lua | 6 ++ .../Provider/BaseProvider.cs | 34 ++++++++-- .../HuggingFace/ProviderHuggingFace.cs | 66 ++++++++++++++++++- .../Provider/ProviderRequestFailureReason.cs | 9 +++ 6 files changed, 121 insertions(+), 6 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index a88361640..000531f3e 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -9286,6 +9286,12 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::HUGGINGFACE::HFINFERENCEPROVIDEREXTENSIONS: -- 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" 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 b508f517e..22677f4e7 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 @@ -9288,6 +9288,12 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::HUGGINGFACE::HFINFERENCEPROVIDEREXTENSIONS: -- 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" 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 23d72267a..818df9e7a 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 @@ -9288,6 +9288,12 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::HUGGINGFACE::HFINFERENCEPROVIDEREXTENSIONS: -- 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" diff --git a/app/MindWork AI Studio/Provider/BaseProvider.cs b/app/MindWork AI Studio/Provider/BaseProvider.cs index f0108a8d9..f1c7c0103 100644 --- a/app/MindWork AI Studio/Provider/BaseProvider.cs +++ b/app/MindWork AI Studio/Provider/BaseProvider.cs @@ -376,8 +376,13 @@ private static void TryGetProviderStreamError(JsonElement root, out string? erro 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 @@ private async Task SendRequest(Func LOGGER = Program.LOGGER_FACTORY.CreateLogger(); + private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ProviderHuggingFace).Namespace, nameof(ProviderHuggingFace)); + /// /// The OpenAI-compatible endpoint which serves every inference provider. /// @@ -48,6 +52,66 @@ private string BuildModelIdentifier(Model model) 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 /// diff --git a/app/MindWork AI Studio/Provider/ProviderRequestFailureReason.cs b/app/MindWork AI Studio/Provider/ProviderRequestFailureReason.cs index c56fcc4f1..4c5a33b41 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 From 0a165b306662c41f88b9ee831f4b711967dd6949 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 30 Aug 2026 15:23:04 +0200 Subject: [PATCH 05/10] Load available models for Hugging Face providers --- .../Dialogs/ProviderDialog.razor | 10 ++++- .../Dialogs/ProviderDialog.razor.cs | 25 +++++++++++- .../Provider/HuggingFace/ModelsResponse.cs | 26 +++++++++++++ .../HuggingFace/ProviderHuggingFace.cs | 38 ++++++++++++++++++- .../Provider/LLMProvidersExtensions.cs | 1 - 5 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 app/MindWork AI Studio/Provider/HuggingFace/ModelsResponse.cs diff --git a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor index aaa891c66..80732a71d 100644 --- a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor +++ b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor @@ -69,7 +69,7 @@ @if (this.DataLLMProvider.IsHFInstanceProviderNeeded()) { - + @foreach (HFInferenceProvider inferenceProvider in Enum.GetValues(typeof(HFInferenceProvider))) { @if (inferenceProvider is not HFInferenceProvider.NONE) @@ -91,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 f491da1ce..88908ce2c 100644 --- a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs @@ -226,7 +226,7 @@ protected override async Task OnInitializedAsync() { 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 @@ private void OnProviderChanged(LLMProviders selectedProvider) 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 @@ this.DataLLMProvider is LLMProviders.SELF_HOSTED && 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/Provider/HuggingFace/ModelsResponse.cs b/app/MindWork AI Studio/Provider/HuggingFace/ModelsResponse.cs new file mode 100644 index 000000000..1522bc10e --- /dev/null +++ b/app/MindWork AI Studio/Provider/HuggingFace/ModelsResponse.cs @@ -0,0 +1,26 @@ +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); \ 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 06fa24ce7..0b87bd5be 100644 --- a/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs +++ b/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs @@ -121,7 +121,7 @@ protected override string GetProviderRequestFailureUserMessage(ProviderRequestFa 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) @@ -176,7 +176,41 @@ public override Task>> EmbedTextAsync(Model e /// 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. Should that leave nothing, we show + /// the whole list instead: some providers serve models the list does not attribute to them, and + /// an empty selection would leave the user without any way forward. + /// + /// 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()).ToList(); + var providerSlug = this.hfProvider.EndpointsId(); + if (string.IsNullOrEmpty(providerSlug)) + return ToModels(chatModels); + + var servedModels = chatModels.Where(hfModel => IsServedBy(hfModel, providerSlug)).ToList(); + return ToModels(servedModels.Count > 0 ? servedModels : chatModels); + } + + 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)); } /// diff --git a/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs b/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs index c4006e665..0f7647c44 100644 --- a/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs +++ b/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs @@ -399,7 +399,6 @@ private static IProvider CreateProvider(this LLMProviders provider, string insta public static bool IsLLMModelProvidedManually(this LLMProviders provider) => provider switch { LLMProviders.FIREWORKS => true, - LLMProviders.HUGGINGFACE => true, _ => false, }; From ad8c51b87b598d923439188e1c8ab85494523fec Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 30 Aug 2026 15:40:42 +0200 Subject: [PATCH 06/10] Hide the Hugging Face Inference API from chat provider selection --- .../Assistants/I18N/allTexts.lua | 3 +++ .../Dialogs/ProviderDialog.razor | 2 +- .../plugin.lua | 3 +++ .../plugin.lua | 3 +++ .../HFInferenceProviderExtensions.cs | 20 +++++++++++++++++++ .../HuggingFace/ProviderHuggingFace.cs | 11 +++++----- .../Tools/Validation/ProviderValidation.cs | 7 +++++++ 7 files changed, 42 insertions(+), 7 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 000531f3e..8a31718db 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -11200,6 +11200,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T125334762"] = -- The instance name must be unique; the chosen name is already in use. UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T1301966140"] = "The instance name must be unique; the chosen name is already in use." +-- This Hugging Face inference provider no longer offers models to chat with. Please select another one. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T1379003702"] = "This Hugging Face inference provider no longer offers models to chat with. Please select another one." + -- Please select a provider. UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T1809312323"] = "Please select a provider." diff --git a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor index 80732a71d..7b6bde330 100644 --- a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor +++ b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor @@ -72,7 +72,7 @@ @foreach (HFInferenceProvider inferenceProvider in Enum.GetValues(typeof(HFInferenceProvider))) { - @if (inferenceProvider is not HFInferenceProvider.NONE) + @if (inferenceProvider.SupportsChat()) { @inferenceProvider.ToName() 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 22677f4e7..37016785b 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 @@ -11202,6 +11202,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T125334762"] = -- The instance name must be unique; the chosen name is already in use. UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T1301966140"] = "Der Instanzname muss eindeutig sein; der gewählte Name wird bereits verwendet." +-- This Hugging Face inference provider no longer offers models to chat with. Please select another one. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T1379003702"] = "Dieser Hugging-Face-Inferenzanbieter bietet keine Modelle mehr zum Chatten an. Bitte wählen Sie einen anderen Anbieter aus." + -- Please select a provider. UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T1809312323"] = "Bitte wählen Sie einen Anbieter 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 818df9e7a..489524cdb 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 @@ -11202,6 +11202,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T125334762"] = -- The instance name must be unique; the chosen name is already in use. UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T1301966140"] = "The instance name must be unique; the chosen name is already in use." +-- This Hugging Face inference provider no longer offers models to chat with. Please select another one. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T1379003702"] = "This Hugging Face inference provider no longer offers models to chat with. Please select another one." + -- Please select a provider. UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T1809312323"] = "Please select a provider." diff --git a/app/MindWork AI Studio/Provider/HuggingFace/HFInferenceProviderExtensions.cs b/app/MindWork AI Studio/Provider/HuggingFace/HFInferenceProviderExtensions.cs index a414116ba..29f7d8c42 100644 --- a/app/MindWork AI Studio/Provider/HuggingFace/HFInferenceProviderExtensions.cs +++ b/app/MindWork AI Studio/Provider/HuggingFace/HFInferenceProviderExtensions.cs @@ -52,6 +52,26 @@ public static class HFInferenceProviderExtensions _ => $":{provider.EndpointsId()}", }; + /// + /// Whether this inference provider serves models to chat with. + /// + /// + /// The Hugging Face Inference API is the odd one out: it grew from the classic NLP tasks and + /// serves embeddings, speech recognition, classification, and translation, but no chat models + /// at all. Asking it for one is answered with "The requested model is not supported by provider + /// 'hf-inference'", whichever model is named. So it must not be offered where a chat provider + /// is chosen. + /// + /// The inference provider. + /// True, when the provider serves chat models. + public static bool SupportsChat(this HFInferenceProvider provider) => provider switch + { + HFInferenceProvider.NONE => false, + HFInferenceProvider.HF_INFERENCE_API => false, + + _ => true, + }; + /// /// Removes the routing suffix from a model, if it carries one. /// diff --git a/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs b/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs index 0b87bd5be..6eb0e22c5 100644 --- a/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs +++ b/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs @@ -184,21 +184,20 @@ public override Task GetTextModels(string? apiKeyProvisional = /// /// /// 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. Should that leave nothing, we show - /// the whole list instead: some providers serve models the list does not attribute to them, and - /// an empty selection would leave the user without any way forward. + /// 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()).ToList(); + var chatModels = response.Data.Where(hfModel => new Model(hfModel.Id, null).IsChatModel()); var providerSlug = this.hfProvider.EndpointsId(); if (string.IsNullOrEmpty(providerSlug)) return ToModels(chatModels); - var servedModels = chatModels.Where(hfModel => IsServedBy(hfModel, providerSlug)).ToList(); - return ToModels(servedModels.Count > 0 ? servedModels : chatModels); + return ToModels(chatModels.Where(hfModel => IsServedBy(hfModel, providerSlug))); } private static IEnumerable ToModels(IEnumerable hfModels) => hfModels.Select(hfModel => new Model(hfModel.Id, null)); diff --git a/app/MindWork AI Studio/Tools/Validation/ProviderValidation.cs b/app/MindWork AI Studio/Tools/Validation/ProviderValidation.cs index 4a1406144..94909fda9 100644 --- a/app/MindWork AI Studio/Tools/Validation/ProviderValidation.cs +++ b/app/MindWork AI Studio/Tools/Validation/ProviderValidation.cs @@ -129,6 +129,13 @@ public sealed class ProviderValidation if (inferenceProvider is HFInferenceProvider.NONE) return TB("Please select an Hugging Face inference provider."); + // + // A provider which was configured earlier may have stopped serving chat models since. It is + // no longer among the choices, so the user has to be told why their selection is empty: + // + if (!inferenceProvider.SupportsChat()) + return TB("This Hugging Face inference provider no longer offers models to chat with. Please select another one."); + return null; } } \ No newline at end of file From 3afda40db8f854f925a670cd2fba5bb91e8b432e Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 30 Aug 2026 15:53:57 +0200 Subject: [PATCH 07/10] Fix chat models being hidden by the kling video marker --- .../Provider/ModelKindExtensions.cs | 62 ++++++++++++++++++- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/app/MindWork AI Studio/Provider/ModelKindExtensions.cs b/app/MindWork AI Studio/Provider/ModelKindExtensions.cs index 997e81ef5..c17000aa6 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 ModelKind DetermineKind(this Model model) 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 @@ private static bool HasAnyMarker(string modelId, string[] markers) 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 From 3cb73777720c307808d3e653a515fdf63403b734 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 30 Aug 2026 16:20:35 +0200 Subject: [PATCH 08/10] Add embedding support for Hugging Face providers --- .../Assistants/I18N/allTexts.lua | 12 +++- .../Settings/SettingsPanelEmbeddings.razor.cs | 1 + .../Dialogs/EmbeddingProviderDialog.razor | 19 ++++++ .../Dialogs/EmbeddingProviderDialog.razor.cs | 24 +++++++ .../Plugins/configuration/plugin.lua | 12 +++- .../plugin.lua | 12 +++- .../plugin.lua | 12 +++- .../Provider/HuggingFace/HFEndpointKind.cs | 24 +++++++ .../HuggingFace/HFInferenceProvider.cs | 9 +-- .../HFInferenceProviderExtensions.cs | 41 ++++++++---- .../Provider/HuggingFace/ModelsResponse.cs | 13 +++- .../HuggingFace/ProviderHuggingFace.cs | 67 +++++++++++++++++-- .../Provider/LLMProvidersExtensions.cs | 15 +++-- .../Settings/EmbeddingProvider.cs | 26 ++++++- .../Tools/Validation/ProviderValidation.cs | 30 +++++++-- 15 files changed, 270 insertions(+), 47 deletions(-) create mode 100644 app/MindWork AI Studio/Provider/HuggingFace/HFEndpointKind.cs diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 8a31718db..f595431be 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" @@ -11200,9 +11206,6 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T125334762"] = -- The instance name must be unique; the chosen name is already in use. UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T1301966140"] = "The instance name must be unique; the chosen name is already in use." --- This Hugging Face inference provider no longer offers models to chat with. Please select another one. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T1379003702"] = "This Hugging Face inference provider no longer offers models to chat with. Please select another one." - -- Please select a provider. UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T1809312323"] = "Please select a provider." @@ -11221,6 +11224,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T3999823516"] -- 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 ba0f4587a..2f2110c10 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 @@ private async Task EditEmbeddingProvider(EmbeddingProvider embeddingProvider) { 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/Dialogs/EmbeddingProviderDialog.razor b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor index 5bcead75a..b48f9ef56 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 4bb3c1896..f82499d0c 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 @@ private EmbeddingProvider CreateEmbeddingProviderSettings() IsEnterpriseConfiguration = this.IsEnterpriseConfiguration, EnterpriseConfigurationPluginId = Guid.Empty, CustomIconDataUrl = this.DataCustomIconDataUrl, + HFInferenceProvider = this.HFInferenceProviderId, }; } @@ -316,6 +324,22 @@ private void OnHostChanged(Host selectedHost) 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/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua index 4ca2d7ec8..c3c0f69f8 100644 --- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua +++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua @@ -121,14 +121,14 @@ CONFIG["LLM_PROVIDERS"] = {} -- -- -- Optional: Hugging Face inference provider. Only relevant for UsedLLMProvider = HUGGINGFACE. -- -- Allowed values are: BASETEN, CEREBRAS, COHERE, DEEPINFRA, FEATHERLESS_AI, FIREWORKS, GROQ, --- -- HF_INFERENCE_API, NOVITA, NSCALE, OVHCLOUD, PUBLIC_AI, SCALEWAY, TOGETHER_AI, ZAI +-- -- 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. Configurations still naming one of them are treated as if no provider was set, and --- -- the user is asked to choose again. +-- -- 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. @@ -210,6 +210,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 37016785b..c989c6e6f 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" @@ -11202,9 +11208,6 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T125334762"] = -- The instance name must be unique; the chosen name is already in use. UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T1301966140"] = "Der Instanzname muss eindeutig sein; der gewählte Name wird bereits verwendet." --- This Hugging Face inference provider no longer offers models to chat with. Please select another one. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T1379003702"] = "Dieser Hugging-Face-Inferenzanbieter bietet keine Modelle mehr zum Chatten an. Bitte wählen Sie einen anderen Anbieter aus." - -- Please select a provider. UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T1809312323"] = "Bitte wählen Sie einen Anbieter aus." @@ -11223,6 +11226,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T3999823516"] -- 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 489524cdb..8cb59f715 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" @@ -11202,9 +11208,6 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T125334762"] = -- The instance name must be unique; the chosen name is already in use. UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T1301966140"] = "The instance name must be unique; the chosen name is already in use." --- This Hugging Face inference provider no longer offers models to chat with. Please select another one. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T1379003702"] = "This Hugging Face inference provider no longer offers models to chat with. Please select another one." - -- Please select a provider. UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T1809312323"] = "Please select a provider." @@ -11223,6 +11226,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T3999823516"] -- 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/HuggingFace/HFEndpointKind.cs b/app/MindWork AI Studio/Provider/HuggingFace/HFEndpointKind.cs new file mode 100644 index 000000000..652c47fde --- /dev/null +++ b/app/MindWork AI Studio/Provider/HuggingFace/HFEndpointKind.cs @@ -0,0 +1,24 @@ +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 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 of the two 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, +} \ 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 81195ade8..a7a2bba8f 100644 --- a/app/MindWork AI Studio/Provider/HuggingFace/HFInferenceProvider.cs +++ b/app/MindWork AI Studio/Provider/HuggingFace/HFInferenceProvider.cs @@ -11,9 +11,11 @@ /// 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). 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. +/// (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 { @@ -36,7 +38,6 @@ public enum HFInferenceProvider FEATHERLESS_AI, FIREWORKS, GROQ, - HF_INFERENCE_API, NOVITA, NSCALE, OVHCLOUD, diff --git a/app/MindWork AI Studio/Provider/HuggingFace/HFInferenceProviderExtensions.cs b/app/MindWork AI Studio/Provider/HuggingFace/HFInferenceProviderExtensions.cs index 29f7d8c42..ba02ad9db 100644 --- a/app/MindWork AI Studio/Provider/HuggingFace/HFInferenceProviderExtensions.cs +++ b/app/MindWork AI Studio/Provider/HuggingFace/HFInferenceProviderExtensions.cs @@ -20,7 +20,6 @@ public static class HFInferenceProviderExtensions HFInferenceProvider.FEATHERLESS_AI => "featherless-ai", HFInferenceProvider.FIREWORKS => "fireworks-ai", HFInferenceProvider.GROQ => "groq", - HFInferenceProvider.HF_INFERENCE_API => "hf-inference", HFInferenceProvider.NOVITA => "novita", HFInferenceProvider.NSCALE => "nscale", HFInferenceProvider.OVHCLOUD => "ovhcloud", @@ -55,21 +54,40 @@ public static class HFInferenceProviderExtensions /// /// 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. + /// /// - /// The Hugging Face Inference API is the odd one out: it grew from the classic NLP tasks and - /// serves embeddings, speech recognition, classification, and translation, but no chat models - /// at all. Asking it for one is answered with "The requested model is not supported by provider - /// 'hf-inference'", whichever model is named. So it must not be offered where a chat provider - /// is chosen. + /// 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 the provider serves chat models. - public static bool SupportsChat(this HFInferenceProvider provider) => provider switch + /// True, when we can create embeddings through this provider. + public static bool SupportsEmbeddings(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.NONE => false, - HFInferenceProvider.HF_INFERENCE_API => false, + HFInferenceProvider.TOGETHER_AI => "https://router.huggingface.co/together/v1/", + HFInferenceProvider.DEEPINFRA => "https://router.huggingface.co/deepinfra/v1/openai/", - _ => true, + _ => string.Empty, }; /// @@ -114,7 +132,6 @@ public static string CatalogFilter(this HFInferenceProvider provider) HFInferenceProvider.FEATHERLESS_AI => "Featherless AI", HFInferenceProvider.FIREWORKS => "Fireworks AI", HFInferenceProvider.GROQ => "Groq", - HFInferenceProvider.HF_INFERENCE_API => "Hugging Face Inference API", HFInferenceProvider.NOVITA => "Novita", HFInferenceProvider.NSCALE => "Nscale", HFInferenceProvider.OVHCLOUD => "OVHcloud", diff --git a/app/MindWork AI Studio/Provider/HuggingFace/ModelsResponse.cs b/app/MindWork AI Studio/Provider/HuggingFace/ModelsResponse.cs index 1522bc10e..79f9847be 100644 --- a/app/MindWork AI Studio/Provider/HuggingFace/ModelsResponse.cs +++ b/app/MindWork AI Studio/Provider/HuggingFace/ModelsResponse.cs @@ -23,4 +23,15 @@ namespace AIStudio.Provider.HuggingFace; /// /// 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); \ No newline at end of file +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 6eb0e22c5..916042d26 100644 --- a/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs +++ b/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs @@ -26,12 +26,44 @@ public sealed class ProviderHuggingFace : BaseProvider /// 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) : base(LLMProviders.HUGGINGFACE, new Uri(ROUTER_BASE_URL), ExternalHttpTrustPolicy.SYSTEM_TRUST_ONLY, LOGGER) + public ProviderHuggingFace(HFInferenceProvider hfProvider, HFEndpointKind endpointKind = HFEndpointKind.CHAT) : base(LLMProviders.HUGGINGFACE, new Uri(BuildBaseURL(hfProvider, endpointKind)), ExternalHttpTrustPolicy.SYSTEM_TRUST_ONLY, LOGGER) { this.hfProvider = hfProvider; - LOGGER.LogInformation($"We use the inference provider '{hfProvider}'. Thus, we address the models as '{hfProvider.ModelSuffix()}'."); + 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; } /// @@ -168,9 +200,15 @@ public override Task TranscribeAudioAsync(Model transcripti } /// - 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); } /// @@ -221,7 +259,24 @@ public override Task GetImageModels(string? apiKeyProvisional = /// 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); } /// @@ -231,4 +286,4 @@ public override Task GetTranscriptionModels(string? apiKeyProvi } #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 0f7647c44..2ab00c378 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. // @@ -274,7 +281,7 @@ public static IProvider CreateProvider(this AIStudio.Settings.Provider providerS /// The provider instance. public static IProvider CreateProvider(this EmbeddingProvider embeddingProviderSettings) { - return embeddingProviderSettings.UsedLLMProvider.CreateProvider(embeddingProviderSettings.Name, embeddingProviderSettings.Host, embeddingProviderSettings.Hostname, 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); } /// @@ -287,7 +294,7 @@ public static IProvider CreateProvider(this TranscriptionProvider transcriptionP return transcriptionProviderSettings.UsedLLMProvider.CreateProvider(transcriptionProviderSettings.Name, transcriptionProviderSettings.Host, transcriptionProviderSettings.Hostname, HFInferenceProvider.NONE, configuredProviderId: transcriptionProviderSettings.Id, isEnterpriseConfiguration: transcriptionProviderSettings.IsEnterpriseConfiguration); } - private static IProvider CreateProvider(this LLMProviders provider, string instanceName, Host host, string hostname, 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 +315,7 @@ private static IProvider CreateProvider(this LLMProviders provider, string insta 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) { 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 }, diff --git a/app/MindWork AI Studio/Settings/EmbeddingProvider.cs b/app/MindWork AI Studio/Settings/EmbeddingProvider.cs index 61324df36..64454e27c 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 static bool TryParseEmbeddingProviderTable(int idx, LuaTable table, Guid 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 static bool TryParseEmbeddingProviderTable(int idx, LuaTable table, Guid 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 @@ private static bool TryReadModelTable(int idx, LuaTable table, Guid configPlugin /// 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 string ExportAsConfigurationSection(string? encryptedApiKey = null) ["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/Tools/Validation/ProviderValidation.cs b/app/MindWork AI Studio/Tools/Validation/ProviderValidation.cs index 94909fda9..3b58fdd86 100644 --- a/app/MindWork AI Studio/Tools/Validation/ProviderValidation.cs +++ b/app/MindWork AI Studio/Tools/Validation/ProviderValidation.cs @@ -122,6 +122,28 @@ public sealed class ProviderValidation } public string? ValidatingHFInstanceProvider(HFInferenceProvider inferenceProvider) + { + if(this.GetProvider() is not LLMProviders.HUGGINGFACE) + return null; + + 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; @@ -129,12 +151,8 @@ public sealed class ProviderValidation if (inferenceProvider is HFInferenceProvider.NONE) return TB("Please select an Hugging Face inference provider."); - // - // A provider which was configured earlier may have stopped serving chat models since. It is - // no longer among the choices, so the user has to be told why their selection is empty: - // - if (!inferenceProvider.SupportsChat()) - return TB("This Hugging Face inference provider no longer offers models to chat with. Please select another one."); + if (!inferenceProvider.SupportsEmbeddings()) + return TB("This Hugging Face inference provider does not create embeddings. Please select another one."); return null; } From c942066f740260d47cb155172581a6365c63bd1d Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 30 Aug 2026 16:29:24 +0200 Subject: [PATCH 09/10] Add speech-to-text support for Hugging Face providers --- .../Assistants/I18N/allTexts.lua | 9 +++++++ .../SettingsPanelTranscription.razor.cs | 1 + .../Dialogs/TranscriptionProviderDialog.razor | 19 +++++++++++++++ .../TranscriptionProviderDialog.razor.cs | 24 +++++++++++++++++++ .../Plugins/configuration/plugin.lua | 6 +++++ .../plugin.lua | 9 +++++++ .../plugin.lua | 9 +++++++ .../Provider/HuggingFace/HFEndpointKind.cs | 11 ++++++--- .../HFInferenceProviderExtensions.cs | 13 ++++++++++ .../HuggingFace/ProviderHuggingFace.cs | 15 +++++++++--- .../Provider/LLMProvidersExtensions.cs | 11 +++++++-- .../Settings/TranscriptionProvider.cs | 24 ++++++++++++++++++- .../Tools/Validation/ProviderValidation.cs | 23 ++++++++++++++++++ 13 files changed, 165 insertions(+), 9 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index f595431be..cbdb9baf7 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -7996,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." @@ -8038,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." @@ -11221,6 +11227,9 @@ 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." diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor.cs index 252a95a44..ccfe0e979 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 @@ private async Task EditTranscriptionProvider(TranscriptionProvider transcription { 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/TranscriptionProviderDialog.razor b/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor index e92889a42..826a29595 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 1e1081f37..b596fcdd7 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 @@ private TranscriptionProvider CreateTranscriptionProviderSettings() IsEnterpriseConfiguration = this.IsEnterpriseConfiguration, EnterpriseConfigurationPluginId = Guid.Empty, CustomIconDataUrl = this.DataCustomIconDataUrl, + HFInferenceProvider = this.HFInferenceProviderId, }; } @@ -331,6 +339,22 @@ private void OnHostChanged(Host selectedHost) 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 c3c0f69f8..5317d8303 100644 --- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua +++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua @@ -179,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"] = "", 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 c989c6e6f..99ec52b56 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 @@ -7998,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." @@ -8040,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." @@ -11223,6 +11229,9 @@ 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." 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 8cb59f715..2aa2f5992 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 @@ -7998,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." @@ -8040,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." @@ -11223,6 +11229,9 @@ 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." diff --git a/app/MindWork AI Studio/Provider/HuggingFace/HFEndpointKind.cs b/app/MindWork AI Studio/Provider/HuggingFace/HFEndpointKind.cs index 652c47fde..d1c821bd8 100644 --- a/app/MindWork AI Studio/Provider/HuggingFace/HFEndpointKind.cs +++ b/app/MindWork AI Studio/Provider/HuggingFace/HFEndpointKind.cs @@ -6,9 +6,9 @@ namespace AIStudio.Provider.HuggingFace; /// /// 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 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 of the two it is for. +/// 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 { @@ -21,4 +21,9 @@ public enum HFEndpointKind /// 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/HFInferenceProviderExtensions.cs b/app/MindWork AI Studio/Provider/HuggingFace/HFInferenceProviderExtensions.cs index ba02ad9db..de57ea88c 100644 --- a/app/MindWork AI Studio/Provider/HuggingFace/HFInferenceProviderExtensions.cs +++ b/app/MindWork AI Studio/Provider/HuggingFace/HFInferenceProviderExtensions.cs @@ -71,6 +71,19 @@ public static class HFInferenceProviderExtensions /// 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. /// diff --git a/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs b/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs index 916042d26..f954e3419 100644 --- a/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs +++ b/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs @@ -194,9 +194,15 @@ public override async IAsyncEnumerable StreamImageCompletion(Model ima #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); } /// @@ -282,7 +288,10 @@ private Task LoadHubModels(SecretStoreType storeType, string pi /// 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 diff --git a/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs b/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs index 2ab00c378..546845c68 100644 --- a/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs +++ b/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs @@ -253,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. // @@ -291,7 +298,7 @@ public static IProvider CreateProvider(this EmbeddingProvider embeddingProviderS /// The provider instance. public static IProvider CreateProvider(this TranscriptionProvider transcriptionProviderSettings) { - return transcriptionProviderSettings.UsedLLMProvider.CreateProvider(transcriptionProviderSettings.Name, transcriptionProviderSettings.Host, transcriptionProviderSettings.Hostname, 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, HFInferenceProvider inferenceProvider, string configuredProviderId = "", string expertProviderApiParameter = "", bool isEnterpriseConfiguration = false, HFEndpointKind hfEndpointKind = HFEndpointKind.CHAT) diff --git a/app/MindWork AI Studio/Settings/TranscriptionProvider.cs b/app/MindWork AI Studio/Settings/TranscriptionProvider.cs index 7ecc79384..7a15f24af 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 static bool TryParseTranscriptionProviderTable(int idx, LuaTable table, G 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 static bool TryParseTranscriptionProviderTable(int idx, LuaTable table, G 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 @@ private static bool TryReadModelTable(int idx, LuaTable table, Guid configPlugin /// 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 string ExportAsConfigurationSection(string? encryptedApiKey = null) ["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 3b58fdd86..ae0d5c9d1 100644 --- a/app/MindWork AI Studio/Tools/Validation/ProviderValidation.cs +++ b/app/MindWork AI Studio/Tools/Validation/ProviderValidation.cs @@ -156,4 +156,27 @@ public sealed class ProviderValidation 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 From 2e859f49d5483c903d627192d34c19cf01e01437 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 30 Aug 2026 16:43:29 +0200 Subject: [PATCH 10/10] Updated changelog --- app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md | 4 ++++ 1 file changed, 4 insertions(+) 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 952d3080a..fcda497a3 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.