Added transparent ProviderTransparency

This commit is contained in:
Paul Koudelka 2026-06-30 01:34:40 +02:00
parent 9fc7eaff99
commit 7d54b9a550
19 changed files with 678 additions and 67 deletions

View File

@ -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"

View File

@ -22,6 +22,13 @@
@T("Create account")
</MudButton>
</MudStack>
@if (!string.IsNullOrWhiteSpace(this.DataLLMProvider.Description()))
{
<MudAlert Severity="Severity.Info" Class="mb-3">
@this.DataLLMProvider.Description()
</MudAlert>
}
@if (this.DataLLMProvider.IsAPIKeyNeeded(this.DataHost))
{
@ -57,57 +64,68 @@
</MudSelect>
}
<MudField FullWidth="true" Label="@T("Model selection")" Variant="Variant.Outlined" Class="mb-3">
<MudStack Row="@true" AlignItems="AlignItems.Center" StretchItems="StretchItems.End">
@if (this.DataLLMProvider.IsEmbeddingModelProvidedManually(this.DataHost))
{
<MudTextField
T="string"
@bind-Text="@this.dataManuallyModel"
Label="@T("Model")"
Class="mb-3"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Dns"
AdornmentColor="Color.Info"
Validation="@this.ValidateManuallyModel"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
HelperText="@T("Currently, we cannot query the embedding models for the selected provider and/or host. Therefore, please enter the model name manually.")"
/>
}
else
{
<MudButton Disabled="@(!this.DataLLMProvider.CanLoadModels(this.DataHost, this.dataAPIKey))" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.Refresh" OnClick="@this.ReloadModels">
@T("Load")
</MudButton>
@if(this.availableModels.Count is 0)
@if (!this.DataLLMProvider.IsEmbeddingModelSelectionHidden(this.DataHost))
{
<MudField FullWidth="true" Label="@T("Model selection")" Variant="Variant.Outlined" Class="mb-3">
<MudStack Row="@true" AlignItems="AlignItems.Center" StretchItems="StretchItems.End">
@if (this.DataLLMProvider.IsEmbeddingModelProvidedManually(this.DataHost))
{
<MudText Typo="Typo.body1">
@T("No models loaded or available.")
</MudText>
<MudTextField
T="string"
@bind-Text="@this.dataManuallyModel"
Label="@T("Model")"
Class="mb-3"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Dns"
AdornmentColor="Color.Info"
Validation="@this.ValidateManuallyModel"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
HelperText="@T("Currently, we cannot query the embedding models for the selected provider and/or host. Therefore, please enter the model name manually.")"
/>
}
else
{
<MudSelect Disabled="@this.IsNoneProvider" @bind-Value="@this.DataModel" Label="@T("Model")"
OpenIcon="@Icons.Material.Filled.FaceRetouchingNatural"
AdornmentColor="Color.Info" Adornment="Adornment.Start"
Validation="@this.providerValidation.ValidatingModel">
@foreach (var model in this.availableModels)
{
<MudSelectItem Value="@model">
@model
</MudSelectItem>
}
</MudSelect>
<MudButton Disabled="@(!this.DataLLMProvider.CanLoadModels(this.DataHost, this.dataAPIKey))" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.Refresh" OnClick="@this.ReloadModels">
@T("Load")
</MudButton>
@if(this.availableModels.Count is 0)
{
<MudText Typo="Typo.body1">
@T("No models loaded or available.")
</MudText>
}
else
{
<MudSelect Disabled="@this.IsNoneProvider" @bind-Value="@this.DataModel" Label="@T("Model")"
OpenIcon="@Icons.Material.Filled.FaceRetouchingNatural"
AdornmentColor="Color.Info" Adornment="Adornment.Start"
Validation="@this.providerValidation.ValidatingModel">
@foreach (var model in this.availableModels)
{
<MudSelectItem Value="@model">
@model
</MudSelectItem>
}
</MudSelect>
}
}
</MudStack>
@if (!string.IsNullOrWhiteSpace(this.dataLoadingModelsIssue))
{
<MudAlert Severity="Severity.Error" Class="mt-3">
@this.dataLoadingModelsIssue
</MudAlert>
}
</MudStack>
@if (!string.IsNullOrWhiteSpace(this.dataLoadingModelsIssue))
{
<MudAlert Severity="Severity.Error" Class="mt-3">
@this.dataLoadingModelsIssue
</MudAlert>
}
</MudField>
</MudField>
}
else if (!(DataLLMProvider is LLMProviders.TRANSPARENCY))
{
<MudField FullWidth="true" Label="@T("Model selection")" Variant="Variant.Outlined" Class="mb-3">
<MudText Typo="Typo.body1">
@T("This host uses the model configured at the provider level. No model selection is available.")
</MudText>
</MudField>
}
@* ReSharper disable once CSharpWarnings::CS8974 *@
<MudTextField

View File

@ -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;
@ -114,7 +115,9 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
{
var cleanedHostname = this.DataHostname.Trim();
Model model = default;
if(this.DataLLMProvider is LLMProviders.SELF_HOSTED)
if (this.DataLLMProvider is LLMProviders.TRANSPARENCY)
model = ProviderTransparencyBase.EMBEDDING_PREVIEW_MODEL;
else if(this.DataLLMProvider is LLMProviders.SELF_HOSTED)
{
if (this.DataHost is Host.OLLAMA)
model = new Model(this.dataManuallyModel, null);
@ -156,6 +159,9 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
if(this.IsEditing)
{
this.dataEditingPreviousInstanceName = this.DataName.ToLowerInvariant();
if (this.DataLLMProvider is LLMProviders.TRANSPARENCY)
return;
// When using self-hosted embedding, we must copy the model name:
if (this.DataLLMProvider is LLMProviders.SELF_HOSTED)

View File

@ -19,6 +19,13 @@
@T("Create account")
</MudButton>
</MudStack>
@if (!string.IsNullOrWhiteSpace(this.DataLLMProvider.Description()))
{
<MudAlert Severity="Severity.Info" Class="mb-3">
@this.DataLLMProvider.Description()
</MudAlert>
}
@if (this.DataLLMProvider.IsAPIKeyNeeded(this.DataHost))
{
@ -126,7 +133,7 @@
}
</MudField>
}
else
else if (!(this.DataLLMProvider is LLMProviders.TRANSPARENCY))
{
<MudField FullWidth="true" Label="@T("Model selection")" Variant="Variant.Outlined" Class="mb-3">
<MudText Typo="Typo.body1">
@ -151,18 +158,21 @@
UserAttributes="@SPELLCHECK_ATTRIBUTES"
/>
<MudStack>
<MudButton OnClick="@this.ToggleExpertSettings">
@(this.showExpertSettings ? T("Hide Expert Settings") : T("Show Expert Settings"))
</MudButton>
<MudDivider />
<MudCollapse Expanded="@this.showExpertSettings" Class="@this.GetExpertStyles">
<MudJustifiedText Class="mb-5">
@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.")
</MudJustifiedText>
<MudTextField T="string" Label=@T("Additional API parameters") Variant="Variant.Outlined" Lines="4" AutoGrow="true" MaxLines="10" HelperText=@T("""Add the parameters in proper JSON formatting, e.g., "temperature": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though.""") Placeholder="@GetPlaceholderExpertSettings" @bind-Value="@this.AdditionalJsonApiParameters" Immediate="true" Validation="@this.ValidateAdditionalJsonApiParameters" OnBlur="@this.OnInputChangeExpertSettings"/>
</MudCollapse>
</MudStack>
@if (this.DataLLMProvider.CanConfigureAdditionalJsonApiParameters())
{
<MudStack>
<MudButton OnClick="@this.ToggleExpertSettings">
@(this.showExpertSettings ? T("Hide Expert Settings") : T("Show Expert Settings"))
</MudButton>
<MudDivider />
<MudCollapse Expanded="@this.showExpertSettings" Class="@this.GetExpertStyles">
<MudJustifiedText Class="mb-5">
@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.")
</MudJustifiedText>
<MudTextField T="string" Label=@T("Additional API parameters") Variant="Variant.Outlined" Lines="4" AutoGrow="true" MaxLines="10" HelperText=@T("""Add the parameters in proper JSON formatting, e.g., "temperature": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though.""") Placeholder="@GetPlaceholderExpertSettings" @bind-Value="@this.AdditionalJsonApiParameters" Immediate="true" Validation="@this.ValidateAdditionalJsonApiParameters" OnBlur="@this.OnInputChangeExpertSettings"/>
</MudCollapse>
</MudStack>
}
</MudForm>
<Issues IssuesData="@this.dataIssues"/>
</DialogContent>

View File

@ -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())

View File

@ -22,6 +22,13 @@
@T("Create account")
</MudButton>
</MudStack>
@if (!string.IsNullOrWhiteSpace(this.DataLLMProvider.Description()))
{
<MudAlert Severity="Severity.Info" Class="mb-3">
@this.DataLLMProvider.Description()
</MudAlert>
}
@if (this.DataLLMProvider.IsAPIKeyNeeded(this.DataHost))
{
@ -111,7 +118,7 @@
}
</MudField>
}
else
else if (!(this.DataLLMProvider is LLMProviders.TRANSPARENCY))
{
<MudField FullWidth="true" Label="@T("Model selection")" Variant="Variant.Outlined" Class="mb-3">
<MudText Typo="Typo.body1">

View File

@ -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.");
}
}

View File

@ -92,6 +92,9 @@ public abstract class BaseProvider : IProvider, ISecretId
/// <inheritdoc />
public abstract string InstanceName { get; set; }
/// <inheritdoc />
public string Description => this.Provider.Description();
/// <inheritdoc />
public string AdditionalJsonApiParameters { get; init; } = string.Empty;

View File

@ -23,6 +23,11 @@ public interface IProvider
/// e.g., to distinguish between different OpenAI API keys.
/// </summary>
public string InstanceName { get; }
/// <summary>
/// Optional provider description text shown in configuration UI.
/// </summary>
public string Description { get; }
/// <summary>
/// The additional API parameters.

View File

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

View File

@ -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
/// <returns>The provider instance.</returns>
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
/// <returns>The provider instance.</returns>
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
/// <returns>The provider instance.</returns>
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
/// <returns>True if model selection should be hidden; otherwise, false.</returns>
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,
};
/// <summary>
/// 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
/// <returns>True if model selection should be hidden; otherwise, false.</returns>
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,
};
}
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,
};
}

