Added Hetzner as an experimental LLM provider

This commit is contained in:
Thorsten Sommer 2026-08-12 14:26:05 +02:00
parent 2d0d938f3d
commit aa11e1c352
Signed by untrusted user who does not match committer: tsommer
GPG Key ID: 371BBA77A02C0108
10 changed files with 131 additions and 2 deletions

View File

@ -8896,6 +8896,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T1014558951"] = "The trust leve
-- You or your organization operate the LLM locally or within your trusted network. In terms of data processing and security, this is the best possible way.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T2124364471"] = "You or your organization operate the LLM locally or within your trusted network. In terms of data processing and security, this is the best possible way."
-- The provider operates its service in the EU and is subject to the **GDPR** (General Data Protection Regulation). It provides access to **open source models**. However, the service is currently **experimental**, and performance and availability are not guaranteed. We have no provider-specific information about whether submitted data is used for training.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T2930312134"] = "The provider operates its service in the EU and is subject to the **GDPR** (General Data Protection Regulation). It provides access to **open source models**. However, the service is currently **experimental**, and performance and availability are not guaranteed. We have no provider-specific information about whether submitted data is used for training."
-- The provider is located in the EU and is subject to the **GDPR** (General Data Protection Regulation). Additionally, the provider states that **your data is not used for training**.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T3010553924"] = "The provider is located in the EU and is subject to the **GDPR** (General Data Protection Regulation). Additionally, the provider states that **your data is not used for training**."

View File

@ -634,7 +634,7 @@ CONFIG["SETTINGS"] = {}
-- Configure a custom confidence scheme.
-- This is used when DataConfidence.ConfidenceScheme is set to CUSTOM.
-- Allowed provider keys are: OPEN_AI, ANTHROPIC, MISTRAL, GOOGLE, X, DEEP_SEEK, ALIBABA_CLOUD,
-- PERPLEXITY, OPEN_ROUTER, FIREWORKS, GROQ, HUGGINGFACE, SELF_HOSTED, HELMHOLTZ, GWDG
-- PERPLEXITY, OPEN_ROUTER, HETZNER, FIREWORKS, GROQ, HUGGINGFACE, SELF_HOSTED, HELMHOLTZ, GWDG
-- Allowed confidence values are: UNTRUSTED, VERY_LOW, LOW, MODERATE, MEDIUM, HIGH
--
-- Replaces, does not merge: a configuration with a higher priority replaces the whole
@ -651,6 +651,7 @@ CONFIG["SETTINGS"] = {}
-- ["ALIBABA_CLOUD"] = "LOW",
-- ["PERPLEXITY"] = "MODERATE",
-- ["OPEN_ROUTER"] = "MODERATE",
-- ["HETZNER"] = "HIGH",
-- ["FIREWORKS"] = "MODERATE",
-- ["GROQ"] = "MODERATE",
-- ["HUGGINGFACE"] = "MODERATE",

View File

@ -64,6 +64,12 @@ public sealed record Confidence
Level = ConfidenceLevel.MEDIUM,
Description = TB("The provider is located in the EU and is subject to the **GDPR** (General Data Protection Regulation). Additionally, the provider states that **your data is not used for training**."),
};
public static readonly Confidence GDPR_EXPERIMENTAL_OPEN_SOURCE = new()
{
Level = ConfidenceLevel.MEDIUM,
Description = TB("The provider operates its service in the EU and is subject to the **GDPR** (General Data Protection Regulation). It provides access to **open source models**. However, the service is currently **experimental**, and performance and availability are not guaranteed. We have no provider-specific information about whether submitted data is used for training."),
};
public static readonly Confidence SELF_HOSTED = new()
{

View File

@ -0,0 +1,93 @@
using System.Runtime.CompilerServices;
using AIStudio.Chat;
using AIStudio.Provider.OpenAI;
using AIStudio.Settings;
namespace AIStudio.Provider.Hetzner;
public sealed class ProviderHetzner() : BaseProvider(LLMProviders.HETZNER, new Uri("https://inference.hetzner.com/api/v1/"), ExternalHttpTrustPolicy.SYSTEM_TRUST_ONLY, LOGGER)
{
private static readonly ILogger<ProviderHetzner> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ProviderHetzner>();
#region Implementation of IProvider
/// <inheritdoc />
public override string Id => LLMProviders.HETZNER.ToSecretId();
/// <inheritdoc />
public override string InstanceName { get; set; } = "Hetzner (Experimental)";
/// <inheritdoc />
public override bool HasModelLoadingCapability => true;
/// <inheritdoc />
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
{
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>(
"Hetzner",
chatModel,
chatThread,
settingsManager,
async (systemPrompt, apiParameters) =>
{
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
return new ChatCompletionAPIRequest
{
Model = chatModel.Id,
Messages = [systemPrompt, ..messages],
Stream = true,
AdditionalApiParameters = apiParameters
};
},
token: token))
yield return content;
}
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc />
public override async IAsyncEnumerable<ImageURL> StreamImageCompletion(Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default)
{
yield break;
}
#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)
{
return Task.FromResult(TranscriptionResult.Failure());
}
/// <inheritdoc />
public override Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
{
return Task.FromResult<IReadOnlyList<IReadOnlyList<float>>>([]);
}
/// <inheritdoc />
public override Task<ModelLoadResult> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
{
return this.LoadModelsResponse<ModelsResponse>(SecretStoreType.LLM_PROVIDER, "models", modelResponse => modelResponse.Data, token, apiKeyProvisional);
}
/// <inheritdoc />
public override Task<ModelLoadResult> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
{
return Task.FromResult(ModelLoadResult.FromModels([]));
}
/// <inheritdoc />
public override Task<ModelLoadResult> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
{
return Task.FromResult(ModelLoadResult.FromModels([]));
}
/// <inheritdoc />
public override Task<ModelLoadResult> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
{
return Task.FromResult(ModelLoadResult.FromModels([]));
}
#endregion
}

