mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-17 17:38:00 +00:00
Reworked the Hugging Face provider (#944)
This commit is contained in:
1 parent
a7228c7782
commit
f14c69b938
28 files changed
+1039
-93
No files matched your search
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user