From 42f4f46523529e97bee0951717070475acc7f555 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peer=20Sch=C3=BCtt?= <20603780+peerschuett@users.noreply.github.com> Date: Wed, 8 Apr 2026 14:52:25 +0200 Subject: [PATCH] First version of the refactoring of ChatRequests --- .../AlibabaCloud/ProviderAlibabaCloud.cs | 68 +++++---------- .../Provider/BaseProvider.cs | 72 ++++++++++++++++ .../Provider/DeepSeek/ProviderDeepSeek.cs | 68 +++++---------- .../Provider/Fireworks/ChatRequest.cs | 20 ----- .../Provider/Fireworks/ProviderFireworks.cs | 71 +++++----------- .../Provider/GWDG/ProviderGWDG.cs | 68 +++++---------- .../Provider/Google/ChatRequest.cs | 20 ----- .../Provider/Google/ProviderGoogle.cs | 68 +++++---------- .../Provider/Groq/ChatRequest.cs | 5 +- .../Provider/Groq/ProviderGroq.cs | 71 ++++++---------- .../Provider/Helmholtz/ProviderHelmholtz.cs | 68 +++++---------- .../HuggingFace/ProviderHuggingFace.cs | 71 +++++----------- .../Provider/Mistral/ProviderMistral.cs | 76 ++++++----------- .../Provider/OpenRouter/ProviderOpenRouter.cs | 78 +++++++---------- .../Provider/Perplexity/ProviderPerplexity.cs | 69 +++++---------- .../Provider/SelfHosted/ChatRequest.cs | 20 ----- .../Provider/SelfHosted/ProviderSelfHosted.cs | 83 +++++++------------ .../Provider/X/ProviderX.cs | 70 +++++----------- 18 files changed, 387 insertions(+), 679 deletions(-) delete mode 100644 app/MindWork AI Studio/Provider/Fireworks/ChatRequest.cs delete mode 100644 app/MindWork AI Studio/Provider/Google/ChatRequest.cs delete mode 100644 app/MindWork AI Studio/Provider/SelfHosted/ChatRequest.cs diff --git a/app/MindWork AI Studio/Provider/AlibabaCloud/ProviderAlibabaCloud.cs b/app/MindWork AI Studio/Provider/AlibabaCloud/ProviderAlibabaCloud.cs index 3535809d..7f2bf792 100644 --- a/app/MindWork AI Studio/Provider/AlibabaCloud/ProviderAlibabaCloud.cs +++ b/app/MindWork AI Studio/Provider/AlibabaCloud/ProviderAlibabaCloud.cs @@ -1,7 +1,5 @@ using System.Net.Http.Headers; using System.Runtime.CompilerServices; -using System.Text; -using System.Text.Json; using AIStudio.Chat; using AIStudio.Provider.OpenAI; @@ -24,52 +22,30 @@ public sealed class ProviderAlibabaCloud() : BaseProvider(LLMProviders.ALIBABA_C /// public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) { - // Get the API key: - var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER); - if(!requestedSecret.Success) - yield break; - - // Prepare the system prompt: - var systemPrompt = new TextMessage - { - Role = "system", - Content = chatThread.PrepareSystemPrompt(settingsManager), - }; - - // Parse the API parameters: - var apiParameters = this.ParseAdditionalApiParameters(); - - // Build the list of messages: - var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel); - - // Prepare the AlibabaCloud HTTP chat request: - var alibabaCloudChatRequest = JsonSerializer.Serialize(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 - }, JSON_SERIALIZER_OPTIONS); + await foreach (var content in this.StreamOpenAICompatibleChatCompletion( + "AlibabaCloud", + chatModel, + chatThread, + settingsManager, + async (systemPrompt, apiParameters) => + { + // Build the list of messages: + var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel); - async Task RequestBuilder() - { - // Build the HTTP post request: - var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); + return new ChatCompletionAPIRequest + { + Model = chatModel.Id, - // Set the authorization header: - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); + // Build the messages: + // - First of all the system prompt + // - Then none-empty user and AI messages + Messages = [systemPrompt, ..messages], - // Set the content: - request.Content = new StringContent(alibabaCloudChatRequest, Encoding.UTF8, "application/json"); - return request; - } - - await foreach (var content in this.StreamChatCompletionInternal("AlibabaCloud", RequestBuilder, token)) + Stream = true, + AdditionalApiParameters = apiParameters + }; + }, + token: token)) yield return content; } @@ -183,4 +159,4 @@ public sealed class ProviderAlibabaCloud() : BaseProvider(LLMProviders.ALIBABA_C return modelResponse.Data.Where(model => prefixes.Any(prefix => model.Id.StartsWith(prefix, StringComparison.InvariantCulture))); } -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Provider/BaseProvider.cs b/app/MindWork AI Studio/Provider/BaseProvider.cs index 9b729824..46e43843 100644 --- a/app/MindWork AI Studio/Provider/BaseProvider.cs +++ b/app/MindWork AI Studio/Provider/BaseProvider.cs @@ -565,6 +565,78 @@ public abstract class BaseProvider : IProvider, ISecretId streamReader.Dispose(); } + /// + /// Streams the chat completion from an OpenAI-compatible provider using the Chat Completion API. + /// + /// The provider name for logging and error reporting. + /// The selected chat model. + /// The current chat thread. + /// The settings manager. + /// Builds the provider-specific request body. + /// The secret store type. + /// Whether the API key is optional. + /// The system prompt role to use. + /// The request path, relative to the provider base URL. + /// Optional additional headers to add. + /// The cancellation token. + /// The request DTO type. + /// The delta stream line type. + /// The annotation stream line type. + /// The streamed content chunks. + protected async IAsyncEnumerable StreamOpenAICompatibleChatCompletion( + string providerName, + Model chatModel, + ChatThread chatThread, + SettingsManager settingsManager, + Func, Task> requestFactory, + SecretStoreType storeType = SecretStoreType.LLM_PROVIDER, + bool isTryingSecret = false, + string systemPromptRole = "system", + string requestPath = "chat/completions", + Action? headersAction = null, + [EnumeratorCancellation] CancellationToken token = default) + where TDelta : IResponseStreamLine + where TAnnotation : IAnnotationStreamLine + { + // Get the API key: + var requestedSecret = await RUST_SERVICE.GetAPIKey(this, storeType, isTrying: isTryingSecret); + if(!requestedSecret.Success && !isTryingSecret) + yield break; + + // Prepare the system prompt: + var systemPrompt = new TextMessage + { + Role = systemPromptRole, + Content = chatThread.PrepareSystemPrompt(settingsManager), + }; + + // Parse the API parameters: + var apiParameters = this.ParseAdditionalApiParameters(); + + // Prepare the provider HTTP chat request: + var providerChatRequest = JsonSerializer.Serialize(await requestFactory(systemPrompt, apiParameters), JSON_SERIALIZER_OPTIONS); + + async Task RequestBuilder() + { + // Build the HTTP post request: + var request = new HttpRequestMessage(HttpMethod.Post, requestPath); + + // Set the authorization header: + if (requestedSecret.Success) + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); + + // Set provider-specific headers: + headersAction?.Invoke(request.Headers); + + // Set the content: + request.Content = new StringContent(providerChatRequest, Encoding.UTF8, "application/json"); + return request; + } + + await foreach (var content in this.StreamChatCompletionInternal(providerName, RequestBuilder, token)) + yield return content; + } + protected async Task PerformStandardTranscriptionRequest(RequestedSecret requestedSecret, Model transcriptionModel, string audioFilePath, Host host = Host.NONE, CancellationToken token = default) { try diff --git a/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs b/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs index e1ae306a..bc1e0806 100644 --- a/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs +++ b/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs @@ -1,7 +1,5 @@ using System.Net.Http.Headers; using System.Runtime.CompilerServices; -using System.Text; -using System.Text.Json; using AIStudio.Chat; using AIStudio.Provider.OpenAI; @@ -24,52 +22,30 @@ public sealed class ProviderDeepSeek() : BaseProvider(LLMProviders.DEEP_SEEK, "h /// public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) { - // Get the API key: - var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER); - if(!requestedSecret.Success) - yield break; - - // Prepare the system prompt: - var systemPrompt = new TextMessage - { - Role = "system", - Content = chatThread.PrepareSystemPrompt(settingsManager), - }; - - // Parse the API parameters: - var apiParameters = this.ParseAdditionalApiParameters(); - - // Build the list of messages: - var messages = await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.Provider, chatModel); - - // Prepare the DeepSeek HTTP chat request: - var deepSeekChatRequest = JsonSerializer.Serialize(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 - }, JSON_SERIALIZER_OPTIONS); + await foreach (var content in this.StreamOpenAICompatibleChatCompletion( + "DeepSeek", + chatModel, + chatThread, + settingsManager, + async (systemPrompt, apiParameters) => + { + // Build the list of messages: + var messages = await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.Provider, chatModel); - async Task RequestBuilder() - { - // Build the HTTP post request: - var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); + return new ChatCompletionAPIRequest + { + Model = chatModel.Id, - // Set the authorization header: - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); + // Build the messages: + // - First of all the system prompt + // - Then none-empty user and AI messages + Messages = [systemPrompt, ..messages], - // Set the content: - request.Content = new StringContent(deepSeekChatRequest, Encoding.UTF8, "application/json"); - return request; - } - - await foreach (var content in this.StreamChatCompletionInternal("DeepSeek", RequestBuilder, token)) + Stream = true, + AdditionalApiParameters = apiParameters + }; + }, + token: token)) yield return content; } @@ -144,4 +120,4 @@ public sealed class ProviderDeepSeek() : BaseProvider(LLMProviders.DEEP_SEEK, "h var modelResponse = await response.Content.ReadFromJsonAsync(token); return modelResponse.Data; } -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Provider/Fireworks/ChatRequest.cs b/app/MindWork AI Studio/Provider/Fireworks/ChatRequest.cs deleted file mode 100644 index 54963feb..00000000 --- a/app/MindWork AI Studio/Provider/Fireworks/ChatRequest.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Text.Json.Serialization; - -namespace AIStudio.Provider.Fireworks; - -/// -/// The Fireworks chat request model. -/// -/// Which model to use for chat completion. -/// The chat messages. -/// Whether to stream the chat completion. -public readonly record struct ChatRequest( - string Model, - IList Messages, - bool Stream -) -{ - // Attention: The "required" modifier is not supported for [JsonExtensionData]. - [JsonExtensionData] - public IDictionary AdditionalApiParameters { get; init; } = new Dictionary(); -} \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs b/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs index 2254b7ad..0091e7a1 100644 --- a/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs +++ b/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs @@ -1,7 +1,4 @@ -using System.Net.Http.Headers; using System.Runtime.CompilerServices; -using System.Text; -using System.Text.Json; using AIStudio.Chat; using AIStudio.Provider.OpenAI; @@ -24,53 +21,31 @@ public class ProviderFireworks() : BaseProvider(LLMProviders.FIREWORKS, "https:/ /// public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) { - // Get the API key: - var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER); - if(!requestedSecret.Success) - yield break; + await foreach (var content in this.StreamOpenAICompatibleChatCompletion( + "Fireworks", + chatModel, + chatThread, + settingsManager, + async (systemPrompt, apiParameters) => + { + // Build the list of messages: + var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel); - // Prepare the system prompt: - var systemPrompt = new TextMessage - { - Role = "system", - Content = chatThread.PrepareSystemPrompt(settingsManager), - }; - - // Parse the API parameters: - var apiParameters = this.ParseAdditionalApiParameters(); - - // Build the list of messages: - var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel); - - // Prepare the Fireworks HTTP chat request: - var fireworksChatRequest = JsonSerializer.Serialize(new ChatRequest - { - Model = chatModel.Id, - - // Build the messages: - // - First of all the system prompt - // - Then none-empty user and AI messages - Messages = [systemPrompt, ..messages], - - // Right now, we only support streaming completions: - Stream = true, - AdditionalApiParameters = apiParameters - }, JSON_SERIALIZER_OPTIONS); + return new ChatCompletionAPIRequest + { + Model = chatModel.Id, - async Task RequestBuilder() - { - // Build the HTTP post request: - var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); + // Build the messages: + // - First of all the system prompt + // - Then none-empty user and AI messages + Messages = [systemPrompt, ..messages], - // Set the authorization header: - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); - - // Set the content: - request.Content = new StringContent(fireworksChatRequest, Encoding.UTF8, "application/json"); - return request; - } - - await foreach (var content in this.StreamChatCompletionInternal("Fireworks", RequestBuilder, token)) + // Right now, we only support streaming completions: + Stream = true, + AdditionalApiParameters = apiParameters + }; + }, + token: token)) yield return content; } @@ -126,4 +101,4 @@ public class ProviderFireworks() : BaseProvider(LLMProviders.FIREWORKS, "https:/ } #endregion -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs b/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs index 41e19fa9..edae7ae9 100644 --- a/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs +++ b/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs @@ -1,7 +1,5 @@ using System.Net.Http.Headers; using System.Runtime.CompilerServices; -using System.Text; -using System.Text.Json; using AIStudio.Chat; using AIStudio.Provider.OpenAI; @@ -24,52 +22,30 @@ public sealed class ProviderGWDG() : BaseProvider(LLMProviders.GWDG, "https://ch /// public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) { - // Get the API key: - var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER); - if(!requestedSecret.Success) - yield break; - - // Prepare the system prompt: - var systemPrompt = new TextMessage - { - Role = "system", - Content = chatThread.PrepareSystemPrompt(settingsManager), - }; - - // Parse the API parameters: - var apiParameters = this.ParseAdditionalApiParameters(); - - // Build the list of messages: - var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel); - - // Prepare the GWDG HTTP chat request: - var gwdgChatRequest = JsonSerializer.Serialize(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 - }, JSON_SERIALIZER_OPTIONS); + await foreach (var content in this.StreamOpenAICompatibleChatCompletion( + "GWDG", + chatModel, + chatThread, + settingsManager, + async (systemPrompt, apiParameters) => + { + // Build the list of messages: + var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel); - async Task RequestBuilder() - { - // Build the HTTP post request: - var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); + return new ChatCompletionAPIRequest + { + Model = chatModel.Id, - // Set the authorization header: - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); + // Build the messages: + // - First of all the system prompt + // - Then none-empty user and AI messages + Messages = [systemPrompt, ..messages], - // Set the content: - request.Content = new StringContent(gwdgChatRequest, Encoding.UTF8, "application/json"); - return request; - } - - await foreach (var content in this.StreamChatCompletionInternal("GWDG", RequestBuilder, token)) + Stream = true, + AdditionalApiParameters = apiParameters + }; + }, + token: token)) yield return content; } @@ -152,4 +128,4 @@ public sealed class ProviderGWDG() : BaseProvider(LLMProviders.GWDG, "https://ch var modelResponse = await response.Content.ReadFromJsonAsync(token); return modelResponse.Data; } -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Provider/Google/ChatRequest.cs b/app/MindWork AI Studio/Provider/Google/ChatRequest.cs deleted file mode 100644 index 1a898c3a..00000000 --- a/app/MindWork AI Studio/Provider/Google/ChatRequest.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Text.Json.Serialization; - -namespace AIStudio.Provider.Google; - -/// -/// The Google chat request model. -/// -/// Which model to use for chat completion. -/// The chat messages. -/// Whether to stream the chat completion. -public readonly record struct ChatRequest( - string Model, - IList Messages, - bool Stream -) -{ - // Attention: The "required" modifier is not supported for [JsonExtensionData]. - [JsonExtensionData] - public IDictionary AdditionalApiParameters { get; init; } = new Dictionary(); -} \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs b/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs index 8a86fcbe..0caf7b05 100644 --- a/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs +++ b/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs @@ -24,53 +24,31 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, "https://gener /// public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) { - // Get the API key: - var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER); - if(!requestedSecret.Success) - yield break; + await foreach (var content in this.StreamOpenAICompatibleChatCompletion( + "Google", + chatModel, + chatThread, + settingsManager, + async (systemPrompt, apiParameters) => + { + // Build the list of messages: + var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel); - // Prepare the system prompt: - var systemPrompt = new TextMessage - { - Role = "system", - Content = chatThread.PrepareSystemPrompt(settingsManager), - }; - - // Parse the API parameters: - var apiParameters = this.ParseAdditionalApiParameters(); - - // Build the list of messages: - var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel); - - // Prepare the Google HTTP chat request: - var geminiChatRequest = JsonSerializer.Serialize(new ChatRequest - { - Model = chatModel.Id, - - // Build the messages: - // - First of all the system prompt - // - Then none-empty user and AI messages - Messages = [systemPrompt, ..messages], - - // Right now, we only support streaming completions: - Stream = true, - AdditionalApiParameters = apiParameters - }, JSON_SERIALIZER_OPTIONS); + return new ChatCompletionAPIRequest + { + Model = chatModel.Id, - async Task RequestBuilder() - { - // Build the HTTP post request: - var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); + // Build the messages: + // - First of all the system prompt + // - Then none-empty user and AI messages + Messages = [systemPrompt, ..messages], - // Set the authorization header: - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); - - // Set the content: - request.Content = new StringContent(geminiChatRequest, Encoding.UTF8, "application/json"); - return request; - } - - await foreach (var content in this.StreamChatCompletionInternal("Google", RequestBuilder, token)) + // Right now, we only support streaming completions: + Stream = true, + AdditionalApiParameters = apiParameters + }; + }, + token: token)) yield return content; } @@ -256,4 +234,4 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, "https://gener ? modelId["models/".Length..] : modelId; } -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Provider/Groq/ChatRequest.cs b/app/MindWork AI Studio/Provider/Groq/ChatRequest.cs index 2e7668f1..07ddce22 100644 --- a/app/MindWork AI Studio/Provider/Groq/ChatRequest.cs +++ b/app/MindWork AI Studio/Provider/Groq/ChatRequest.cs @@ -13,10 +13,11 @@ public readonly record struct ChatRequest( string Model, IList Messages, bool Stream, - int Seed + [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + int? Seed ) { // Attention: The "required" modifier is not supported for [JsonExtensionData]. [JsonExtensionData] public IDictionary AdditionalApiParameters { get; init; } = new Dictionary(); -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs b/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs index 8f938667..a3ca862d 100644 --- a/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs +++ b/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs @@ -1,7 +1,5 @@ using System.Net.Http.Headers; using System.Runtime.CompilerServices; -using System.Text; -using System.Text.Json; using AIStudio.Chat; using AIStudio.Provider.OpenAI; @@ -24,53 +22,34 @@ public class ProviderGroq() : BaseProvider(LLMProviders.GROQ, "https://api.groq. /// public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) { - // Get the API key: - var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER); - if(!requestedSecret.Success) - yield break; + await foreach (var content in this.StreamOpenAICompatibleChatCompletion( + "Groq", + chatModel, + chatThread, + settingsManager, + async (systemPrompt, apiParameters) => + { + var seed = TryPopIntParameter(apiParameters, "seed", out var parsedSeed) ? parsedSeed : (int?)null; - // Prepare the system prompt: - var systemPrompt = new TextMessage - { - Role = "system", - Content = chatThread.PrepareSystemPrompt(settingsManager), - }; - - // Parse the API parameters: - var apiParameters = this.ParseAdditionalApiParameters(); - - // Build the list of messages: - var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel); - - // Prepare the OpenAI HTTP chat request: - var groqChatRequest = JsonSerializer.Serialize(new ChatRequest - { - Model = chatModel.Id, - - // Build the messages: - // - First of all the system prompt - // - Then none-empty user and AI messages - Messages = [systemPrompt, ..messages], - - // Right now, we only support streaming completions: - Stream = true, - AdditionalApiParameters = apiParameters - }, JSON_SERIALIZER_OPTIONS); + // Build the list of messages: + var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel); - async Task RequestBuilder() - { - // Build the HTTP post request: - var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); + return new ChatRequest + { + Model = chatModel.Id, - // Set the authorization header: - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); + // Build the messages: + // - First of all the system prompt + // - Then none-empty user and AI messages + Messages = [systemPrompt, ..messages], - // Set the content: - request.Content = new StringContent(groqChatRequest, Encoding.UTF8, "application/json"); - return request; - } - - await foreach (var content in this.StreamChatCompletionInternal("Groq", RequestBuilder, token)) + // Right now, we only support streaming completions: + Stream = true, + Seed = seed, + AdditionalApiParameters = apiParameters + }; + }, + token: token)) yield return content; } @@ -148,4 +127,4 @@ public class ProviderGroq() : BaseProvider(LLMProviders.GROQ, "https://api.groq. !n.Id.StartsWith("distil-", StringComparison.OrdinalIgnoreCase) && !n.Id.Contains("-tts", StringComparison.OrdinalIgnoreCase)); } -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs b/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs index 070597a3..bfa7a758 100644 --- a/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs +++ b/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs @@ -1,7 +1,5 @@ using System.Net.Http.Headers; using System.Runtime.CompilerServices; -using System.Text; -using System.Text.Json; using AIStudio.Chat; using AIStudio.Provider.OpenAI; @@ -24,52 +22,30 @@ public sealed class ProviderHelmholtz() : BaseProvider(LLMProviders.HELMHOLTZ, " /// public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) { - // Get the API key: - var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER); - if(!requestedSecret.Success) - yield break; - - // Prepare the system prompt: - var systemPrompt = new TextMessage - { - Role = "system", - Content = chatThread.PrepareSystemPrompt(settingsManager), - }; - - // Parse the API parameters: - var apiParameters = this.ParseAdditionalApiParameters(); - - // Build the list of messages: - var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel); - - // Prepare the Helmholtz HTTP chat request: - var helmholtzChatRequest = JsonSerializer.Serialize(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 - }, JSON_SERIALIZER_OPTIONS); + await foreach (var content in this.StreamOpenAICompatibleChatCompletion( + "Helmholtz", + chatModel, + chatThread, + settingsManager, + async (systemPrompt, apiParameters) => + { + // Build the list of messages: + var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel); - async Task RequestBuilder() - { - // Build the HTTP post request: - var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); + return new ChatCompletionAPIRequest + { + Model = chatModel.Id, - // Set the authorization header: - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); + // Build the messages: + // - First of all the system prompt + // - Then none-empty user and AI messages + Messages = [systemPrompt, ..messages], - // Set the content: - request.Content = new StringContent(helmholtzChatRequest, Encoding.UTF8, "application/json"); - return request; - } - - await foreach (var content in this.StreamChatCompletionInternal("Helmholtz", RequestBuilder, token)) + Stream = true, + AdditionalApiParameters = apiParameters + }; + }, + token: token)) yield return content; } @@ -151,4 +127,4 @@ public sealed class ProviderHelmholtz() : BaseProvider(LLMProviders.HELMHOLTZ, " var modelResponse = await response.Content.ReadFromJsonAsync(token); return modelResponse.Data; } -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs b/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs index f2e8c380..c22b5c50 100644 --- a/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs +++ b/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs @@ -1,7 +1,4 @@ -using System.Net.Http.Headers; -using System.Runtime.CompilerServices; -using System.Text; -using System.Text.Json; +using System.Runtime.CompilerServices; using AIStudio.Chat; using AIStudio.Provider.OpenAI; @@ -29,52 +26,30 @@ public sealed class ProviderHuggingFace : BaseProvider /// public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) { - // Get the API key: - var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER); - if(!requestedSecret.Success) - yield break; + await foreach (var content in this.StreamOpenAICompatibleChatCompletion( + "HuggingFace", + chatModel, + chatThread, + settingsManager, + async (systemPrompt, apiParameters) => + { + // Build the list of messages: + var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel); - // Prepare the system prompt: - var systemPrompt = new TextMessage - { - Role = "system", - Content = chatThread.PrepareSystemPrompt(settingsManager), - }; - - // Parse the API parameters: - var apiParameters = this.ParseAdditionalApiParameters(); - - // Build the list of messages: - var message = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel); - - // Prepare the HuggingFace HTTP chat request: - var huggingfaceChatRequest = JsonSerializer.Serialize(new ChatCompletionAPIRequest - { - Model = chatModel.Id, - - // Build the messages: - // - First of all the system prompt - // - Then none-empty user and AI messages - Messages = [systemPrompt, ..message], - - Stream = true, - AdditionalApiParameters = apiParameters - }, JSON_SERIALIZER_OPTIONS); + return new ChatCompletionAPIRequest + { + Model = chatModel.Id, - async Task RequestBuilder() - { - // Build the HTTP post request: - var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); + // Build the messages: + // - First of all the system prompt + // - Then none-empty user and AI messages + Messages = [systemPrompt, ..messages], - // Set the authorization header: - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); - - // Set the content: - request.Content = new StringContent(huggingfaceChatRequest, Encoding.UTF8, "application/json"); - return request; - } - - await foreach (var content in this.StreamChatCompletionInternal("HuggingFace", RequestBuilder, token)) + Stream = true, + AdditionalApiParameters = apiParameters + }; + }, + token: token)) yield return content; } @@ -123,4 +98,4 @@ public sealed class ProviderHuggingFace : BaseProvider } #endregion -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs index 485729fb..b40f6657 100644 --- a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs +++ b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs @@ -1,7 +1,5 @@ using System.Net.Http.Headers; using System.Runtime.CompilerServices; -using System.Text; -using System.Text.Json; using AIStudio.Chat; using AIStudio.Provider.OpenAI; @@ -22,58 +20,36 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, "http /// public override async IAsyncEnumerable StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) { - // Get the API key: - var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER); - if(!requestedSecret.Success) - yield break; + await foreach (var content in this.StreamOpenAICompatibleChatCompletion( + "Mistral", + chatModel, + chatThread, + settingsManager, + async (systemPrompt, apiParameters) => + { + var safePrompt = TryPopBoolParameter(apiParameters, "safe_prompt", out var parsedSafePrompt) && parsedSafePrompt; + var randomSeed = TryPopIntParameter(apiParameters, "random_seed", out var parsedRandomSeed) ? parsedRandomSeed : (int?)null; - // Prepare the system prompt: - var systemPrompt = new TextMessage - { - Role = "system", - Content = chatThread.PrepareSystemPrompt(settingsManager), - }; - - // Parse the API parameters: - var apiParameters = this.ParseAdditionalApiParameters(); - var safePrompt = TryPopBoolParameter(apiParameters, "safe_prompt", out var parsedSafePrompt) && parsedSafePrompt; - var randomSeed = TryPopIntParameter(apiParameters, "random_seed", out var parsedRandomSeed) ? parsedRandomSeed : (int?)null; + // Build the list of messages: + var messages = await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.Provider, chatModel); - // Build the list of messages: - var messages = await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.Provider, chatModel); - - // Prepare the Mistral HTTP chat request: - var mistralChatRequest = JsonSerializer.Serialize(new ChatRequest - { - Model = chatModel.Id, - - // Build the messages: - // - First of all the system prompt - // - Then none-empty user and AI messages - Messages = [systemPrompt, ..messages], - - // Right now, we only support streaming completions: - Stream = true, - RandomSeed = randomSeed, - SafePrompt = safePrompt, - AdditionalApiParameters = apiParameters - }, JSON_SERIALIZER_OPTIONS); + return new ChatRequest + { + Model = chatModel.Id, - - async Task RequestBuilder() - { - // Build the HTTP post request: - var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); + // Build the messages: + // - First of all the system prompt + // - Then none-empty user and AI messages + Messages = [systemPrompt, ..messages], - // Set the authorization header: - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); - - // Set the content: - request.Content = new StringContent(mistralChatRequest, Encoding.UTF8, "application/json"); - return request; - } - - await foreach (var content in this.StreamChatCompletionInternal("Mistral", RequestBuilder, token)) + // Right now, we only support streaming completions: + Stream = true, + RandomSeed = randomSeed, + SafePrompt = safePrompt, + AdditionalApiParameters = apiParameters + }; + }, + token: token)) yield return content; } diff --git a/app/MindWork AI Studio/Provider/OpenRouter/ProviderOpenRouter.cs b/app/MindWork AI Studio/Provider/OpenRouter/ProviderOpenRouter.cs index 4995cca9..9f2c1b13 100644 --- a/app/MindWork AI Studio/Provider/OpenRouter/ProviderOpenRouter.cs +++ b/app/MindWork AI Studio/Provider/OpenRouter/ProviderOpenRouter.cs @@ -1,7 +1,5 @@ using System.Net.Http.Headers; using System.Runtime.CompilerServices; -using System.Text; -using System.Text.Json; using AIStudio.Chat; using AIStudio.Provider.OpenAI; @@ -27,57 +25,37 @@ public sealed class ProviderOpenRouter() : BaseProvider(LLMProviders.OPEN_ROUTER /// public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) { - // Get the API key: - var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER); - if(!requestedSecret.Success) - yield break; + await foreach (var content in this.StreamOpenAICompatibleChatCompletion( + "OpenRouter", + chatModel, + chatThread, + settingsManager, + async (systemPrompt, apiParameters) => + { + // Build the list of messages: + var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel); - // Prepare the system prompt: - var systemPrompt = new TextMessage - { - Role = "system", - Content = chatThread.PrepareSystemPrompt(settingsManager), - }; + return new ChatCompletionAPIRequest + { + Model = chatModel.Id, - // Parse the API parameters: - var apiParameters = this.ParseAdditionalApiParameters(); - - // Build the list of messages: - var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel); + // Build the messages: + // - First of all the system prompt + // - Then none-empty user and AI messages + Messages = [systemPrompt, ..messages], - // Prepare the OpenRouter HTTP chat request: - var openRouterChatRequest = JsonSerializer.Serialize(new ChatCompletionAPIRequest - { - Model = chatModel.Id, - - // Build the messages: - // - First of all the system prompt - // - Then none-empty user and AI messages - Messages = [systemPrompt, ..messages], - - // Right now, we only support streaming completions: - Stream = true, - AdditionalApiParameters = apiParameters - }, JSON_SERIALIZER_OPTIONS); - - async Task RequestBuilder() - { - // Build the HTTP post request: - var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); - - // Set the authorization header: - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); - - // Set custom headers for project identification: - request.Headers.Add("HTTP-Referer", PROJECT_WEBSITE); - request.Headers.Add("X-Title", PROJECT_NAME); - - // Set the content: - request.Content = new StringContent(openRouterChatRequest, Encoding.UTF8, "application/json"); - return request; - } - - await foreach (var content in this.StreamChatCompletionInternal("OpenRouter", RequestBuilder, token)) + // Right now, we only support streaming completions: + Stream = true, + AdditionalApiParameters = apiParameters + }; + }, + headersAction: headers => + { + // Set custom headers for project identification: + headers.Add("HTTP-Referer", PROJECT_WEBSITE); + headers.Add("X-Title", PROJECT_NAME); + }, + token: token)) yield return content; } diff --git a/app/MindWork AI Studio/Provider/Perplexity/ProviderPerplexity.cs b/app/MindWork AI Studio/Provider/Perplexity/ProviderPerplexity.cs index 4c73dc2d..745dd974 100644 --- a/app/MindWork AI Studio/Provider/Perplexity/ProviderPerplexity.cs +++ b/app/MindWork AI Studio/Provider/Perplexity/ProviderPerplexity.cs @@ -1,7 +1,4 @@ -using System.Net.Http.Headers; using System.Runtime.CompilerServices; -using System.Text; -using System.Text.Json; using AIStudio.Chat; using AIStudio.Provider.OpenAI; @@ -33,51 +30,29 @@ public sealed class ProviderPerplexity() : BaseProvider(LLMProviders.PERPLEXITY, /// public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) { - // Get the API key: - var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER); - if(!requestedSecret.Success) - yield break; - - // Prepare the system prompt: - var systemPrompt = new TextMessage - { - Role = "system", - Content = chatThread.PrepareSystemPrompt(settingsManager), - }; - - // Parse the API parameters: - var apiParameters = this.ParseAdditionalApiParameters(); - - // Build the list of messages: - var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel); - - // Prepare the Perplexity HTTP chat request: - var perplexityChatRequest = JsonSerializer.Serialize(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 - }, JSON_SERIALIZER_OPTIONS); + await foreach (var content in this.StreamOpenAICompatibleChatCompletion( + "Perplexity", + chatModel, + chatThread, + settingsManager, + async (systemPrompt, apiParameters) => + { + // Build the list of messages: + var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel); - async Task RequestBuilder() - { - // Build the HTTP post request: - var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); + return new ChatCompletionAPIRequest + { + Model = chatModel.Id, - // Set the authorization header: - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); - - // Set the content: - request.Content = new StringContent(perplexityChatRequest, Encoding.UTF8, "application/json"); - return request; - } - - await foreach (var content in this.StreamChatCompletionInternal("Perplexity", RequestBuilder, token)) + // 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)) yield return content; } @@ -128,4 +103,4 @@ public sealed class ProviderPerplexity() : BaseProvider(LLMProviders.PERPLEXITY, #endregion private Task> LoadModels() => Task.FromResult>(KNOWN_MODELS); -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Provider/SelfHosted/ChatRequest.cs b/app/MindWork AI Studio/Provider/SelfHosted/ChatRequest.cs deleted file mode 100644 index e1da56bd..00000000 --- a/app/MindWork AI Studio/Provider/SelfHosted/ChatRequest.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Text.Json.Serialization; - -namespace AIStudio.Provider.SelfHosted; - -/// -/// The chat request model. -/// -/// Which model to use for chat completion. -/// The chat messages. -/// Whether to stream the chat completion. -public readonly record struct ChatRequest( - string Model, - IList Messages, - bool Stream -) -{ - // Attention: The "required" modifier is not supported for [JsonExtensionData]. - [JsonExtensionData] - public IDictionary AdditionalApiParameters { get; init; } = new Dictionary(); -} \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs b/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs index 8204fa6c..01e86cc3 100644 --- a/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs +++ b/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs @@ -1,7 +1,5 @@ using System.Net.Http.Headers; using System.Runtime.CompilerServices; -using System.Text; -using System.Text.Json; using AIStudio.Chat; using AIStudio.Provider.OpenAI; @@ -25,58 +23,39 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide /// public override async IAsyncEnumerable StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) { - // Get the API key: - var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER, isTrying: true); - - // Prepare the system prompt: - var systemPrompt = new TextMessage - { - Role = "system", - Content = chatThread.PrepareSystemPrompt(settingsManager), - }; - - // Parse the API parameters: - var apiParameters = this.ParseAdditionalApiParameters(); + await foreach (var content in this.StreamOpenAICompatibleChatCompletion( + "self-hosted provider", + chatModel, + chatThread, + settingsManager, + async (systemPrompt, apiParameters) => + { + // Build the list of messages. The image format depends on the host: + // - Ollama uses the direct image URL format: { "type": "image_url", "image_url": "data:..." } + // - LM Studio, vLLM, and llama.cpp use the nested image URL format: { "type": "image_url", "image_url": { "url": "data:..." } } + var messages = host switch + { + Host.OLLAMA => await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.Provider, chatModel), + _ => await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel), + }; - // Build the list of messages. The image format depends on the host: - // - Ollama uses the direct image URL format: { "type": "image_url", "image_url": "data:..." } - // - LM Studio, vLLM, and llama.cpp use the nested image URL format: { "type": "image_url", "image_url": { "url": "data:..." } } - var messages = host switch - { - Host.OLLAMA => await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.Provider, chatModel), - _ => await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel), - }; - - // Prepare the OpenAI HTTP chat request: - var providerChatRequest = JsonSerializer.Serialize(new ChatRequest - { - Model = chatModel.Id, - - // Build the messages: - // - First of all the system prompt - // - Then none-empty user and AI messages - Messages = [systemPrompt, ..messages], - - // Right now, we only support streaming completions: - Stream = true, - AdditionalApiParameters = apiParameters - }, JSON_SERIALIZER_OPTIONS); + return new ChatCompletionAPIRequest + { + Model = chatModel.Id, - async Task RequestBuilder() - { - // Build the HTTP post request: - var request = new HttpRequestMessage(HttpMethod.Post, host.ChatURL()); + // Build the messages: + // - First of all the system prompt + // - Then none-empty user and AI messages + Messages = [systemPrompt, ..messages], - // Set the authorization header: - if (requestedSecret.Success) - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); - - // Set the content: - request.Content = new StringContent(providerChatRequest, Encoding.UTF8, "application/json"); - return request; - } - - await foreach (var content in this.StreamChatCompletionInternal("self-hosted provider", RequestBuilder, token)) + // Right now, we only support streaming completions: + Stream = true, + AdditionalApiParameters = apiParameters + }; + }, + isTryingSecret: true, + requestPath: host.ChatURL(), + token: token)) yield return content; } @@ -211,4 +190,4 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide filterPhrases.All( filter => model.Id.Contains(filter, StringComparison.InvariantCulture))) .Select(n => new Provider.Model(n.Id, null)); } -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Provider/X/ProviderX.cs b/app/MindWork AI Studio/Provider/X/ProviderX.cs index 21d6e2ca..8c1685ee 100644 --- a/app/MindWork AI Studio/Provider/X/ProviderX.cs +++ b/app/MindWork AI Studio/Provider/X/ProviderX.cs @@ -1,7 +1,5 @@ using System.Net.Http.Headers; using System.Runtime.CompilerServices; -using System.Text; -using System.Text.Json; using AIStudio.Chat; using AIStudio.Provider.OpenAI; @@ -24,53 +22,31 @@ public sealed class ProviderX() : BaseProvider(LLMProviders.X, "https://api.x.ai /// public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) { - // Get the API key: - var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER); - if(!requestedSecret.Success) - yield break; - - // Prepare the system prompt: - var systemPrompt = new TextMessage - { - Role = "system", - Content = chatThread.PrepareSystemPrompt(settingsManager), - }; - - // Parse the API parameters: - var apiParameters = this.ParseAdditionalApiParameters(); - - // Build the list of messages: - var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel); - - // Prepare the xAI HTTP chat request: - var xChatRequest = JsonSerializer.Serialize(new ChatCompletionAPIRequest - { - Model = chatModel.Id, - - // Build the messages: - // - First of all the system prompt - // - Then none-empty user and AI messages - Messages = [systemPrompt, ..messages], - - // Right now, we only support streaming completions: - Stream = true, - AdditionalApiParameters = apiParameters - }, JSON_SERIALIZER_OPTIONS); + await foreach (var content in this.StreamOpenAICompatibleChatCompletion( + "xAI", + chatModel, + chatThread, + settingsManager, + async (systemPrompt, apiParameters) => + { + // Build the list of messages: + var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel); - async Task RequestBuilder() - { - // Build the HTTP post request: - var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); + return new ChatCompletionAPIRequest + { + Model = chatModel.Id, - // Set the authorization header: - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); + // Build the messages: + // - First of all the system prompt + // - Then none-empty user and AI messages + Messages = [systemPrompt, ..messages], - // Set the content: - request.Content = new StringContent(xChatRequest, Encoding.UTF8, "application/json"); - return request; - } - - await foreach (var content in this.StreamChatCompletionInternal("xAI", RequestBuilder, token)) + // Right now, we only support streaming completions: + Stream = true, + AdditionalApiParameters = apiParameters + }; + }, + token: token)) yield return content; } @@ -158,4 +134,4 @@ public sealed class ProviderX() : BaseProvider(LLMProviders.X, "https://api.x.ai } ]); } -} \ No newline at end of file +}