mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-13 20:13:48 +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
@@ -376,8 +376,13 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
errorCode = TryGetString(root, "code");
|
||||
errorType = TryGetString(root, "type");
|
||||
|
||||
// Services built on FastAPI, such as Helmholtz Blablador, word their errors as "detail":
|
||||
errorMessage = TryGetString(root, "message") ?? TryGetString(root, "detail");
|
||||
//
|
||||
// Services built on FastAPI, such as Helmholtz Blablador, word their errors as "detail".
|
||||
// And some providers put the sentence straight into "error" instead of an object, e.g.
|
||||
// {"error": "Model not supported by provider novita"}. The object form was handled above,
|
||||
// so reading "error" here can only meet the plain sentence:
|
||||
//
|
||||
errorMessage = TryGetString(root, "message") ?? TryGetString(root, "detail") ?? TryGetString(root, "error");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -510,16 +515,35 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
|
||||
if(nextResponse.StatusCode is HttpStatusCode.BadRequest)
|
||||
{
|
||||
//
|
||||
// The provider explains the problem in the body, while the reason phrase says no
|
||||
// more than "Bad Request". We show that explanation and fall back to the phrase
|
||||
// only when the body carries none:
|
||||
//
|
||||
var badRequestMessage = ReadProviderErrorMessage(errorBody);
|
||||
if (string.IsNullOrWhiteSpace(badRequestMessage))
|
||||
badRequestMessage = nextResponse.ReasonPhrase;
|
||||
|
||||
//
|
||||
// When we recognize what went wrong, we say what it means for the user instead of
|
||||
// guessing at the message format. The classification happened above already:
|
||||
//
|
||||
var classifiedMessage = this.GetProviderRequestFailureUserMessage(providerRequestFailure);
|
||||
if(!string.IsNullOrWhiteSpace(classifiedMessage))
|
||||
{
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, classifiedMessage));
|
||||
}
|
||||
|
||||
// Check if the error body contains "context" and "token" (case-insensitive),
|
||||
// which indicates that the context window is likely exceeded:
|
||||
if(errorBody.Contains("context", StringComparison.InvariantCultureIgnoreCase) &&
|
||||
else if(errorBody.Contains("context", StringComparison.InvariantCultureIgnoreCase) &&
|
||||
errorBody.Contains("token", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). The data of the chat, including all file attachments, is probably too large for the selected model and provider. The provider message is: '{2}'"), this.InstanceName, this.Provider, nextResponse.ReasonPhrase)));
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). The data of the chat, including all file attachments, is probably too large for the selected model and provider. The provider message is: '{2}'"), this.InstanceName, this.Provider, badRequestMessage)));
|
||||
}
|
||||
else
|
||||
{
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). The required message format might be changed. The provider message is: '{2}'"), this.InstanceName, this.Provider, nextResponse.ReasonPhrase)));
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). The required message format might be changed. The provider message is: '{2}'"), this.InstanceName, this.Provider, badRequestMessage)));
|
||||
}
|
||||
|
||||
this.logger.LogError("Failed request with status code {ResponseStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -206,10 +206,17 @@ public static class LLMProvidersExtensions
|
||||
LLMProviders.FIREWORKS => false,
|
||||
LLMProviders.X => false,
|
||||
LLMProviders.DEEP_SEEK => false,
|
||||
LLMProviders.HUGGINGFACE => false,
|
||||
LLMProviders.PERPLEXITY => false,
|
||||
LLMProviders.HETZNER => false,
|
||||
|
||||
//
|
||||
// Hugging Face serves embeddings, but not through the router endpoint we chat with: that
|
||||
// one answers "/v1/embeddings" with a plain "Not Found". They have to be asked of one
|
||||
// inference provider directly, and only some of them answer the OpenAI-compatible form.
|
||||
// Which ones is decided by HFInferenceProviderExtensions.SupportsEmbeddings.
|
||||
//
|
||||
LLMProviders.HUGGINGFACE => true,
|
||||
|
||||
//
|
||||
// Self-hosted providers are treated as a special case anyway.
|
||||
//
|
||||
@@ -246,9 +253,16 @@ public static class LLMProvidersExtensions
|
||||
LLMProviders.ANTHROPIC => false,
|
||||
LLMProviders.X => false,
|
||||
LLMProviders.DEEP_SEEK => false,
|
||||
LLMProviders.HUGGINGFACE => false,
|
||||
LLMProviders.PERPLEXITY => false,
|
||||
|
||||
//
|
||||
// Hugging Face transcribes audio, but like embeddings, not through the router endpoint we
|
||||
// chat with: that one answers "/v1/audio/transcriptions" with a plain "Not Found". Only
|
||||
// some of the inference providers answer the OpenAI-compatible form, which
|
||||
// HFInferenceProviderExtensions.SupportsTranscription decides.
|
||||
//
|
||||
LLMProviders.HUGGINGFACE => true,
|
||||
|
||||
//
|
||||
// Self-hosted providers are treated as a special case anyway.
|
||||
//
|
||||
@@ -264,7 +278,7 @@ public static class LLMProvidersExtensions
|
||||
/// <returns>The provider instance.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -274,7 +288,7 @@ public static class LLMProvidersExtensions
|
||||
/// <returns>The provider instance.</returns>
|
||||
public static IProvider CreateProvider(this EmbeddingProvider embeddingProviderSettings)
|
||||
{
|
||||
return embeddingProviderSettings.UsedLLMProvider.CreateProvider(embeddingProviderSettings.Name, embeddingProviderSettings.Host, embeddingProviderSettings.Hostname, embeddingProviderSettings.Model, HFInferenceProvider.NONE, configuredProviderId: embeddingProviderSettings.Id, isEnterpriseConfiguration: embeddingProviderSettings.IsEnterpriseConfiguration);
|
||||
return embeddingProviderSettings.UsedLLMProvider.CreateProvider(embeddingProviderSettings.Name, embeddingProviderSettings.Host, embeddingProviderSettings.Hostname, embeddingProviderSettings.HFInferenceProvider, configuredProviderId: embeddingProviderSettings.Id, isEnterpriseConfiguration: embeddingProviderSettings.IsEnterpriseConfiguration, hfEndpointKind: HFEndpointKind.EMBEDDING);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -284,10 +298,10 @@ public static class LLMProvidersExtensions
|
||||
/// <returns>The provider instance.</returns>
|
||||
public static IProvider CreateProvider(this TranscriptionProvider transcriptionProviderSettings)
|
||||
{
|
||||
return transcriptionProviderSettings.UsedLLMProvider.CreateProvider(transcriptionProviderSettings.Name, transcriptionProviderSettings.Host, transcriptionProviderSettings.Hostname, transcriptionProviderSettings.Model, HFInferenceProvider.NONE, configuredProviderId: transcriptionProviderSettings.Id, isEnterpriseConfiguration: transcriptionProviderSettings.IsEnterpriseConfiguration);
|
||||
return transcriptionProviderSettings.UsedLLMProvider.CreateProvider(transcriptionProviderSettings.Name, transcriptionProviderSettings.Host, transcriptionProviderSettings.Hostname, transcriptionProviderSettings.HFInferenceProvider, configuredProviderId: transcriptionProviderSettings.Id, isEnterpriseConfiguration: transcriptionProviderSettings.IsEnterpriseConfiguration, hfEndpointKind: HFEndpointKind.TRANSCRIPTION);
|
||||
}
|
||||
|
||||
private static IProvider CreateProvider(this LLMProviders provider, string instanceName, Host host, string hostname, Model model, HFInferenceProvider inferenceProvider, string configuredProviderId = "", string expertProviderApiParameter = "", bool isEnterpriseConfiguration = false)
|
||||
private static IProvider CreateProvider(this LLMProviders provider, string instanceName, Host host, string hostname, HFInferenceProvider inferenceProvider, string configuredProviderId = "", string expertProviderApiParameter = "", bool isEnterpriseConfiguration = false, HFEndpointKind hfEndpointKind = HFEndpointKind.CHAT)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -308,7 +322,7 @@ public static class LLMProvidersExtensions
|
||||
|
||||
LLMProviders.GROQ => new ProviderGroq { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
|
||||
LLMProviders.FIREWORKS => new ProviderFireworks { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
|
||||
LLMProviders.HUGGINGFACE => new ProviderHuggingFace(inferenceProvider, model) { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
|
||||
LLMProviders.HUGGINGFACE => new ProviderHuggingFace(inferenceProvider, hfEndpointKind) { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
|
||||
|
||||
LLMProviders.SELF_HOSTED => new ProviderSelfHosted(host, hostname) { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
|
||||
|
||||
@@ -392,14 +406,13 @@ public static class LLMProvidersExtensions
|
||||
public static string GetModelsOverviewURL(this LLMProviders provider, HFInferenceProvider inferenceProvider) => provider switch
|
||||
{
|
||||
LLMProviders.FIREWORKS => "https://fireworks.ai/models?show=Serverless",
|
||||
LLMProviders.HUGGINGFACE => $"https://huggingface.co/models?inference_provider={inferenceProvider.EndpointsId()}",
|
||||
LLMProviders.HUGGINGFACE => $"https://huggingface.co/models?inference_provider={inferenceProvider.CatalogFilter()}",
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
public static bool IsLLMModelProvidedManually(this LLMProviders provider) => provider switch
|
||||
{
|
||||
LLMProviders.FIREWORKS => true,
|
||||
LLMProviders.HUGGINGFACE => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
|
||||
@@ -46,7 +46,16 @@ public static class ModelKindExtensions
|
||||
|
||||
private static readonly string[] IMAGE_GENERATION_MARKERS = ["flux", "stable-diffusion", "sdxl", "dall-e", "midjourney", "gpt-image"];
|
||||
|
||||
private static readonly string[] VIDEO_GENERATION_MARKERS = ["sora", "veo-", "kling", "runway"];
|
||||
private static readonly string[] VIDEO_GENERATION_MARKERS = ["sora", "veo-", "runway"];
|
||||
|
||||
//
|
||||
// Markers which have to stand as a word of their own. "kling" is such a case: taken as a plain
|
||||
// substring, it also matches the organization "Klingspor", the model "Inkling", and the
|
||||
// fine-tune "Llama-2-7b-chat-klingon" -- all of them models to chat with, which would vanish
|
||||
// from the user's list. The video models themselves are named "kling-v1" or "kling-video",
|
||||
// where the name ends at a separator.
|
||||
//
|
||||
private static readonly string[] VIDEO_GENERATION_WORD_MARKERS = ["kling"];
|
||||
|
||||
//
|
||||
// Voxtral is marketed as an audio model which understands speech, so one could expect it to work
|
||||
@@ -101,7 +110,7 @@ public static class ModelKindExtensions
|
||||
if (HasAnyMarker(model.Id, IMAGE_GENERATION_MARKERS))
|
||||
return ModelKind.IMAGE_GENERATION;
|
||||
|
||||
if (HasAnyMarker(model.Id, VIDEO_GENERATION_MARKERS))
|
||||
if (HasAnyMarker(model.Id, VIDEO_GENERATION_MARKERS) || HasAnyWordMarker(model.Id, VIDEO_GENERATION_WORD_MARKERS))
|
||||
return ModelKind.VIDEO_GENERATION;
|
||||
|
||||
if (HasAnyMarker(model.Id, REALTIME_MARKERS))
|
||||
@@ -158,4 +167,53 @@ public static class ModelKindExtensions
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the model name contains one of the markers as a word of its own.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
/// <param name="modelId">The ID of the model.</param>
|
||||
/// <param name="markers">The markers to look for.</param>
|
||||
/// <returns>True, when one of the markers stands as a word of its own.</returns>
|
||||
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]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The characters which separate the parts of a model name, such as in "fal-ai/kling-video".
|
||||
/// </summary>
|
||||
/// <param name="character">The character to check.</param>
|
||||
/// <returns>True, when the character separates two parts of a name.</returns>
|
||||
private static bool IsSeparator(char character) => character is '/' or '-' or '_' or '.' or ' ' or ':';
|
||||
}
|
||||
@@ -5,4 +5,13 @@ public enum ProviderRequestFailureReason
|
||||
NONE,
|
||||
INSUFFICIENT_QUOTA,
|
||||
TOO_MANY_REQUESTS,
|
||||
|
||||
/// <summary>
|
||||
/// The provider does not serve the requested model.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This applies to gateways which route to other providers: the model exists, but the one
|
||||
/// meant to answer for it does not offer it.
|
||||
/// </remarks>
|
||||
MODEL_NOT_SUPPORTED_BY_PROVIDER,
|
||||
}
|
||||
Reference in new issue
Block a user