From 7d54b9a55094319a19b5f77ab8f766b201dca10c Mon Sep 17 00:00:00 2001 From: Paul Koudelka Date: Tue, 30 Jun 2026 01:34:40 +0200 Subject: [PATCH] Added transparent ProviderTransparency --- .../Assistants/I18N/allTexts.lua | 9 + .../Dialogs/EmbeddingProviderDialog.razor | 108 ++++---- .../Dialogs/EmbeddingProviderDialog.razor.cs | 8 +- .../Dialogs/ProviderDialog.razor | 36 ++- .../Dialogs/ProviderDialog.razor.cs | 12 +- .../Dialogs/TranscriptionProviderDialog.razor | 9 +- .../TranscriptionProviderDialog.razor.cs | 12 +- .../Provider/BaseProvider.cs | 3 + app/MindWork AI Studio/Provider/IProvider.cs | 5 + .../Provider/LLMProviders.cs | 1 + .../Provider/LLMProvidersExtensions.cs | 60 ++++- app/MindWork AI Studio/Provider/NoProvider.cs | 3 + .../Transparency/ProviderTransparency.cs | 244 ++++++++++++++++++ .../Transparency/ProviderTransparencyBase.cs | 169 ++++++++++++ .../ProviderTransparencyEmbedding.cs | 32 +++ .../ProviderTransparencyTranscription.cs | 17 ++ .../Settings/ProviderExtensions.cs | 9 +- .../Settings/SettingsManager.cs | 6 + .../Tools/Validation/ProviderValidation.cs | 2 +- 19 files changed, 678 insertions(+), 67 deletions(-) create mode 100644 app/MindWork AI Studio/Provider/Transparency/ProviderTransparency.cs create mode 100644 app/MindWork AI Studio/Provider/Transparency/ProviderTransparencyBase.cs create mode 100644 app/MindWork AI Studio/Provider/Transparency/ProviderTransparencyEmbedding.cs create mode 100644 app/MindWork AI Studio/Provider/Transparency/ProviderTransparencyTranscription.cs diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 99257cc7..17ae4fb2 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -4138,6 +4138,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T2842060373"] = "In -- Currently, we cannot query the embedding models for the selected provider and/or host. Therefore, please enter the model name manually. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T290547799"] = "Currently, we cannot query the embedding models for the selected provider and/or host. Therefore, please enter the model name manually." +-- This host uses the model configured at the provider level. No model selection is available. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T3783329915"] = "This host uses the model configured at the provider level. No model selection is available." + -- Model selection UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T416738168"] = "Model selection" @@ -6685,6 +6688,12 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCELEVELEXTENSIONS::T786675843"] = " -- Self-hosted UI_TEXT_CONTENT["AISTUDIO::PROVIDER::LLMPROVIDERSEXTENSIONS::T146444217"] = "Self-hosted" +-- This provider never contacts an external service. It only shows the exact request AI Studio generated locally so you can inspect it for transparency and research. It is not intended to produce real model answers. +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::LLMPROVIDERSEXTENSIONS::T2035654846"] = "This provider never contacts an external service. It only shows the exact request AI Studio generated locally so you can inspect it for transparency and research. It is not intended to produce real model answers." + +-- Transparency +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::LLMPROVIDERSEXTENSIONS::T2360978711"] = "Transparency" + -- No provider selected UI_TEXT_CONTENT["AISTUDIO::PROVIDER::LLMPROVIDERSEXTENSIONS::T2897045472"] = "No provider selected" diff --git a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor index 85e6e6ef..e7699703 100644 --- a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor +++ b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor @@ -22,6 +22,13 @@ @T("Create account") + + @if (!string.IsNullOrWhiteSpace(this.DataLLMProvider.Description())) + { + + @this.DataLLMProvider.Description() + + } @if (this.DataLLMProvider.IsAPIKeyNeeded(this.DataHost)) { @@ -57,57 +64,68 @@ } - - - @if (this.DataLLMProvider.IsEmbeddingModelProvidedManually(this.DataHost)) - { - - } - else - { - - @T("Load") - - @if(this.availableModels.Count is 0) + @if (!this.DataLLMProvider.IsEmbeddingModelSelectionHidden(this.DataHost)) + { + + + @if (this.DataLLMProvider.IsEmbeddingModelProvidedManually(this.DataHost)) { - - @T("No models loaded or available.") - + } else { - - @foreach (var model in this.availableModels) - { - - @model - - } - + + @T("Load") + + @if(this.availableModels.Count is 0) + { + + @T("No models loaded or available.") + + } + else + { + + @foreach (var model in this.availableModels) + { + + @model + + } + + } } + + @if (!string.IsNullOrWhiteSpace(this.dataLoadingModelsIssue)) + { + + @this.dataLoadingModelsIssue + } - - @if (!string.IsNullOrWhiteSpace(this.dataLoadingModelsIssue)) - { - - @this.dataLoadingModelsIssue - - } - + + } + else if (!(DataLLMProvider is LLMProviders.TRANSPARENCY)) + { + + + @T("This host uses the model configured at the provider level. No model selection is available.") + + + } @* ReSharper disable once CSharpWarnings::CS8974 *@ + + @if (!string.IsNullOrWhiteSpace(this.DataLLMProvider.Description())) + { + + @this.DataLLMProvider.Description() + + } @if (this.DataLLMProvider.IsAPIKeyNeeded(this.DataHost)) { @@ -126,7 +133,7 @@ } } - else + else if (!(this.DataLLMProvider is LLMProviders.TRANSPARENCY)) { @@ -151,18 +158,21 @@ UserAttributes="@SPELLCHECK_ATTRIBUTES" /> - - - @(this.showExpertSettings ? T("Hide Expert Settings") : T("Show Expert Settings")) - - - - - @T("Please be aware: This section is for experts only. You are responsible for verifying the correctness of the additional parameters you provide to the API call. By default, AI Studio uses the OpenAI-compatible chat completions API, when that it is supported by the underlying service and model.") - - - - + @if (this.DataLLMProvider.CanConfigureAdditionalJsonApiParameters()) + { + + + @(this.showExpertSettings ? T("Hide Expert Settings") : T("Show Expert Settings")) + + + + + @T("Please be aware: This section is for experts only. You are responsible for verifying the correctness of the additional parameters you provide to the API call. By default, AI Studio uses the OpenAI-compatible chat completions API, when that it is supported by the underlying service and model.") + + + + + } diff --git a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs index 0e395324..ff94cc49 100644 --- a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs @@ -4,6 +4,7 @@ using System.Text.Json; using AIStudio.Components; using AIStudio.Provider; using AIStudio.Provider.HuggingFace; +using AIStudio.Provider.Transparency; using AIStudio.Tools.Services; using AIStudio.Tools.Validation; @@ -132,7 +133,11 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId // Determine the model based on the provider and host configuration: Model model; - if (this.DataLLMProvider.IsLLMModelSelectionHidden(this.DataHost)) + if (this.DataLLMProvider is LLMProviders.TRANSPARENCY) + { + model = ProviderTransparencyBase.CHAT_PREVIEW_MODEL; + } + else if (this.DataLLMProvider.IsLLMModelSelectionHidden(this.DataHost)) { // Use system model placeholder for hosts that don't support model selection (e.g., llama.cpp): model = Model.SYSTEM_MODEL; @@ -157,7 +162,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId Hostname = cleanedHostname.EndsWith('/') ? cleanedHostname[..^1] : cleanedHostname, Host = this.DataHost, HFInferenceProvider = this.HFInferenceProviderId, - AdditionalJsonApiParameters = this.AdditionalJsonApiParameters, + AdditionalJsonApiParameters = this.DataLLMProvider.CanConfigureAdditionalJsonApiParameters() ? this.AdditionalJsonApiParameters : string.Empty, }; } @@ -182,6 +187,9 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId if(this.IsEditing) { this.dataEditingPreviousInstanceName = this.DataInstanceName.ToLowerInvariant(); + + if (this.DataLLMProvider is LLMProviders.TRANSPARENCY) + return; // When using Fireworks or Hugging Face, we must copy the model name: if (this.DataLLMProvider.IsLLMModelProvidedManually()) diff --git a/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor b/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor index 78d2dea2..c7a84e30 100644 --- a/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor +++ b/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor @@ -22,6 +22,13 @@ @T("Create account") + + @if (!string.IsNullOrWhiteSpace(this.DataLLMProvider.Description())) + { + + @this.DataLLMProvider.Description() + + } @if (this.DataLLMProvider.IsAPIKeyNeeded(this.DataHost)) { @@ -111,7 +118,7 @@ } } - else + else if (!(this.DataLLMProvider is LLMProviders.TRANSPARENCY)) { diff --git a/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor.cs index faa3d3be..5d9fa846 100644 --- a/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor.cs @@ -1,5 +1,6 @@ using AIStudio.Components; using AIStudio.Provider; +using AIStudio.Provider.Transparency; using AIStudio.Settings; using AIStudio.Tools.Services; using AIStudio.Tools.Validation; @@ -116,7 +117,11 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId // Determine the model based on the provider and host configuration: Model model; - if (this.DataLLMProvider.IsTranscriptionModelSelectionHidden(this.DataHost)) + if (this.DataLLMProvider is LLMProviders.TRANSPARENCY) + { + model = ProviderTransparencyBase.TRANSCRIPTION_PREVIEW_MODEL; + } + else if (this.DataLLMProvider.IsTranscriptionModelSelectionHidden(this.DataHost)) { // Use system model placeholder for hosts that don't support model selection (e.g., whisper.cpp): model = Model.SYSTEM_MODEL; @@ -171,6 +176,9 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId if(this.IsEditing) { this.dataEditingPreviousInstanceName = this.DataName.ToLowerInvariant(); + + if (this.DataLLMProvider is LLMProviders.TRANSPARENCY) + return; // When using self-hosted models, we must copy the model name: if (this.DataLLMProvider is LLMProviders.SELF_HOSTED) @@ -312,7 +320,7 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId } catch (Exception e) { - this.Logger.LogError($"Failed to load models from provider '{this.DataLLMProvider}' (host={this.DataHost}, hostname='{this.DataHostname}'): {e.Message}");; + this.Logger.LogError($"Failed to load models from provider '{this.DataLLMProvider}' (host={this.DataHost}, hostname='{this.DataHostname}'): {e.Message}"); this.dataLoadingModelsIssue = T("We are currently unable to communicate with the provider to load models. Please try again later."); } } diff --git a/app/MindWork AI Studio/Provider/BaseProvider.cs b/app/MindWork AI Studio/Provider/BaseProvider.cs index e4ce964d..3b733c4e 100644 --- a/app/MindWork AI Studio/Provider/BaseProvider.cs +++ b/app/MindWork AI Studio/Provider/BaseProvider.cs @@ -92,6 +92,9 @@ public abstract class BaseProvider : IProvider, ISecretId /// public abstract string InstanceName { get; set; } + /// + public string Description => this.Provider.Description(); + /// public string AdditionalJsonApiParameters { get; init; } = string.Empty; diff --git a/app/MindWork AI Studio/Provider/IProvider.cs b/app/MindWork AI Studio/Provider/IProvider.cs index 3656b5e3..4391f253 100644 --- a/app/MindWork AI Studio/Provider/IProvider.cs +++ b/app/MindWork AI Studio/Provider/IProvider.cs @@ -23,6 +23,11 @@ public interface IProvider /// e.g., to distinguish between different OpenAI API keys. /// public string InstanceName { get; } + + /// + /// Optional provider description text shown in configuration UI. + /// + public string Description { get; } /// /// The additional API parameters. diff --git a/app/MindWork AI Studio/Provider/LLMProviders.cs b/app/MindWork AI Studio/Provider/LLMProviders.cs index 6a560036..3654742e 100644 --- a/app/MindWork AI Studio/Provider/LLMProviders.cs +++ b/app/MindWork AI Studio/Provider/LLMProviders.cs @@ -16,6 +16,7 @@ public enum LLMProviders ALIBABA_CLOUD = 12, PERPLEXITY = 14, OPEN_ROUTER = 15, + TRANSPARENCY = 16, FIREWORKS = 5, GROQ = 6, diff --git a/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs b/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs index e71cef95..fe8b5122 100644 --- a/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs +++ b/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs @@ -12,6 +12,7 @@ using AIStudio.Provider.OpenAI; using AIStudio.Provider.OpenRouter; using AIStudio.Provider.Perplexity; using AIStudio.Provider.SelfHosted; +using AIStudio.Provider.Transparency; using AIStudio.Provider.X; using AIStudio.Settings; using AIStudio.Tools.PluginSystem; @@ -44,6 +45,7 @@ public static class LLMProvidersExtensions LLMProviders.ALIBABA_CLOUD => "Alibaba Cloud", LLMProviders.PERPLEXITY => "Perplexity", LLMProviders.OPEN_ROUTER => "OpenRouter", + LLMProviders.TRANSPARENCY => TB("Transparency"), LLMProviders.GROQ => "Groq", LLMProviders.FIREWORKS => "Fireworks.ai", @@ -97,6 +99,8 @@ 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.TRANSPARENCY => Confidence.SELF_HOSTED.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)), @@ -120,6 +124,7 @@ public static class LLMProvidersExtensions LLMProviders.GOOGLE => true, LLMProviders.HELMHOLTZ => true, LLMProviders.ALIBABA_CLOUD => true, + LLMProviders.TRANSPARENCY => true, // // Providers that do not support embeddings: @@ -151,6 +156,7 @@ public static class LLMProvidersExtensions LLMProviders.MISTRAL => true, LLMProviders.FIREWORKS => true, LLMProviders.GWDG => true, + LLMProviders.TRANSPARENCY => true, // // Providers that support transcription but provide no OpenAI-compatible API yet: @@ -186,6 +192,16 @@ public static class LLMProvidersExtensions /// The provider instance. public static IProvider CreateProvider(this AIStudio.Settings.Provider providerSettings) { + if (providerSettings.UsedLLMProvider is LLMProviders.TRANSPARENCY) + { + return new ProviderTransparency + { + InstanceName = providerSettings.InstanceName, + AdditionalJsonApiParameters = providerSettings.AdditionalJsonApiParameters, + IsEnterpriseConfiguration = providerSettings.IsEnterpriseConfiguration, + }; + } + return providerSettings.UsedLLMProvider.CreateProvider(providerSettings.InstanceName, providerSettings.Host, providerSettings.Hostname, providerSettings.Model, providerSettings.HFInferenceProvider, providerSettings.AdditionalJsonApiParameters, providerSettings.IsEnterpriseConfiguration); } @@ -196,6 +212,15 @@ public static class LLMProvidersExtensions /// The provider instance. public static IProvider CreateProvider(this EmbeddingProvider embeddingProviderSettings) { + if (embeddingProviderSettings.UsedLLMProvider is LLMProviders.TRANSPARENCY) + { + return new ProviderTransparencyEmbedding + { + InstanceName = embeddingProviderSettings.Name, + IsEnterpriseConfiguration = embeddingProviderSettings.IsEnterpriseConfiguration, + }; + } + return embeddingProviderSettings.UsedLLMProvider.CreateProvider(embeddingProviderSettings.Name, embeddingProviderSettings.Host, embeddingProviderSettings.Hostname, embeddingProviderSettings.Model, HFInferenceProvider.NONE, isEnterpriseConfiguration: embeddingProviderSettings.IsEnterpriseConfiguration); } @@ -206,6 +231,15 @@ public static class LLMProvidersExtensions /// The provider instance. public static IProvider CreateProvider(this TranscriptionProvider transcriptionProviderSettings) { + if (transcriptionProviderSettings.UsedLLMProvider is LLMProviders.TRANSPARENCY) + { + return new ProviderTransparencyTranscription + { + InstanceName = transcriptionProviderSettings.Name, + IsEnterpriseConfiguration = transcriptionProviderSettings.IsEnterpriseConfiguration, + }; + } + return transcriptionProviderSettings.UsedLLMProvider.CreateProvider(transcriptionProviderSettings.Name, transcriptionProviderSettings.Host, transcriptionProviderSettings.Hostname, transcriptionProviderSettings.Model, HFInferenceProvider.NONE, isEnterpriseConfiguration: transcriptionProviderSettings.IsEnterpriseConfiguration); } @@ -224,6 +258,7 @@ public static class LLMProvidersExtensions LLMProviders.ALIBABA_CLOUD => new ProviderAlibabaCloud { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, LLMProviders.PERPLEXITY => new ProviderPerplexity { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, LLMProviders.OPEN_ROUTER => new ProviderOpenRouter { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.TRANSPARENCY => new ProviderTransparency { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, LLMProviders.GROQ => new ProviderGroq { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, LLMProviders.FIREWORKS => new ProviderFireworks { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, @@ -336,10 +371,17 @@ public static class LLMProvidersExtensions /// True if model selection should be hidden; otherwise, false. public static bool IsLLMModelSelectionHidden(this LLMProviders provider, Host host) => provider switch { + LLMProviders.TRANSPARENCY => true, LLMProviders.SELF_HOSTED => host is Host.LLAMA_CPP, _ => false, }; + public static bool IsEmbeddingModelSelectionHidden(this LLMProviders provider, Host host) => provider switch + { + LLMProviders.TRANSPARENCY => true, + _ => false, + }; + /// /// Determines if the model selection should be completely hidden for transcription providers. /// This is the case when the host does not support model selection (e.g., whisper.cpp). @@ -349,6 +391,7 @@ public static class LLMProvidersExtensions /// True if model selection should be hidden; otherwise, false. public static bool IsTranscriptionModelSelectionHidden(this LLMProviders provider, Host host) => provider switch { + LLMProviders.TRANSPARENCY => true, LLMProviders.SELF_HOSTED => host is Host.WHISPER_CPP, _ => false, }; @@ -411,6 +454,9 @@ public static class LLMProvidersExtensions public static bool CanLoadModels(this LLMProviders provider, Host host, string? apiKey) { + if (provider is LLMProviders.TRANSPARENCY) + return false; + if (provider is LLMProviders.SELF_HOSTED) { switch (host) @@ -442,4 +488,16 @@ public static class LLMProvidersExtensions LLMProviders.HUGGINGFACE => true, _ => false, }; -} \ No newline at end of file + + public static string Description(this LLMProviders provider) => provider switch + { + LLMProviders.TRANSPARENCY => TB("This provider never contacts an external service. It only shows the exact request AI Studio generated locally so you can inspect it for transparency and research. It is not intended to produce real model answers."), + _ => string.Empty, + }; + + public static bool CanConfigureAdditionalJsonApiParameters(this LLMProviders provider) => provider switch + { + LLMProviders.TRANSPARENCY => false, + _ => true, + }; +} diff --git a/app/MindWork AI Studio/Provider/NoProvider.cs b/app/MindWork AI Studio/Provider/NoProvider.cs index 1106292a..8c04eedf 100644 --- a/app/MindWork AI Studio/Provider/NoProvider.cs +++ b/app/MindWork AI Studio/Provider/NoProvider.cs @@ -15,6 +15,9 @@ public class NoProvider : IProvider public string InstanceName { get; set; } = "None"; + /// + public string Description => this.Provider.Description(); + /// public string AdditionalJsonApiParameters { get; init; } = string.Empty; diff --git a/app/MindWork AI Studio/Provider/Transparency/ProviderTransparency.cs b/app/MindWork AI Studio/Provider/Transparency/ProviderTransparency.cs new file mode 100644 index 00000000..062db94a --- /dev/null +++ b/app/MindWork AI Studio/Provider/Transparency/ProviderTransparency.cs @@ -0,0 +1,244 @@ +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Json; + +using AIStudio.Chat; +using AIStudio.Provider.OpenAI; +using AIStudio.Settings; + +namespace AIStudio.Provider.Transparency; + +public sealed class ProviderTransparency() : ProviderTransparencyBase(LOGGER) +{ + private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(); + + public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) + { + var effectiveModel = NormalizeModel(chatModel, CHAT_PREVIEW_MODEL); + var requestBlocks = chatThread.Blocks + .Where(block => !IsTransparencyPreviewBlock(block)) + .ToList(); + var skippedTransparencyPreviewCount = chatThread.Blocks.Count - requestBlocks.Count; + var preparedSystemPrompt = chatThread.PrepareSystemPrompt(settingsManager); + var systemPrompt = new TextMessage + { + Role = "system", + Content = preparedSystemPrompt, + }; + + var apiParameters = this.ParseAdditionalApiParameters(); + var messages = await requestBlocks.BuildMessagesUsingDirectImageUrlAsync(this.Provider, effectiveModel); + var requestBody = JsonSerializer.Serialize(new ChatCompletionAPIRequest + { + Model = effectiveModel.Id, + Messages = [systemPrompt, ..messages], + Stream = true, + AdditionalApiParameters = apiParameters, + }, JSON_SERIALIZER_OPTIONS); + var readableBreakdown = this.BuildReadableBreakdown(chatThread, preparedSystemPrompt, requestBlocks, skippedTransparencyPreviewCount, settingsManager); + + yield return new ContentStreamChunk( + this.BuildJsonRequestPreview( + "chat/completions", + effectiveModel, + requestBody, + readableBreakdown, + ("Message count", (messages.Count + 1).ToString()), + ("Stream", bool.TrueString), + ("Additional API parameters", apiParameters.Count.ToString())), + []); + } + + private string BuildReadableBreakdown(ChatThread chatThread, string preparedSystemPrompt, IReadOnlyList requestBlocks, int skippedTransparencyPreviewCount, SettingsManager settingsManager) + { + var builder = new StringBuilder(); + AppendSystemPromptBreakdown(builder, chatThread, preparedSystemPrompt, settingsManager); + AppendConversationBreakdown(builder, chatThread, requestBlocks, skippedTransparencyPreviewCount, settingsManager); + return builder.ToString().TrimEnd(); + } + + private static void AppendSystemPromptBreakdown(StringBuilder builder, ChatThread chatThread, string preparedSystemPrompt, SettingsManager settingsManager) + { + var baseSystemPrompt = chatThread.SystemPrompt; + var selectedTemplate = ResolveSelectedChatTemplate(chatThread, settingsManager); + var selectedProfile = ResolveSelectedProfile(chatThread, settingsManager); + + builder.AppendLine("Final system prompt sent to the provider:"); + builder.AppendLine("```text"); + builder.AppendLine(preparedSystemPrompt); + builder.AppendLine("```"); + builder.AppendLine(); + builder.AppendLine("System prompt sources:"); + + if (!string.IsNullOrWhiteSpace(baseSystemPrompt)) + { + builder.AppendLine("- Base chat system prompt:"); + builder.AppendLine("```text"); + builder.AppendLine(baseSystemPrompt); + builder.AppendLine("```"); + } + + if (selectedTemplate is not null) + { + builder.AppendLine($"- Chat template system prompt from `{selectedTemplate.GetSafeName()}`:"); + builder.AppendLine("```text"); + builder.AppendLine(selectedTemplate.ToSystemPrompt()); + builder.AppendLine("```"); + + if (selectedTemplate.ExampleConversation.Count > 0) + builder.AppendLine($"- Chat template example conversation adds `{selectedTemplate.ExampleConversation.Count}` hidden message(s) to the request history."); + } + + if (!string.IsNullOrWhiteSpace(chatThread.AugmentedData)) + { + builder.AppendLine("- Augmented context added by AI Studio:"); + builder.AppendLine("```text"); + builder.AppendLine(chatThread.AugmentedData); + builder.AppendLine("```"); + } + + if (selectedProfile is not null && selectedTemplate?.AllowProfileUsage != false) + { + if (!string.IsNullOrWhiteSpace(selectedProfile.NeedToKnow)) + { + builder.AppendLine($"- Profile `{selectedProfile.GetSafeName()}`: What should you know about the user?"); + builder.AppendLine("```text"); + builder.AppendLine(selectedProfile.NeedToKnow); + builder.AppendLine("```"); + } + + if (!string.IsNullOrWhiteSpace(selectedProfile.Actions)) + { + builder.AppendLine($"- Profile `{selectedProfile.GetSafeName()}`: The user wants you to consider the following things."); + builder.AppendLine("```text"); + builder.AppendLine(selectedProfile.Actions); + builder.AppendLine("```"); + } + } + + if (selectedTemplate is not null && !selectedTemplate.AllowProfileUsage) + builder.AppendLine($"- Profile instructions are disabled by chat template `{selectedTemplate.GetSafeName()}`."); + + if (chatThread.IncludeDateTime) + builder.AppendLine("- AI Studio prepends the current UTC and local date/time before the system prompt."); + } + + private static void AppendConversationBreakdown(StringBuilder builder, ChatThread chatThread, IReadOnlyList requestBlocks, int skippedTransparencyPreviewCount, SettingsManager settingsManager) + { + var selectedTemplate = ResolveSelectedChatTemplate(chatThread, settingsManager); + var orderedBlocks = requestBlocks + .Where(block => block.ContentType is ContentType.TEXT && block.Content is ContentText text && !string.IsNullOrWhiteSpace(text.Text)) + .OrderBy(block => block.Time) + .ToList(); + + builder.AppendLine(); + builder.AppendLine("Conversation history sources:"); + + if (skippedTransparencyPreviewCount > 0) + builder.AppendLine($"- Previous transparency preview responses are excluded from the generated request: `{skippedTransparencyPreviewCount}`."); + + if (orderedBlocks.Count == 0) + { + builder.AppendLine("- No prior chat messages are part of this request."); + return; + } + + var templateMessageCount = CountTemplateExampleMessages(orderedBlocks, selectedTemplate); + if (templateMessageCount > 0) + { + builder.AppendLine($"- Chat template example conversation from `{selectedTemplate!.GetSafeName()}`:"); + builder.AppendLine("```text"); + foreach (var block in orderedBlocks.Take(templateMessageCount)) + builder.AppendLine(SummarizeBlock(block)); + builder.AppendLine("```"); + } + + var remainingBlocks = orderedBlocks.Skip(templateMessageCount).ToList(); + var visibleBlocks = remainingBlocks.Where(block => !block.HideFromUser).ToList(); + var hiddenBlocks = remainingBlocks.Where(block => block.HideFromUser).ToList(); + + if (visibleBlocks.Count > 0) + { + builder.AppendLine("- Visible chat history and current user input:"); + builder.AppendLine("```text"); + foreach (var block in visibleBlocks) + builder.AppendLine(SummarizeBlock(block)); + builder.AppendLine("```"); + } + + if (hiddenBlocks.Count > 0) + { + builder.AppendLine("- Hidden messages included in the request:"); + builder.AppendLine("```text"); + foreach (var block in hiddenBlocks) + builder.AppendLine(SummarizeBlock(block)); + builder.AppendLine("```"); + } + } + + private static ChatTemplate? ResolveSelectedChatTemplate(ChatThread chatThread, SettingsManager settingsManager) + { + if (string.IsNullOrWhiteSpace(chatThread.SelectedChatTemplate)) + return null; + + if (!Guid.TryParse(chatThread.SelectedChatTemplate, out var templateId) || templateId == Guid.Empty || chatThread.SelectedChatTemplate == ChatTemplate.NO_CHAT_TEMPLATE.Id) + return null; + + return settingsManager.ConfigurationData.ChatTemplates.FirstOrDefault(template => template.Id == chatThread.SelectedChatTemplate); + } + + private static Profile? ResolveSelectedProfile(ChatThread chatThread, SettingsManager settingsManager) + { + if (string.IsNullOrWhiteSpace(chatThread.SelectedProfile)) + return null; + + if (!Guid.TryParse(chatThread.SelectedProfile, out var profileId) || profileId == Guid.Empty || chatThread.SelectedProfile == Profile.NO_PROFILE.Id) + return null; + + return settingsManager.ConfigurationData.Profiles.FirstOrDefault(profile => profile.Id == chatThread.SelectedProfile); + } + + private static int CountTemplateExampleMessages(IReadOnlyList orderedBlocks, ChatTemplate? selectedTemplate) + { + if (selectedTemplate is null || selectedTemplate.ExampleConversation.Count == 0) + return 0; + + var matchingCount = 0; + foreach (var pair in orderedBlocks.Zip(selectedTemplate.ExampleConversation)) + { + if (pair.First.Role != pair.Second.Role) + break; + + if (pair.First.Content is not ContentText firstText || pair.Second.Content is not ContentText secondText) + break; + + if (!string.Equals(firstText.Text.Trim(), secondText.Text.Trim(), StringComparison.Ordinal)) + break; + + matchingCount++; + } + + return matchingCount; + } + + private static string SummarizeBlock(ContentBlock block) + { + if (block.Content is not ContentText contentText) + return $"{block.Role.ToChatTemplateName()}: [unsupported content]"; + + var attachmentSuffix = contentText.FileAttachments.Count == 0 + ? string.Empty + : $" [attachments: {contentText.FileAttachments.Count}]"; + + var visibilitySuffix = block.HideFromUser ? " [hidden]" : string.Empty; + return $"{block.Role.ToChatTemplateName()}: {contentText.Text.Trim()}{attachmentSuffix}{visibilitySuffix}"; + } + + private static bool IsTransparencyPreviewBlock(ContentBlock block) + { + if (block.Role is not ChatRole.AI || block.Content is not ContentText contentText) + return false; + + return contentText.Text.StartsWith(PREVIEW_NOTICE, StringComparison.Ordinal); + } +} diff --git a/app/MindWork AI Studio/Provider/Transparency/ProviderTransparencyBase.cs b/app/MindWork AI Studio/Provider/Transparency/ProviderTransparencyBase.cs new file mode 100644 index 00000000..f4b02e55 --- /dev/null +++ b/app/MindWork AI Studio/Provider/Transparency/ProviderTransparencyBase.cs @@ -0,0 +1,169 @@ +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Json; + +using AIStudio.Chat; +using AIStudio.Settings; + +namespace AIStudio.Provider.Transparency; + +public abstract class ProviderTransparencyBase(ILogger logger) : BaseProvider(LLMProviders.TRANSPARENCY, new Uri("https://transparency.invalid/v1/"), ExternalHttpTrustPolicy.SYSTEM_TRUST_ONLY, logger) +{ + protected const string PREVIEW_NOTICE = "Transparency preview only. AI Studio generated this request locally and did not contact an external provider."; + + private static readonly JsonSerializerOptions PRETTY_JSON_SERIALIZER_OPTIONS = new() + { + WriteIndented = true, + }; + + protected readonly ILogger Logger = logger; + + public static readonly Model CHAT_PREVIEW_MODEL = new("transparency-preview", "Transparency Preview"); + + public static readonly Model EMBEDDING_PREVIEW_MODEL = new("transparency-embedding-preview", "Transparency Embedding Preview"); + + public static readonly Model TRANSCRIPTION_PREVIEW_MODEL = new("transparency-transcription-preview", "Transparency Transcription Preview"); + + public override string Id => LLMProviders.TRANSPARENCY.ToName(); + + public override string InstanceName { get; set; } = "Transparency"; + + public override bool HasModelLoadingCapability => false; + + public override Task GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) => Task.FromResult(SuccessfulModelLoadResult([CHAT_PREVIEW_MODEL])); + + public override Task GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default) => Task.FromResult(SuccessfulModelLoadResult([])); + + public override Task GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default) => Task.FromResult(SuccessfulModelLoadResult([EMBEDDING_PREVIEW_MODEL])); + + public override Task GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default) => Task.FromResult(SuccessfulModelLoadResult([TRANSCRIPTION_PREVIEW_MODEL])); + + public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) + { + await Task.CompletedTask; + yield break; + } + + public override async IAsyncEnumerable StreamImageCompletion(Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default) + { + await Task.CompletedTask; + yield break; + } + + public override Task TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default) => Task.FromResult(TranscriptionResult.Failure()); + + public override Task>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) => Task.FromResult>>([]); + + protected static Model NormalizeModel(Model model, Model fallbackModel) + { + if (model.IsSystemModel || string.IsNullOrWhiteSpace(model.Id)) + return fallbackModel; + + return model; + } + + protected string BuildJsonRequestPreview(string requestPath, Model model, string rawJson, string? readableBreakdown = null, params (string Label, string Value)[] details) + { + var requestUri = new Uri(this.BaseUri, requestPath); + var builder = new StringBuilder(); + builder.AppendLine(PREVIEW_NOTICE); + builder.AppendLine(); + builder.AppendLine("**Request metadata**"); + builder.AppendLine($"- URL: `{requestUri}`"); + builder.AppendLine($"- Instance: `{this.InstanceName}`"); + builder.AppendLine($"- Model: `{model.Id}`"); + + foreach (var (label, value) in details) + builder.AppendLine($"- {label}: `{value}`"); + + builder.AppendLine(); + builder.AppendLine("**Readable body**"); + if (!string.IsNullOrWhiteSpace(readableBreakdown)) + { + builder.AppendLine(readableBreakdown.TrimEnd()); + builder.AppendLine(); + } + builder.AppendLine("```json"); + builder.AppendLine(PrettyPrintJson(rawJson)); + builder.AppendLine("```"); + builder.AppendLine(); + builder.AppendLine("**Original unchanged JSON**"); + builder.AppendLine("```json"); + builder.AppendLine(rawJson); + builder.AppendLine("```"); + return builder.ToString().TrimEnd(); + } + + protected string BuildMultipartRequestPreview(string requestPath, Model model, string audioFilePath, string mimeType) + { + var requestUri = new Uri(this.BaseUri, requestPath); + var fileName = Path.GetFileName(audioFilePath); + var readableBody = JsonSerializer.Serialize(new + { + model = model.Id, + file = new + { + source_path = audioFilePath, + file_name = fileName, + mime_type = mimeType, + }, + }, PRETTY_JSON_SERIALIZER_OPTIONS); + + var rawBody = $$""" + file=@"{{audioFilePath}}"; filename="{{fileName}}"; content-type="{{mimeType}}" + model={{model.Id}} + """; + + var builder = new StringBuilder(); + builder.AppendLine(PREVIEW_NOTICE); + builder.AppendLine(); + builder.AppendLine("**Request metadata**"); + builder.AppendLine($"- URL: `{requestUri}`"); + builder.AppendLine($"- Instance: `{this.InstanceName}`"); + builder.AppendLine($"- Model: `{model.Id}`"); + builder.AppendLine($"- Audio file: `{audioFilePath}`"); + builder.AppendLine(); + builder.AppendLine("**Readable body**"); + builder.AppendLine("```json"); + builder.AppendLine(readableBody); + builder.AppendLine("```"); + builder.AppendLine(); + builder.AppendLine("**Original request body**"); + builder.AppendLine("This request does not use JSON. These are the multipart fields AI Studio prepared:"); + builder.AppendLine("```text"); + builder.AppendLine(rawBody); + builder.AppendLine("```"); + return builder.ToString().TrimEnd(); + } + + protected static IReadOnlyList> CreateDummyEmbeddings(int count) + { + var vectorCount = Math.Max(count, 1); + return Enumerable.Range(0, vectorCount) + .Select(index => (IReadOnlyList)new float[] + { + 0.125f + index, + 0.25f + index, + 0.375f + index, + 0.5f + index, + 0.625f + index, + 0.75f + index, + 0.875f + index, + 1f + index, + }) + .ToArray(); + } + + private static string PrettyPrintJson(string rawJson) + { + try + { + using var jsonDocument = JsonDocument.Parse(rawJson); + return JsonSerializer.Serialize(jsonDocument.RootElement, PRETTY_JSON_SERIALIZER_OPTIONS); + } + catch (JsonException) + { + return rawJson; + } + } +} diff --git a/app/MindWork AI Studio/Provider/Transparency/ProviderTransparencyEmbedding.cs b/app/MindWork AI Studio/Provider/Transparency/ProviderTransparencyEmbedding.cs new file mode 100644 index 00000000..92ec79f6 --- /dev/null +++ b/app/MindWork AI Studio/Provider/Transparency/ProviderTransparencyEmbedding.cs @@ -0,0 +1,32 @@ +using System.Text.Json; + +using AIStudio.Settings; + +namespace AIStudio.Provider.Transparency; + +public sealed class ProviderTransparencyEmbedding() : ProviderTransparencyBase(LOGGER) +{ + private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(); + + public override Task>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) + { + var effectiveModel = NormalizeModel(embeddingModel, EMBEDDING_PREVIEW_MODEL); + var requestBody = JsonSerializer.Serialize(new + { + model = effectiveModel.Id, + input = texts, + encoding_format = "float", + }, JSON_SERIALIZER_OPTIONS); + + var preview = this.BuildJsonRequestPreview( + "embeddings", + effectiveModel, + requestBody, + readableBreakdown: null, + ("Input collection count", texts.Count.ToString()), + ("Stream", bool.FalseString)); + + this.Logger.LogInformation("Transparency embedding preview for '{ProviderInstanceName}' (provider={ProviderType}).{NewLine}{Preview}", this.InstanceName, this.Provider, Environment.NewLine, preview); + return Task.FromResult(CreateDummyEmbeddings(texts.Count)); + } +} diff --git a/app/MindWork AI Studio/Provider/Transparency/ProviderTransparencyTranscription.cs b/app/MindWork AI Studio/Provider/Transparency/ProviderTransparencyTranscription.cs new file mode 100644 index 00000000..efd248b3 --- /dev/null +++ b/app/MindWork AI Studio/Provider/Transparency/ProviderTransparencyTranscription.cs @@ -0,0 +1,17 @@ +using AIStudio.Settings; +using AIStudio.Tools.MIME; + +namespace AIStudio.Provider.Transparency; + +public sealed class ProviderTransparencyTranscription() : ProviderTransparencyBase(LOGGER) +{ + private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(); + + public override Task TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default) + { + var effectiveModel = NormalizeModel(transcriptionModel, TRANSCRIPTION_PREVIEW_MODEL); + var mimeType = Builder.FromFilename(audioFilePath); + var preview = this.BuildMultipartRequestPreview("audio/transcriptions", effectiveModel, audioFilePath, mimeType); + return Task.FromResult(TranscriptionResult.FromText(preview)); + } +} diff --git a/app/MindWork AI Studio/Settings/ProviderExtensions.cs b/app/MindWork AI Studio/Settings/ProviderExtensions.cs index 8ce0ab2a..1eae9893 100644 --- a/app/MindWork AI Studio/Settings/ProviderExtensions.cs +++ b/app/MindWork AI Studio/Settings/ProviderExtensions.cs @@ -32,6 +32,13 @@ public static partial class ProviderExtensions LLMProviders.GROQ => GetModelCapabilitiesOpenSource(model), LLMProviders.FIREWORKS => GetModelCapabilitiesOpenSource(model), LLMProviders.HUGGINGFACE => GetModelCapabilitiesOpenSource(model), + LLMProviders.TRANSPARENCY => + [ + Capability.TEXT_INPUT, + Capability.MULTIPLE_IMAGE_INPUT, + Capability.TEXT_OUTPUT, + Capability.CHAT_COMPLETION_API, + ], LLMProviders.HELMHOLTZ => GetModelCapabilitiesOpenSource(model), LLMProviders.GWDG => GetModelCapabilitiesOpenSource(model), @@ -40,4 +47,4 @@ public static partial class ProviderExtensions _ => [] }; -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Settings/SettingsManager.cs b/app/MindWork AI Studio/Settings/SettingsManager.cs index 3ec8906c..9ba78888 100644 --- a/app/MindWork AI Studio/Settings/SettingsManager.cs +++ b/app/MindWork AI Studio/Settings/SettingsManager.cs @@ -391,6 +391,7 @@ public sealed class SettingsManager case ConfidenceSchemes.TRUST_ALL: return llmProvider switch { + LLMProviders.TRANSPARENCY => ConfidenceLevel.HIGH, LLMProviders.SELF_HOSTED => ConfidenceLevel.HIGH, _ => ConfidenceLevel.MEDIUM, @@ -399,6 +400,7 @@ public sealed class SettingsManager case ConfidenceSchemes.TRUST_USA_EUROPE: return llmProvider switch { + LLMProviders.TRANSPARENCY => ConfidenceLevel.HIGH, LLMProviders.SELF_HOSTED => ConfidenceLevel.HIGH, LLMProviders.DEEP_SEEK => ConfidenceLevel.LOW, @@ -408,6 +410,7 @@ public sealed class SettingsManager case ConfidenceSchemes.TRUST_USA: return llmProvider switch { + LLMProviders.TRANSPARENCY => ConfidenceLevel.HIGH, LLMProviders.SELF_HOSTED => ConfidenceLevel.HIGH, LLMProviders.MISTRAL => ConfidenceLevel.LOW, LLMProviders.HELMHOLTZ => ConfidenceLevel.LOW, @@ -420,6 +423,7 @@ public sealed class SettingsManager case ConfidenceSchemes.TRUST_EUROPE: return llmProvider switch { + LLMProviders.TRANSPARENCY => ConfidenceLevel.HIGH, LLMProviders.SELF_HOSTED => ConfidenceLevel.HIGH, LLMProviders.MISTRAL => ConfidenceLevel.MEDIUM, LLMProviders.HELMHOLTZ => ConfidenceLevel.MEDIUM, @@ -431,6 +435,7 @@ public sealed class SettingsManager case ConfidenceSchemes.TRUST_ASIA: return llmProvider switch { + LLMProviders.TRANSPARENCY => ConfidenceLevel.HIGH, LLMProviders.SELF_HOSTED => ConfidenceLevel.HIGH, LLMProviders.DEEP_SEEK => ConfidenceLevel.MEDIUM, @@ -440,6 +445,7 @@ public sealed class SettingsManager case ConfidenceSchemes.LOCAL_TRUST_ONLY: return llmProvider switch { + LLMProviders.TRANSPARENCY => ConfidenceLevel.HIGH, LLMProviders.SELF_HOSTED => ConfidenceLevel.HIGH, _ => ConfidenceLevel.VERY_LOW, diff --git a/app/MindWork AI Studio/Tools/Validation/ProviderValidation.cs b/app/MindWork AI Studio/Tools/Validation/ProviderValidation.cs index bb72feb4..0d3a4e6b 100644 --- a/app/MindWork AI Studio/Tools/Validation/ProviderValidation.cs +++ b/app/MindWork AI Studio/Tools/Validation/ProviderValidation.cs @@ -73,7 +73,7 @@ public sealed class ProviderValidation public string? ValidatingModel(Model model) { // For NONE providers, no validation is needed: - if (this.GetProvider() is LLMProviders.NONE) + if (this.GetProvider() is LLMProviders.NONE or LLMProviders.TRANSPARENCY) return null; // For self-hosted llama.cpp or whisper.cpp, no model selection needed