Reworked the Hugging Face provider (#944)

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-08-30 16:45:14 +02:00
1 parent a7228c7782
commit f14c69b938
28 files changed
+1039 -93

No files matched your search

@@ -0,0 +1,29 @@
namespace AIStudio.Provider.HuggingFace;
/// <summary>
/// Which of the Hugging Face endpoints a provider instance talks to.
/// </summary>
/// <remarks>
/// Hugging Face serves chatting and everything else from different places. Chat completions go to
/// the router's own OpenAI-compatible endpoint, which accepts the model IDs as the hub writes them
/// and picks an inference provider from a suffix. Embeddings and transcription do not exist there
/// at all and have to be asked of one provider's own route. Because the base URL is fixed when a
/// provider instance is built, the instance has to know from the start which one it is for.
/// </remarks>
public enum HFEndpointKind
{
/// <summary>
/// The router's own endpoint, which serves chat completions.
/// </summary>
CHAT,
/// <summary>
/// The OpenAI-compatible route of one inference provider, which serves embeddings.
/// </summary>
EMBEDDING,
/// <summary>
/// The OpenAI-compatible route of one inference provider, which transcribes audio.
/// </summary>
TRANSCRIPTION,
}
@@ -3,16 +3,46 @@
/// <summary>
/// Enum for inference providers that Hugging Face supports.
/// </summary>
/// <remarks>
/// Besides the providers themselves, this enum carries the routing strategies Hugging Face offers.
/// They are no providers, but they take the same place: the router picks a provider for us instead
/// of us naming one.
///
/// NONE must stay the first value: settings are read through the tolerant enum converter, which
/// falls back to the first value whenever it meets a name we no longer know. That is what happens
/// to a configuration naming one of the providers Hugging Face stopped routing in July 2026
/// (Hyperbolic, SambaNova, Nebius, NVIDIA, Clarifai, Black Forest Labs), and to one naming the
/// Hugging Face Inference API, which serves no model we can reach: it has no chat models at all,
/// and its OpenAI-compatible routes for embeddings and transcription do not exist. Such a provider
/// has to end up on NONE, where the validation asks the user to choose again. Were a routing
/// strategy first, those configurations would silently switch to automatic routing instead.
/// </remarks>
public enum HFInferenceProvider
{
NONE,
//
// Routing strategies. Hugging Face writes them where a provider name would go:
//
AUTOMATIC,
CHEAPEST,
PREFERRED,
//
// The providers Hugging Face routes:
//
BASETEN,
CEREBRAS,
NEBIUS_AI_STUDIO,
SAMBANOVA,
NOVITA,
HYPERBOLIC,
TOGETHER_AI,
COHERE,
DEEPINFRA,
FEATHERLESS_AI,
FIREWORKS,
HF_INFERENCE_API,
GROQ,
NOVITA,
NSCALE,
OVHCLOUD,
PUBLIC_AI,
SCALEWAY,
TOGETHER_AI,
ZAI,
}
@@ -1,43 +1,158 @@
namespace AIStudio.Provider.HuggingFace;
using AIStudio.Tools.PluginSystem;
namespace AIStudio.Provider.HuggingFace;
public static class HFInferenceProviderExtensions
{
public static string Endpoints(this HFInferenceProvider provider, Model model) => provider switch
{
HFInferenceProvider.CEREBRAS => "cerebras/v1/",
HFInferenceProvider.NEBIUS_AI_STUDIO => "nebius/v1/",
HFInferenceProvider.SAMBANOVA => "sambanova/v1/",
HFInferenceProvider.NOVITA => "novita/v3/openai/",
HFInferenceProvider.HYPERBOLIC => "hyperbolic/v1/",
HFInferenceProvider.TOGETHER_AI => "together/v1/",
HFInferenceProvider.FIREWORKS => "fireworks-ai/inference/v1/",
HFInferenceProvider.HF_INFERENCE_API => $"hf-inference/models/{model.ToString()}/v1/",
_ => string.Empty,
};
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(HFInferenceProviderExtensions).Namespace, nameof(HFInferenceProviderExtensions));
/// <summary>
/// The slug Hugging Face uses for this inference provider.
/// </summary>
/// <param name="provider">The inference provider.</param>
/// <returns>The slug, or an empty string for the routing strategies, which name no provider.</returns>
public static string EndpointsId(this HFInferenceProvider provider) => provider switch
{
HFInferenceProvider.BASETEN => "baseten",
HFInferenceProvider.CEREBRAS => "cerebras",
HFInferenceProvider.NEBIUS_AI_STUDIO => "nebius",
HFInferenceProvider.SAMBANOVA => "sambanova",
HFInferenceProvider.COHERE => "cohere",
HFInferenceProvider.DEEPINFRA => "deepinfra",
HFInferenceProvider.FEATHERLESS_AI => "featherless-ai",
HFInferenceProvider.FIREWORKS => "fireworks-ai",
HFInferenceProvider.GROQ => "groq",
HFInferenceProvider.NOVITA => "novita",
HFInferenceProvider.HYPERBOLIC => "hyperbolic",
HFInferenceProvider.NSCALE => "nscale",
HFInferenceProvider.OVHCLOUD => "ovhcloud",
HFInferenceProvider.PUBLIC_AI => "publicai",
HFInferenceProvider.SCALEWAY => "scaleway",
HFInferenceProvider.TOGETHER_AI => "together",
HFInferenceProvider.FIREWORKS => "fireworks",
HFInferenceProvider.HF_INFERENCE_API => "hf-inference",
HFInferenceProvider.ZAI => "zai-org",
_ => string.Empty,
};
/// <summary>
/// The suffix which tells the router where to send the request.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="provider">The inference provider.</param>
/// <returns>The suffix including its colon, or an empty string when the router should choose.</returns>
public static string ModelSuffix(this HFInferenceProvider provider) => provider switch
{
HFInferenceProvider.NONE or HFInferenceProvider.AUTOMATIC => string.Empty,
HFInferenceProvider.CHEAPEST => ":cheapest",
HFInferenceProvider.PREFERRED => ":preferred",
_ => $":{provider.EndpointsId()}",
};
/// <summary>
/// Whether this inference provider serves models to chat with.
/// </summary>
/// <param name="provider">The inference provider.</param>
/// <returns>True, when the provider serves chat models.</returns>
public static bool SupportsChat(this HFInferenceProvider provider) => provider is not HFInferenceProvider.NONE;
/// <summary>
/// Whether this inference provider creates embeddings for us.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="provider">The inference provider.</param>
/// <returns>True, when we can create embeddings through this provider.</returns>
public static bool SupportsEmbeddings(this HFInferenceProvider provider) => provider is HFInferenceProvider.TOGETHER_AI or HFInferenceProvider.DEEPINFRA;
/// <summary>
/// Whether this inference provider transcribes audio for us.
/// </summary>
/// <remarks>
/// 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".
/// </remarks>
/// <param name="provider">The inference provider.</param>
/// <returns>True, when we can transcribe audio through this provider.</returns>
public static bool SupportsTranscription(this HFInferenceProvider provider) => provider is HFInferenceProvider.TOGETHER_AI or HFInferenceProvider.DEEPINFRA;
/// <summary>
/// The base URL of the provider's own OpenAI-compatible route.
/// </summary>
/// <remarks>
/// 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".
/// </remarks>
/// <param name="provider">The inference provider.</param>
/// <returns>The base URL, or an empty string when the provider has no such route.</returns>
public static string ProviderBaseURL(this HFInferenceProvider provider) => provider switch
{
HFInferenceProvider.TOGETHER_AI => "https://router.huggingface.co/together/v1/",
HFInferenceProvider.DEEPINFRA => "https://router.huggingface.co/deepinfra/v1/openai/",
_ => string.Empty,
};
/// <summary>
/// Removes the routing suffix from a model, if it carries one.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="model">The model as it is configured.</param>
/// <returns>The model without its routing suffix.</returns>
public static Model WithoutRoutingSuffix(this Model model)
{
var separatorIndex = model.Id.LastIndexOf(':');
return separatorIndex is -1 ? model : model with { Id = model.Id[..separatorIndex] };
}
/// <summary>
/// The value to filter the Hugging Face model catalog by.
/// </summary>
/// <param name="provider">The inference provider.</param>
/// <returns>The provider slug, or "all" when no particular provider was chosen.</returns>
public static string CatalogFilter(this HFInferenceProvider provider)
{
var slug = provider.EndpointsId();
return string.IsNullOrEmpty(slug) ? "all" : slug;
}
public static string ToName(this HFInferenceProvider provider) => provider switch
{
HFInferenceProvider.AUTOMATIC => TB("Automatic: the fastest provider"),
HFInferenceProvider.CHEAPEST => TB("Automatic: the cheapest provider"),
HFInferenceProvider.PREFERRED => TB("Automatic: your preferred order"),
HFInferenceProvider.BASETEN => "Baseten",
HFInferenceProvider.CEREBRAS => "Cerebras",
HFInferenceProvider.NEBIUS_AI_STUDIO => "Nebius AI Studio",
HFInferenceProvider.SAMBANOVA => "Sambanova",
HFInferenceProvider.NOVITA => "Novita",
HFInferenceProvider.HYPERBOLIC => "Hyperbolic",
HFInferenceProvider.TOGETHER_AI => "Together AI",
HFInferenceProvider.COHERE => "Cohere",
HFInferenceProvider.DEEPINFRA => "DeepInfra",
HFInferenceProvider.FEATHERLESS_AI => "Featherless AI",
HFInferenceProvider.FIREWORKS => "Fireworks AI",
HFInferenceProvider.HF_INFERENCE_API => "Hugging Face Inference API",
HFInferenceProvider.GROQ => "Groq",
HFInferenceProvider.NOVITA => "Novita",
HFInferenceProvider.NSCALE => "Nscale",
HFInferenceProvider.OVHCLOUD => "OVHcloud",
HFInferenceProvider.PUBLIC_AI => "Public AI",
HFInferenceProvider.SCALEWAY => "Scaleway",
HFInferenceProvider.TOGETHER_AI => "Together AI",
HFInferenceProvider.ZAI => "Z.ai",
_ => string.Empty,
};
}
@@ -0,0 +1,37 @@
namespace AIStudio.Provider.HuggingFace;
/// <summary>
/// A data model for the response from the model endpoint of the Hugging Face router.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="Data">The models the router knows.</param>
public readonly record struct ModelsResponse(IList<HFModel> Data);
/// <summary>
/// One model as the Hugging Face router describes it.
/// </summary>
/// <param name="Id">The ID of the model, written as "org/model".</param>
/// <param name="Providers">The inference providers serving this model.</param>
public readonly record struct HFModel(string Id, IList<HFModelProvider>? Providers);
/// <summary>
/// One inference provider serving a model.
/// </summary>
/// <param name="Provider">The slug of the inference provider, e.g. "novita".</param>
/// <param name="Status">Whether the provider currently serves the model. Known value: "live".</param>
public readonly record struct HFModelProvider(string Provider, string Status);
/// <summary>
/// One model as the Hugging Face hub lists it.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="Id">The ID of the model, written as "org/model".</param>
public readonly record struct HubModel(string Id);
@@ -1,8 +1,10 @@
using System.Runtime.CompilerServices;
using System.Net;
using System.Runtime.CompilerServices;
using AIStudio.Chat;
using AIStudio.Provider.OpenAI;
using AIStudio.Settings;
using AIStudio.Tools.PluginSystem;
namespace AIStudio.Provider.HuggingFace;
@@ -10,11 +12,138 @@ public sealed class ProviderHuggingFace : BaseProvider
{
private static readonly ILogger<ProviderHuggingFace> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ProviderHuggingFace>();
public ProviderHuggingFace(HFInferenceProvider hfProvider, Model model) : base(LLMProviders.HUGGINGFACE, new Uri($"https://router.huggingface.co/{hfProvider.Endpoints(model)}"), ExternalHttpTrustPolicy.SYSTEM_TRUST_ONLY, LOGGER)
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ProviderHuggingFace).Namespace, nameof(ProviderHuggingFace));
/// <summary>
/// The OpenAI-compatible endpoint which serves every inference provider.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
private const string ROUTER_BASE_URL = "https://router.huggingface.co/v1/";
/// <summary>
/// Where the models of an inference provider are listed.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
private const string HUB_MODELS_URL = "https://huggingface.co/api/models?limit=100&sort=downloads&direction=-1&inference_provider=";
private readonly HFInferenceProvider hfProvider;
public ProviderHuggingFace(HFInferenceProvider hfProvider, HFEndpointKind endpointKind = HFEndpointKind.CHAT) : base(LLMProviders.HUGGINGFACE, new Uri(BuildBaseURL(hfProvider, endpointKind)), ExternalHttpTrustPolicy.SYSTEM_TRUST_ONLY, LOGGER)
{
LOGGER.LogInformation($"We use the inference provider '{hfProvider}'. Thus we use the base URL 'https://router.huggingface.co/{hfProvider.Endpoints(model)}'.");
this.hfProvider = hfProvider;
LOGGER.LogInformation($"We use the inference provider '{hfProvider}' for {endpointKind}. Thus, we use the base URL '{BuildBaseURL(hfProvider, endpointKind)}'.");
}
/// <summary>
/// Determines the base URL for the endpoint this provider instance talks to.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="hfProvider">The chosen inference provider.</param>
/// <param name="endpointKind">Which endpoint this instance is built for.</param>
/// <returns>The base URL to use.</returns>
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;
}
/// <summary>
/// Builds the model name to send to the router.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="model">The model the user chose.</param>
/// <returns>The model name including the provider suffix, when one applies.</returns>
private string BuildModelIdentifier(Model model)
{
var modelId = model.Id;
if (string.IsNullOrWhiteSpace(modelId) || modelId.Contains(':'))
return modelId;
return $"{modelId}{this.hfProvider.ModelSuffix()}";
}
/// <summary>
/// Recognizes the router's answer for a model the chosen inference provider does not serve.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="value">A piece of the failed response: an error code, a message, or the body.</param>
/// <returns>True, when this text names an unsupported model.</returns>
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
/// <inheritdoc />
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);
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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
/// <inheritdoc />
@@ -24,7 +153,7 @@ public sealed class ProviderHuggingFace : BaseProvider
public override string InstanceName { get; set; } = "HuggingFace";
/// <inheritdoc />
public override bool HasModelLoadingCapability => false;
public override bool HasModelLoadingCapability => true;
/// <inheritdoc />
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
@@ -41,7 +170,7 @@ public sealed class ProviderHuggingFace : BaseProvider
return new ChatCompletionAPIRequest
{
Model = chatModel.Id,
Model = this.BuildModelIdentifier(chatModel),
// Build the messages:
// - First of all the system prompt
@@ -65,21 +194,66 @@ public sealed class ProviderHuggingFace : BaseProvider
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc />
public override Task<TranscriptionResult> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
public override async Task<TranscriptionResult> 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);
}
/// <inhertidoc />
public override Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
public override async Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
{
return Task.FromResult<IReadOnlyList<IReadOnlyList<float>>>([]);
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);
}
/// <inheritdoc />
public override Task<ModelLoadResult> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
{
return Task.FromResult(ModelLoadResult.FromModels([]));
return this.LoadModelsResponse<ModelsResponse>(SecretStoreType.LLM_PROVIDER, "models", this.SelectChatModels, token, apiKeyProvisional);
}
/// <summary>
/// Picks the models the user may chat with through the chosen inference provider.
/// </summary>
/// <remarks>
/// The router reports every model it knows, together with the providers serving it. When the
/// user named a provider, we show what that provider offers and nothing else. Showing more
/// would be a disservice: every model outside that list is answered with a bad request, and the
/// user would only learn about it once they try to chat.
/// </remarks>
/// <param name="response">The response of the model endpoint.</param>
/// <returns>The models to offer.</returns>
private IEnumerable<Model> SelectChatModels(ModelsResponse response)
{
var chatModels = response.Data.Where(hfModel => new Model(hfModel.Id, null).IsChatModel());
var providerSlug = this.hfProvider.EndpointsId();
if (string.IsNullOrEmpty(providerSlug))
return ToModels(chatModels);
return ToModels(chatModels.Where(hfModel => IsServedBy(hfModel, providerSlug)));
}
private static IEnumerable<Model> ToModels(IEnumerable<HFModel> 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));
}
/// <inheritdoc />
@@ -91,14 +265,34 @@ public sealed class ProviderHuggingFace : BaseProvider
/// <inheritdoc />
public override Task<ModelLoadResult> 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);
}
/// <summary>
/// Loads the models one inference provider offers for a task, as the hub lists them.
/// </summary>
/// <param name="storeType">Which stored API key to use.</param>
/// <param name="pipelineTag">The task to ask for, as the hub names it.</param>
/// <param name="apiKeyProvisional">An API key which is not stored yet.</param>
/// <param name="token">The cancellation token to use.</param>
/// <returns>The models of that provider for that task.</returns>
private Task<ModelLoadResult> LoadHubModels(SecretStoreType storeType, string pipelineTag, string? apiKeyProvisional, CancellationToken token)
{
var requestURL = $"{HUB_MODELS_URL}{this.hfProvider.EndpointsId()}&pipeline_tag={pipelineTag}";
return this.LoadModelsResponse<IList<HubModel>>(storeType, requestURL, hubModels => hubModels.Select(hubModel => new Model(hubModel.Id, null)), token, apiKeyProvisional);
}
/// <inheritdoc />
public override Task<ModelLoadResult> 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
}
}