View File

@ -15,6 +15,9 @@ public class NoProvider : IProvider
public string InstanceName { get; set; } = "None";
/// <inheritdoc />
public string Description => this.Provider.Description();
/// <inheritdoc />
public string AdditionalJsonApiParameters { get; init; } = string.Empty;

View File

@ -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<ProviderTransparency> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ProviderTransparency>();
public override async IAsyncEnumerable<ContentStreamChunk> 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<ContentBlock> 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<ContentBlock> 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<ContentBlock> 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);
}
}

View File

@ -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<ModelLoadResult> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) => Task.FromResult(SuccessfulModelLoadResult([CHAT_PREVIEW_MODEL]));
public override Task<ModelLoadResult> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default) => Task.FromResult(SuccessfulModelLoadResult([]));
public override Task<ModelLoadResult> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default) => Task.FromResult(SuccessfulModelLoadResult([EMBEDDING_PREVIEW_MODEL]));
public override Task<ModelLoadResult> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default) => Task.FromResult(SuccessfulModelLoadResult([TRANSCRIPTION_PREVIEW_MODEL]));
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
{
await Task.CompletedTask;
yield break;
}
public override async IAsyncEnumerable<ImageURL> 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<TranscriptionResult> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default) => Task.FromResult(TranscriptionResult.Failure());
public override Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts) => Task.FromResult<IReadOnlyList<IReadOnlyList<float>>>([]);
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<IReadOnlyList<float>> CreateDummyEmbeddings(int count)
{
var vectorCount = Math.Max(count, 1);
return Enumerable.Range(0, vectorCount)
.Select(index => (IReadOnlyList<float>)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;
}
}
}

View File

@ -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<ProviderTransparencyEmbedding> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ProviderTransparencyEmbedding>();
public override Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> 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));
}
}

View File

@ -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<ProviderTransparencyTranscription> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ProviderTransparencyTranscription>();
public override Task<TranscriptionResult> 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));
}
}

View File

@ -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
_ => []
};
}
}

View File

@ -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,

View File

@ -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