mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-14 04:33: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
@@ -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<EmbeddingProvider> LOGGER = Program.LOGGER_FACTORY.CreateLogger<EmbeddingProvider>();
|
||||
|
||||
@@ -103,6 +105,16 @@ public sealed record EmbeddingProvider(
|
||||
if (table.TryGetValue("AllowUserProvidedAPIKey", out var allowUserProvidedApiKeyValue) && allowUserProvidedApiKeyValue.TryRead<bool>(out var allowUserProvidedApiKeyBool))
|
||||
allowUserProvidedApiKey = allowUserProvidedApiKeyBool;
|
||||
|
||||
var hfInferenceProvider = HFInferenceProvider.NONE;
|
||||
if (table.TryGetValue("HFInferenceProvider", out var hfInferenceProviderValue) && hfInferenceProviderValue.TryRead<string>(out var hfInferenceProviderText))
|
||||
{
|
||||
if (!Enum.TryParse(hfInferenceProviderText, true, out hfInferenceProvider))
|
||||
{
|
||||
LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid Hugging Face inference provider enum value. (Plugin ID: {configPluginId})");
|
||||
hfInferenceProvider = HFInferenceProvider.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
var customIconDataUrl = string.Empty;
|
||||
if (table.TryGetValue("IconPath", out var iconPathValue))
|
||||
{
|
||||
@@ -126,6 +138,7 @@ public sealed record EmbeddingProvider(
|
||||
Host = host,
|
||||
AllowUserProvidedAPIKey = allowUserProvidedApiKey,
|
||||
CustomIconDataUrl = customIconDataUrl,
|
||||
HFInferenceProvider = hfInferenceProvider,
|
||||
};
|
||||
|
||||
// Handle an encrypted API key if present. When the user manages their own key for this
|
||||
@@ -192,6 +205,14 @@ public sealed record EmbeddingProvider(
|
||||
/// <returns>A Lua configuration section string.</returns>
|
||||
public string ExportAsConfigurationSection(string? encryptedApiKey = null)
|
||||
{
|
||||
var hfInferenceProviderLine = string.Empty;
|
||||
if (this.HFInferenceProvider is not HFInferenceProvider.NONE)
|
||||
{
|
||||
hfInferenceProviderLine = $"""
|
||||
["HFInferenceProvider"] = "{this.HFInferenceProvider}",
|
||||
""";
|
||||
}
|
||||
|
||||
var apiKeyLine = string.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(encryptedApiKey))
|
||||
{
|
||||
@@ -205,9 +226,10 @@ public sealed record EmbeddingProvider(
|
||||
["Id"] = "{{Guid.NewGuid().ToString()}}",
|
||||
["Name"] = "{{LuaTools.EscapeLuaString(this.Name)}}",
|
||||
["UsedLLMProvider"] = "{{this.UsedLLMProvider}}",
|
||||
|
||||
|
||||
["Host"] = "{{this.Host}}",
|
||||
["Hostname"] = "{{LuaTools.EscapeLuaString(this.Hostname)}}",
|
||||
{{hfInferenceProviderLine}}
|
||||
{{apiKeyLine}}
|
||||
["Model"] = {
|
||||
["Id"] = "{{LuaTools.EscapeLuaString(this.Model.Id)}}",
|
||||
|
||||
+20
-10
@@ -4,16 +4,26 @@ namespace AIStudio.Settings;
|
||||
|
||||
public static partial class ProviderExtensions
|
||||
{
|
||||
private static List<Capability> GetModelCapabilitiesOpenRouter(Model model)
|
||||
/// <summary>
|
||||
/// Determines the capabilities of a model offered through a gateway.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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".
|
||||
/// </remarks>
|
||||
/// <param name="model">The model as the gateway names it.</param>
|
||||
/// <returns>The capabilities of the model when reached through a gateway.</returns>
|
||||
private static List<Capability> GetModelCapabilitiesGateway(Model model)
|
||||
{
|
||||
//
|
||||
// OpenRouter model IDs follow the pattern "vendor/model-name". Examples:
|
||||
// Model IDs follow the pattern "vendor/model-name". Examples:
|
||||
// - openai/gpt-5.6
|
||||
// - anthropic/claude-opus-5
|
||||
// - google/gemini-3.7-flash
|
||||
// - qwen/qwen3.8-flash-next
|
||||
//
|
||||
// OpenRouter offers the models of all the other providers. Instead of keeping a
|
||||
// A gateway offers the models of all the other providers. Instead of keeping a
|
||||
// second set of rules here, which would always lag behind, we hand the model
|
||||
// over to the provider implementation which already knows it. The vendor prefix
|
||||
// has to be removed first: some of those implementations match the beginning of
|
||||
@@ -41,26 +51,26 @@ public static partial class ProviderExtensions
|
||||
"perplexity" => GetModelCapabilitiesPerplexity(bareModel),
|
||||
|
||||
// Everything else is open source: Qwen, Llama, GLM, Kimi, Muse, Hunyuan,
|
||||
// Nemotron, Grok, and whatever OpenRouter adds next. DeepSeek belongs here
|
||||
// Nemotron, Grok, and whatever a gateway adds next. DeepSeek belongs here
|
||||
// as well: its own implementation covers the aliases of the DeepSeek
|
||||
// platform, while OpenRouter uses the names of the open weights.
|
||||
// platform, while the gateways use the names of the open weights.
|
||||
_ => GetModelCapabilitiesOpenSource(bareModel),
|
||||
};
|
||||
|
||||
return NormalizeForOpenRouter(capabilities);
|
||||
return NormalizeForGateway(capabilities);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adjusts the capabilities reported by another provider for use through OpenRouter.
|
||||
/// Adjusts the capabilities reported by another provider for use through a gateway.
|
||||
/// </summary>
|
||||
/// <param name="capabilities">The capabilities as reported by the provider implementation.</param>
|
||||
/// <returns>The capabilities as they apply when using the model through OpenRouter.</returns>
|
||||
/// <returns>The capabilities as they apply when using the model through a gateway.</returns>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
private static List<Capability> NormalizeForOpenRouter(List<Capability> capabilities)
|
||||
private static List<Capability> NormalizeForGateway(List<Capability> capabilities)
|
||||
{
|
||||
capabilities.Remove(Capability.RESPONSES_API);
|
||||
if(!capabilities.Contains(Capability.CHAT_COMPLETION_API))
|
||||
@@ -462,6 +462,84 @@ public static partial class ProviderExtensions
|
||||
Capability.CHAT_COMPLETION_API,
|
||||
];
|
||||
|
||||
//
|
||||
// Google Gemma models. Gemma is the open-weights family, while Gemini is not, which is why
|
||||
// Gemma is handled here and Gemini in the Google implementation.
|
||||
//
|
||||
if (modelName.IndexOf("gemma") is not -1)
|
||||
{
|
||||
//
|
||||
// Every checkpoint of the Gemma 4 generation is multimodal and understands video as
|
||||
// well; there is no text-only variant. Audio input is limited to the E2B, E4B, and 12B
|
||||
// checkpoints. The models can think, but only when the request asks them to: their chat
|
||||
// template keeps the thinking channel closed by default.
|
||||
//
|
||||
if (modelName.IndexOf("gemma-4") is not -1 ||
|
||||
modelName.IndexOf("gemma4") is not -1 ||
|
||||
modelName.IndexOf("gemma 4") is not -1)
|
||||
{
|
||||
if (modelName.IndexOf("e2b") is not -1 ||
|
||||
modelName.IndexOf("e4b") is not -1 ||
|
||||
modelName.IndexOf("12b") is not -1)
|
||||
return
|
||||
[
|
||||
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
|
||||
Capability.AUDIO_INPUT, Capability.VIDEO_INPUT,
|
||||
Capability.TEXT_OUTPUT,
|
||||
|
||||
Capability.OPTIONAL_REASONING, Capability.FUNCTION_CALLING,
|
||||
Capability.CHAT_COMPLETION_API,
|
||||
];
|
||||
|
||||
return
|
||||
[
|
||||
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
|
||||
Capability.VIDEO_INPUT,
|
||||
Capability.TEXT_OUTPUT,
|
||||
|
||||
Capability.OPTIONAL_REASONING, Capability.FUNCTION_CALLING,
|
||||
Capability.CHAT_COMPLETION_API,
|
||||
];
|
||||
}
|
||||
|
||||
//
|
||||
// Gemma 3 accepts images from the 4B checkpoint upwards; the 1B one is text-only. This
|
||||
// generation does not reason. The check for the small checkpoint looks for "-1b" rather
|
||||
// than "1b", so that a name such as gemma-3-31b does not match it.
|
||||
//
|
||||
if (modelName.IndexOf("gemma-3") is not -1 ||
|
||||
modelName.IndexOf("gemma3") is not -1 ||
|
||||
modelName.IndexOf("gemma 3") is not -1)
|
||||
{
|
||||
if (modelName.IndexOf("-1b") is not -1)
|
||||
return
|
||||
[
|
||||
Capability.TEXT_INPUT, Capability.TEXT_OUTPUT,
|
||||
|
||||
Capability.FUNCTION_CALLING,
|
||||
Capability.CHAT_COMPLETION_API,
|
||||
];
|
||||
|
||||
return
|
||||
[
|
||||
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
|
||||
Capability.TEXT_OUTPUT,
|
||||
|
||||
Capability.FUNCTION_CALLING,
|
||||
Capability.CHAT_COMPLETION_API,
|
||||
];
|
||||
}
|
||||
|
||||
//
|
||||
// The earlier generations take text only and were not built for tool usage:
|
||||
//
|
||||
return
|
||||
[
|
||||
Capability.TEXT_INPUT, Capability.TEXT_OUTPUT,
|
||||
Capability.CHAT_COMPLETION_API,
|
||||
];
|
||||
}
|
||||
|
||||
//
|
||||
// Z AI / GLM models:
|
||||
//
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Provider.HuggingFace;
|
||||
|
||||
namespace AIStudio.Settings;
|
||||
|
||||
@@ -53,21 +54,27 @@ public static partial class ProviderExtensions
|
||||
LLMProviders.DEEP_SEEK => GetModelCapabilitiesDeepSeek(model),
|
||||
LLMProviders.ALIBABA_CLOUD => GetModelCapabilitiesAlibaba(model),
|
||||
LLMProviders.PERPLEXITY => GetModelCapabilitiesPerplexity(model),
|
||||
LLMProviders.OPEN_ROUTER => GetModelCapabilitiesOpenRouter(model),
|
||||
LLMProviders.OPEN_ROUTER => GetModelCapabilitiesGateway(model),
|
||||
LLMProviders.HETZNER or LLMProviders.IONOS => GetModelCapabilitiesOpenSource(model),
|
||||
|
||||
//
|
||||
// LiteLLM is a gateway just like OpenRouter, and it names its models the same way:
|
||||
// "vendor/model", e.g. "anthropic/claude-opus-5" or "azure/gpt-5.6". So we let the
|
||||
// OpenRouter detection handle it, which resolves the vendor prefix and asks the
|
||||
// gateway detection handle it, which resolves the vendor prefix and asks the
|
||||
// provider who really knows the model. Everything it cannot place is treated as
|
||||
// an open source model, which is the right fallback for a freely named alias:
|
||||
//
|
||||
LLMProviders.LITE_LLM => GetModelCapabilitiesOpenRouter(model),
|
||||
LLMProviders.LITE_LLM => GetModelCapabilitiesGateway(model),
|
||||
|
||||
LLMProviders.GROQ => GetModelCapabilitiesOpenSource(model),
|
||||
LLMProviders.FIREWORKS => GetModelCapabilitiesOpenSource(model),
|
||||
LLMProviders.HUGGINGFACE => GetModelCapabilitiesOpenSource(model),
|
||||
LLMProviders.GROQ or LLMProviders.FIREWORKS => GetModelCapabilitiesOpenSource(model),
|
||||
|
||||
//
|
||||
// Hugging Face names its models the way the hub does, "org/model", which is the same
|
||||
// shape the other gateways use. So we let the gateway detection resolve the organization
|
||||
// and ask the provider implementation which really knows the model. The routing suffix
|
||||
// has to go first: it says which inference provider answers, not what the model is.
|
||||
//
|
||||
LLMProviders.HUGGINGFACE => GetModelCapabilitiesGateway(model.WithoutRoutingSuffix()),
|
||||
|
||||
LLMProviders.HELMHOLTZ => GetModelCapabilitiesOpenSource(model),
|
||||
LLMProviders.GWDG => GetModelCapabilitiesOpenSource(model),
|
||||
|
||||
@@ -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<TranscriptionProvider> LOGGER = Program.LOGGER_FACTORY.CreateLogger<TranscriptionProvider>();
|
||||
|
||||
@@ -103,6 +105,16 @@ public sealed record TranscriptionProvider(
|
||||
if (table.TryGetValue("AllowUserProvidedAPIKey", out var allowUserProvidedApiKeyValue) && allowUserProvidedApiKeyValue.TryRead<bool>(out var allowUserProvidedApiKeyBool))
|
||||
allowUserProvidedApiKey = allowUserProvidedApiKeyBool;
|
||||
|
||||
var hfInferenceProvider = HFInferenceProvider.NONE;
|
||||
if (table.TryGetValue("HFInferenceProvider", out var hfInferenceProviderValue) && hfInferenceProviderValue.TryRead<string>(out var hfInferenceProviderText))
|
||||
{
|
||||
if (!Enum.TryParse(hfInferenceProviderText, true, out hfInferenceProvider))
|
||||
{
|
||||
LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid Hugging Face inference provider enum value. (Plugin ID: {configPluginId})");
|
||||
hfInferenceProvider = HFInferenceProvider.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
var customIconDataUrl = string.Empty;
|
||||
if (table.TryGetValue("IconPath", out var iconPathValue))
|
||||
{
|
||||
@@ -126,6 +138,7 @@ public sealed record TranscriptionProvider(
|
||||
Host = host,
|
||||
AllowUserProvidedAPIKey = allowUserProvidedApiKey,
|
||||
CustomIconDataUrl = customIconDataUrl,
|
||||
HFInferenceProvider = hfInferenceProvider,
|
||||
};
|
||||
|
||||
// Handle an encrypted API key if present. When the user manages their own key for this
|
||||
@@ -192,6 +205,14 @@ public sealed record TranscriptionProvider(
|
||||
/// <returns>A Lua configuration section string.</returns>
|
||||
public string ExportAsConfigurationSection(string? encryptedApiKey = null)
|
||||
{
|
||||
var hfInferenceProviderLine = string.Empty;
|
||||
if (this.HFInferenceProvider is not HFInferenceProvider.NONE)
|
||||
{
|
||||
hfInferenceProviderLine = $"""
|
||||
["HFInferenceProvider"] = "{this.HFInferenceProvider}",
|
||||
""";
|
||||
}
|
||||
|
||||
var apiKeyLine = string.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(encryptedApiKey))
|
||||
{
|
||||
@@ -208,6 +229,7 @@ public sealed record TranscriptionProvider(
|
||||
|
||||
["Host"] = "{{this.Host}}",
|
||||
["Hostname"] = "{{LuaTools.EscapeLuaString(this.Hostname)}}",
|
||||
{{hfInferenceProviderLine}}
|
||||
{{apiKeyLine}}
|
||||
["Model"] = {
|
||||
["Id"] = "{{LuaTools.EscapeLuaString(this.Model.Id)}}",
|
||||
|
||||
Reference in new issue
Block a user