View File

@ -16,6 +16,7 @@ public enum LLMProviders
ALIBABA_CLOUD = 12,
PERPLEXITY = 14,
OPEN_ROUTER = 15,
HETZNER = 16,
FIREWORKS = 5,
GROQ = 6,

View File

@ -6,6 +6,7 @@ using AIStudio.Provider.Google;
using AIStudio.Provider.Groq;
using AIStudio.Provider.GWDG;
using AIStudio.Provider.Helmholtz;
using AIStudio.Provider.Hetzner;
using AIStudio.Provider.HuggingFace;
using AIStudio.Provider.Mistral;
using AIStudio.Provider.OpenAI;
@ -56,6 +57,7 @@ public static class LLMProvidersExtensions
LLMProviders.ALIBABA_CLOUD => "Alibaba Cloud",
LLMProviders.PERPLEXITY => "Perplexity",
LLMProviders.OPEN_ROUTER => "OpenRouter",
LLMProviders.HETZNER => "Hetzner (Experimental)",
LLMProviders.GROQ => "Groq",
LLMProviders.FIREWORKS => "Fireworks.ai",
@ -91,6 +93,7 @@ public static class LLMProvidersExtensions
LLMProviders.ALIBABA_CLOUD => "Alibaba Cloud",
LLMProviders.PERPLEXITY => "Perplexity",
LLMProviders.OPEN_ROUTER => "OpenRouter",
LLMProviders.HETZNER => "Hetzner",
LLMProviders.GROQ => "Groq",
LLMProviders.FIREWORKS => "Fireworks.ai",
@ -144,6 +147,12 @@ public static class LLMProvidersExtensions
LLMProviders.OPEN_ROUTER => Confidence.USA_HUB.WithRegion("America, U.S.").WithSources("https://openrouter.ai/privacy", "https://openrouter.ai/terms").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
LLMProviders.HETZNER => Confidence.GDPR_EXPERIMENTAL_OPEN_SOURCE.WithRegion("Europe, Germany").WithSources(
"https://experiments.hetzner.com/docs/inference",
"https://www.hetzner.com/legal/privacy-policy/",
"https://www.hetzner.com/legal/terms-and-conditions/"
).WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
LLMProviders.SELF_HOSTED => Confidence.SELF_HOSTED.WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
LLMProviders.HELMHOLTZ => Confidence.GDPR_NO_TRAINING.WithRegion("Europe, Germany").WithSources("https://helmholtz.cloud/services/?serviceID=d7d5c597-a2f6-4bd1-b71e-4d6499d98570").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
@ -180,6 +189,7 @@ public static class LLMProvidersExtensions
LLMProviders.HUGGINGFACE => false,
LLMProviders.PERPLEXITY => false,
LLMProviders.OPEN_ROUTER => true,
LLMProviders.HETZNER => false,
//
// Self-hosted providers are treated as a special case anyway.
@ -209,6 +219,7 @@ public static class LLMProvidersExtensions
// Providers that do not support transcription:
//
LLMProviders.OPEN_ROUTER => false,
LLMProviders.HETZNER => false,
LLMProviders.GROQ => false,
LLMProviders.ANTHROPIC => false,
LLMProviders.X => false,
@ -271,6 +282,7 @@ public static class LLMProvidersExtensions
LLMProviders.ALIBABA_CLOUD => new ProviderAlibabaCloud { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.PERPLEXITY => new ProviderPerplexity { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.OPEN_ROUTER => new ProviderOpenRouter { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.HETZNER => new ProviderHetzner { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.GROQ => new ProviderGroq { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.FIREWORKS => new ProviderFireworks { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
@ -302,6 +314,7 @@ public static class LLMProvidersExtensions
LLMProviders.ALIBABA_CLOUD => "https://account.alibabacloud.com/register/intl_register.htm",
LLMProviders.PERPLEXITY => "https://www.perplexity.ai/account/api",
LLMProviders.OPEN_ROUTER => "https://openrouter.ai/keys",
LLMProviders.HETZNER => "https://experiments.hetzner.com",
LLMProviders.GROQ => "https://console.groq.com/",
LLMProviders.FIREWORKS => "https://fireworks.ai/login",
@ -327,6 +340,7 @@ public static class LLMProvidersExtensions
LLMProviders.PERPLEXITY => "https://www.perplexity.ai/account/api/",
LLMProviders.OPEN_ROUTER => "https://openrouter.ai/activity",
LLMProviders.HUGGINGFACE => "https://huggingface.co/settings/billing",
LLMProviders.HETZNER => "https://experiments.hetzner.com",
_ => string.Empty,
};
@ -345,6 +359,7 @@ public static class LLMProvidersExtensions
LLMProviders.PERPLEXITY => true,
LLMProviders.OPEN_ROUTER => true,
LLMProviders.HUGGINGFACE => true,
LLMProviders.HETZNER => true,
_ => false,
};
@ -422,6 +437,7 @@ public static class LLMProvidersExtensions
LLMProviders.ALIBABA_CLOUD => true,
LLMProviders.PERPLEXITY => true,
LLMProviders.OPEN_ROUTER => true,
LLMProviders.HETZNER => true,
LLMProviders.GROQ => true,
LLMProviders.FIREWORKS => true,
@ -445,6 +461,7 @@ public static class LLMProvidersExtensions
LLMProviders.ALIBABA_CLOUD => true,
LLMProviders.PERPLEXITY => true,
LLMProviders.OPEN_ROUTER => true,
LLMProviders.HETZNER => true,
LLMProviders.GROQ => true,
LLMProviders.FIREWORKS => true,

View File

@ -90,6 +90,7 @@ public static partial class ProviderExtensions
GetQwenReasoningState(parameters)),
LLMProviders.OPEN_ROUTER or
LLMProviders.HETZNER or
LLMProviders.X or
LLMProviders.DEEP_SEEK or
LLMProviders.GROQ or

View File

@ -54,6 +54,7 @@ public static partial class ProviderExtensions
LLMProviders.ALIBABA_CLOUD => GetModelCapabilitiesAlibaba(model),
LLMProviders.PERPLEXITY => GetModelCapabilitiesPerplexity(model),
LLMProviders.OPEN_ROUTER => GetModelCapabilitiesOpenRouter(model),
LLMProviders.HETZNER => GetModelCapabilitiesOpenSource(model),
LLMProviders.GROQ => GetModelCapabilitiesOpenSource(model),
LLMProviders.FIREWORKS => GetModelCapabilitiesOpenSource(model),

View File

@ -599,8 +599,9 @@ public sealed class SettingsManager
{
LLMProviders.SELF_HOSTED => ConfidenceLevel.HIGH,
LLMProviders.DEEP_SEEK => ConfidenceLevel.LOW,
LLMProviders.ALIBABA_CLOUD => ConfidenceLevel.LOW,
_ => ConfidenceLevel.MEDIUM,
_ => ConfidenceLevel.MEDIUM,
};
case ConfidenceSchemes.TRUST_USA:
@ -610,7 +611,9 @@ public sealed class SettingsManager
LLMProviders.MISTRAL => ConfidenceLevel.LOW,
LLMProviders.HELMHOLTZ => ConfidenceLevel.LOW,
LLMProviders.GWDG => ConfidenceLevel.LOW,
LLMProviders.HETZNER => ConfidenceLevel.LOW,
LLMProviders.DEEP_SEEK => ConfidenceLevel.LOW,
LLMProviders.ALIBABA_CLOUD => ConfidenceLevel.LOW,
_ => ConfidenceLevel.MEDIUM,
};
@ -622,6 +625,7 @@ public sealed class SettingsManager
LLMProviders.MISTRAL => ConfidenceLevel.MEDIUM,
LLMProviders.HELMHOLTZ => ConfidenceLevel.MEDIUM,
LLMProviders.GWDG => ConfidenceLevel.MEDIUM,
LLMProviders.HETZNER => ConfidenceLevel.MEDIUM,
_ => ConfidenceLevel.LOW,
};
@ -631,6 +635,7 @@ public sealed class SettingsManager
{
LLMProviders.SELF_HOSTED => ConfidenceLevel.HIGH,
LLMProviders.DEEP_SEEK => ConfidenceLevel.MEDIUM,
LLMProviders.ALIBABA_CLOUD => ConfidenceLevel.MEDIUM,
_ => ConfidenceLevel.LOW,
};

View File

@ -1,4 +1,5 @@
# v26.8.1, build 251 (2026-08-xx xx:xx UTC)
- Added Hetzner's experimental inference API as an LLM provider. It runs open-source models in the EU and supports text and image chats through its OpenAI-compatible API.
- Added a prototype Visual Briefing Assistant that turns documents, data, images, audio, and video into self-contained interactive HTML briefings. When you want to test it, you have to enable this preview feature in your app settings.
- Added organization-configurable defaults and visibility controls for the Visual Briefing Assistant.
- Added a share button for assistants, configurations, and language plugins. It uses the native share dialog on Windows and macOS. For Linux, we added an export option, which stores the plugin archive at a location of your choice. When you work on a translation for a new language, you can now hand your current state to testers or to us with one click.