2025-02-26 13:22:01 +01:00
using System.Net.Http.Headers ;
using System.Runtime.CompilerServices ;
2026-04-14 13:39:11 +02:00
using System.Text.Json ;
2025-02-26 13:22:01 +01:00
using AIStudio.Chat ;
using AIStudio.Provider.OpenAI ;
using AIStudio.Settings ;
namespace AIStudio.Provider.Helmholtz ;
2026-05-31 18:46:54 +02:00
public sealed class ProviderHelmholtz () : BaseProvider ( LLMProviders . HELMHOLTZ , new Uri ( "https://api.helmholtz-blablador.fz-juelich.de/v1/" ), ExternalHttpTrustPolicy . SYSTEM_TRUST_ONLY , LOGGER )
2025-02-26 13:22:01 +01:00
{
2025-09-03 21:25:17 +02:00
private static readonly ILogger < ProviderHelmholtz > LOGGER = Program . LOGGER_FACTORY . CreateLogger < ProviderHelmholtz >();
2025-02-26 13:22:01 +01:00
#region Implementation of IProvider
/// <inheritdoc />
2026-06-20 15:55:09 +02:00
public override string Id => LLMProviders . HELMHOLTZ . ToSecretId ();
2025-02-26 13:22:01 +01:00
/// <inheritdoc />
public override string InstanceName { get ; set ; } = "Helmholtz Blablador" ;
2026-04-16 11:24:22 +02:00
/// <inheritdoc />
public override bool HasModelLoadingCapability => true ;
2025-02-26 13:22:01 +01:00
/// <inheritdoc />
2025-08-31 14:27:35 +02:00
public override async IAsyncEnumerable < ContentStreamChunk > StreamChatCompletion ( Model chatModel , ChatThread chatThread , SettingsManager settingsManager , [ EnumeratorCancellation ] CancellationToken token = default )
2025-02-26 13:22:01 +01:00
{
2026-04-13 13:33:17 +02:00
await foreach ( var content in this . StreamOpenAICompatibleChatCompletion < ChatCompletionAPIRequest , ChatCompletionDeltaStreamLine , ChatCompletionAnnotationStreamLine >(
"Helmholtz" ,
chatModel ,
chatThread ,
settingsManager ,
2026-09-04 15:48:07 +02:00
async ( systemPrompt , apiParameters , tools ) =>
2026-04-13 13:33:17 +02:00
{
// Build the list of messages:
2026-09-13 14:17:25 +02:00
var messages = await chatThread . Blocks . BuildMessagesUsingNestedImageUrlAsync ( this . CreateSettingsProvider ( chatModel ));
2026-04-13 13:33:17 +02:00
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 ,
2026-09-04 15:48:07 +02:00
Tools = tools ,
2026-04-13 13:33:17 +02:00
AdditionalApiParameters = apiParameters
};
},
token : token ))
2025-02-26 13:22:01 +01: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 16:02:28 +01:00
/// <inheritdoc />
2026-08-29 22:25:12 +02:00
public override async Task < TranscriptionResult > TranscribeAudioAsync ( Model transcriptionModel , string audioFilePath , SettingsManager settingsManager , CancellationToken token = default )
2026-01-11 16:02:28 +01:00
{
2026-08-29 22:25:12 +02:00
var requestedSecret = await Program . RUST_SERVICE . GetAPIKey ( this , SecretStoreType . TRANSCRIPTION_PROVIDER );
return await this . PerformStandardTranscriptionRequest ( requestedSecret , transcriptionModel , audioFilePath , token : token );
2026-01-11 16:02:28 +01:00
}
2026-02-20 15:32:54 +01:00
/// <inhertidoc />
public override async Task < IReadOnlyList < IReadOnlyList < float >>> EmbedTextAsync ( Model embeddingModel , SettingsManager settingsManager , CancellationToken token = default , params List < string > texts )
{
2026-06-10 21:01:27 +02:00
var requestedSecret = await Program . RUST_SERVICE . GetAPIKey ( this , SecretStoreType . EMBEDDING_PROVIDER );
2026-02-20 15:32:54 +01:00
return await this . PerformStandardTextEmbeddingRequest ( requestedSecret , embeddingModel , token : token , texts : texts );
}
2025-02-26 13:22:01 +01:00
/// <inheritdoc />
2026-04-14 13:39:11 +02:00
public override async Task < ModelLoadResult > GetTextModels ( string? apiKeyProvisional = null , CancellationToken token = default )
2025-02-26 13:22:01 +01:00
{
2026-09-04 15:48:07 +02:00
var result = await this . LoadModels ( SecretStoreType . LLM_PROVIDER , apiKeyProvisional , token );
2026-04-14 13:39:11 +02:00
return result with
{
Models =
[
2026-09-13 14:17:25 +02:00
..result.Models.Where(model => model.IsChatModel(this.Provider))
2026-04-14 13:39:11 +02:00
]
};
2025-02-26 13:22:01 +01:00
}
/// <inheritdoc />
2026-04-14 13:39:11 +02:00
public override Task < ModelLoadResult > GetImageModels ( string? apiKeyProvisional = null , CancellationToken token = default )
2025-02-26 13:22:01 +01:00
{
2026-04-14 13:39:11 +02:00
return Task . FromResult ( ModelLoadResult . FromModels ([]));
2025-02-26 13:22:01 +01:00
}
/// <inheritdoc />
2026-04-14 13:39:11 +02:00
public override async Task < ModelLoadResult > GetEmbeddingModels ( string? apiKeyProvisional = null , CancellationToken token = default )
2025-02-26 13:22:01 +01:00
{
2026-09-04 15:48:07 +02:00
var result = await this . LoadModels ( SecretStoreType . EMBEDDING_PROVIDER , apiKeyProvisional , token );
2026-04-14 13:39:11 +02:00
return result with
{
Models =
[
2026-09-13 14:17:25 +02:00
..result.Models.Where(model => model.IsEmbeddingModel(this.Provider))
2026-04-14 13:39:11 +02:00
]
};
2025-02-26 13:22:01 +01:00
}
2025-05-11 12:51:35 +02:00
2026-01-09 12:45:21 +01:00
/// <inheritdoc />
2026-08-29 22:25:12 +02:00
public override async Task < ModelLoadResult > GetTranscriptionModels ( string? apiKeyProvisional = null , CancellationToken token = default )
2026-01-09 12:45:21 +01:00
{
2026-09-04 15:48:07 +02:00
var result = await this . LoadModels ( SecretStoreType . TRANSCRIPTION_PROVIDER , apiKeyProvisional , token );
2026-08-29 22:25:12 +02:00
return result with
{
Models =
[
2026-09-13 14:17:25 +02:00
..result.Models.Where(model => model.IsTranscriptionModel(this.Provider))
2026-08-29 22:25:12 +02:00
]
};
2026-01-09 12:45:21 +01:00
}
2025-02-26 13:22:01 +01:00
#endregion
2026-09-04 15:48:07 +02:00
private async Task < ModelLoadResult > LoadModels ( SecretStoreType storeType , string? apiKeyProvisional , CancellationToken token )
2025-02-26 13:22:01 +01:00
{
2026-04-14 13:39:11 +02:00
var secretKey = await this . GetModelLoadingSecretKey ( storeType , apiKeyProvisional );
if ( string . IsNullOrWhiteSpace ( secretKey ))
return FailedModelLoadResult ( ModelLoadFailureReason . INVALID_OR_MISSING_API_KEY , "No API key available for model loading." );
2025-02-26 13:22:01 +01:00
2026-04-14 13:39:11 +02:00
try
{
2026-05-21 16:48:34 +02:00
using var request = new HttpRequestMessage ( HttpMethod . Get , "models" );
request . Headers . Authorization = new AuthenticationHeaderValue ( "Bearer" , secretKey );
using var response = await this . HttpClient . SendAsync ( request , token );
var body = await response . Content . ReadAsStringAsync ( token );
if (! response . IsSuccessStatusCode )
return FailedModelLoadResult ( GetDefaultModelLoadFailureReason ( response ), $"Status={(int)response.StatusCode} {response.ReasonPhrase}; Body='{body}'" );
try
{
var modelResponse = JsonSerializer . Deserialize < ModelsResponse >( body , JSON_SERIALIZER_OPTIONS );
return SuccessfulModelLoadResult ( modelResponse . Data );
}
catch ( JsonException e )
{
if ( body . Contains ( "API key" , StringComparison . InvariantCultureIgnoreCase ))
return FailedModelLoadResult ( ModelLoadFailureReason . INVALID_OR_MISSING_API_KEY , body );
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 FailedModelLoadResult ( ModelLoadFailureReason . INVALID_RESPONSE , body );
}
catch ( Exception e )
{
LOGGER . LogError ( e , "Unexpected error while loading models from Helmholtz API. Status Code: {StatusCode}. Reason: {ReasonPhrase}" , response . StatusCode , response . ReasonPhrase );
return FailedModelLoadResult ( ModelLoadFailureReason . UNKNOWN , e . Message );
}
2026-04-14 13:39:11 +02:00
}
2026-05-21 16:48:34 +02:00
catch ( Exception e ) when ( this . IsTimeoutException ( e , token ))
2026-04-14 13:39:11 +02:00
{
2026-05-21 16:48:34 +02:00
await this . SendTimeoutError ( "loading the available models" );
LOGGER . LogError ( e , "Timed out while loading models from Helmholtz provider '{ProviderInstanceName}'." , this . InstanceName );
return FailedModelLoadResult ( ModelLoadFailureReason . PROVIDER_UNAVAILABLE , e . Message );
2026-04-14 13:39:11 +02:00
}
2025-02-26 13:22:01 +01:00
}
2026-09-04 15:48:07 +02:00
}