2025-02-26 12:22:01 +00:00
using System.Net.Http.Headers ;
using System.Runtime.CompilerServices ;
2026-04-14 07:16:47 +00:00
using System.Text.Json ;
2025-02-26 12:22:01 +00:00
using AIStudio.Chat ;
using AIStudio.Provider.OpenAI ;
using AIStudio.Settings ;
namespace AIStudio.Provider.Helmholtz ;
2025-12-30 17:30:32 +00:00
public sealed class ProviderHelmholtz ( ) : BaseProvider ( LLMProviders . HELMHOLTZ , "https://api.helmholtz-blablador.fz-juelich.de/v1/" , LOGGER )
2025-02-26 12:22:01 +00:00
{
2025-09-03 19:25:17 +00:00
private static readonly ILogger < ProviderHelmholtz > LOGGER = Program . LOGGER_FACTORY . CreateLogger < ProviderHelmholtz > ( ) ;
2025-02-26 12:22:01 +00:00
#region Implementation of IProvider
/// <inheritdoc />
public override string Id = > LLMProviders . HELMHOLTZ . ToName ( ) ;
/// <inheritdoc />
public override string InstanceName { get ; set ; } = "Helmholtz Blablador" ;
/// <inheritdoc />
2025-08-31 12:27:35 +00:00
public override async IAsyncEnumerable < ContentStreamChunk > StreamChatCompletion ( Model chatModel , ChatThread chatThread , SettingsManager settingsManager , [ EnumeratorCancellation ] CancellationToken token = default )
2025-02-26 12:22:01 +00:00
{
2026-04-13 11:33:17 +00:00
await foreach ( var content in this . StreamOpenAICompatibleChatCompletion < ChatCompletionAPIRequest , ChatCompletionDeltaStreamLine , ChatCompletionAnnotationStreamLine > (
"Helmholtz" ,
chatModel ,
chatThread ,
settingsManager ,
async ( systemPrompt , apiParameters ) = >
{
// Build the list of messages:
var messages = await chatThread . Blocks . BuildMessagesUsingNestedImageUrlAsync ( this . Provider , chatModel ) ;
return new ChatCompletionAPIRequest
{
Model = chatModel . Id ,
// Build the messages:
// - First of all the system prompt
// - Then none-empty user and AI messages
Messages = [ systemPrompt , . . messages ] ,
Stream = true ,
AdditionalApiParameters = apiParameters
} ;
} ,
token : token ) )
2025-02-26 12:22:01 +00:00
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
2026-01-11 15:02:28 +00:00
/// <inheritdoc />
public override Task < string > TranscribeAudioAsync ( Model transcriptionModel , string audioFilePath , SettingsManager settingsManager , CancellationToken token = default )
{
return Task . FromResult ( string . Empty ) ;
}
2026-02-20 14:32:54 +00:00
/// <inhertidoc />
public override async Task < IReadOnlyList < IReadOnlyList < float > > > EmbedTextAsync ( Model embeddingModel , SettingsManager settingsManager , CancellationToken token = default , params List < string > texts )
{
var requestedSecret = await RUST_SERVICE . GetAPIKey ( this , SecretStoreType . EMBEDDING_PROVIDER ) ;
return await this . PerformStandardTextEmbeddingRequest ( requestedSecret , embeddingModel , token : token , texts : texts ) ;
}
2025-02-26 12:22:01 +00:00
/// <inheritdoc />
public override async Task < IEnumerable < Model > > GetTextModels ( string? apiKeyProvisional = null , CancellationToken token = default )
{
2026-01-11 15:02:28 +00:00
var models = await this . LoadModels ( SecretStoreType . LLM_PROVIDER , token , apiKeyProvisional ) ;
2025-02-26 12:22:01 +00:00
return models . Where ( model = > ! model . Id . StartsWith ( "text-" , StringComparison . InvariantCultureIgnoreCase ) & &
! model . Id . StartsWith ( "alias-embedding" , StringComparison . InvariantCultureIgnoreCase ) ) ;
}
/// <inheritdoc />
public override Task < IEnumerable < Model > > GetImageModels ( string? apiKeyProvisional = null , CancellationToken token = default )
{
return Task . FromResult ( Enumerable . Empty < Model > ( ) ) ;
}
/// <inheritdoc />
public override async Task < IEnumerable < Model > > GetEmbeddingModels ( string? apiKeyProvisional = null , CancellationToken token = default )
{
2026-01-11 15:02:28 +00:00
var models = await this . LoadModels ( SecretStoreType . EMBEDDING_PROVIDER , token , apiKeyProvisional ) ;
2025-02-26 12:22:01 +00:00
return models . Where ( model = >
model . Id . StartsWith ( "alias-embedding" , StringComparison . InvariantCultureIgnoreCase ) | |
model . Id . StartsWith ( "text-" , StringComparison . InvariantCultureIgnoreCase ) | |
model . Id . Contains ( "gritlm" , StringComparison . InvariantCultureIgnoreCase ) ) ;
}
2025-05-11 10:51:35 +00:00
2026-01-09 11:45:21 +00:00
/// <inheritdoc />
public override Task < IEnumerable < Model > > GetTranscriptionModels ( string? apiKeyProvisional = null , CancellationToken token = default )
{
return Task . FromResult ( Enumerable . Empty < Model > ( ) ) ;
}
2025-02-26 12:22:01 +00:00
#endregion
2026-01-11 15:02:28 +00:00
private async Task < IEnumerable < Model > > LoadModels ( SecretStoreType storeType , CancellationToken token , string? apiKeyProvisional = null )
2025-02-26 12:22:01 +00:00
{
var secretKey = apiKeyProvisional switch
{
not null = > apiKeyProvisional ,
2026-01-11 15:02:28 +00:00
_ = > await RUST_SERVICE . GetAPIKey ( this , storeType ) switch
2025-02-26 12:22:01 +00:00
{
{ Success : true } result = > await result . Secret . Decrypt ( ENCRYPTION ) ,
_ = > null ,
}
} ;
if ( secretKey is null )
return [ ] ;
using var request = new HttpRequestMessage ( HttpMethod . Get , "models" ) ;
request . Headers . Authorization = new AuthenticationHeaderValue ( "Bearer" , secretKey ) ;
using var response = await this . httpClient . SendAsync ( request , token ) ;
2026-04-14 07:16:47 +00:00
// Unfortunately, the Helmholtz API does not return a non-success status code when the API key is invalid. Instead, it returns a 200 OK with a body that contains an error message.
// Therefore, we have to check the body of the response to determine if the request was successful or not.
2025-02-26 12:22:01 +00:00
if ( ! response . IsSuccessStatusCode )
return [ ] ;
2026-04-14 07:16:47 +00:00
try
{
var modelResponse = await response . Content . ReadFromJsonAsync < ModelsResponse > ( token ) ;
return modelResponse . Data ;
}
catch ( JsonException e )
{
//
// We expect a JsonException to be thrown when the API key is invalid, because the body of the response will not
// be a valid JSON. Therefore, we catch this exception and show an appropriate error message to the user.
//
var body = await response . Content . ReadAsStringAsync ( token ) ;
if ( body . Contains ( "invalid API key" , StringComparison . InvariantCultureIgnoreCase ) | |
body . Contains ( "missing API key" , StringComparison . InvariantCultureIgnoreCase ) )
{
LOGGER . LogWarning ( "Invalid API key provided for provider {ProviderId}. The response body was: '{ResponseBody}'" , this . Id , body ) ;
return [ ] ;
}
LOGGER . LogError ( e , "Unexpected error while parsing models from Helmholtz API response. Status Code: {StatusCode}. Reason: {ReasonPhrase}. Response Body: '{ResponseBody}'" , response . StatusCode , response . ReasonPhrase , body ) ;
return [ ] ;
}
catch ( Exception e )
{
LOGGER . LogError ( e , "Unexpected error while loading models from Helmholtz API. Status Code: {StatusCode}. Reason: {ReasonPhrase}" , response . StatusCode , response . ReasonPhrase ) ;
return [ ] ;
}
2025-02-26 12:22:01 +00:00
}
2026-04-14 07:16:47 +00:00
}