Merged main into pr/919

This commit is contained in:
Thorsten Sommer 2026-08-13 14:03:11 +02:00
commit 3d925f2ed9
Signed by untrusted user who does not match committer: tsommer
GPG Key ID: 371BBA77A02C0108
47 changed files with 1044 additions and 291 deletions

View File

@ -78,6 +78,7 @@ Since March 2025: We have started developing the plugin system. There will be la
</h3>
</summary>
- v26.8.1: Added Hetzner's EU-hosted inference API as a provider, along with support for the latest open-source models like DeepSeek V4, GLM 5.2, Kimi K2.7 & K3, and Qwen 3.6 & 3.8; added the Visual Briefing Assistant as a preview feature and the Batch Processing Assistant to process entire folders of documents in one run; you can now share, import, and delete plugins; greatly improved working with files, including much better Word and OpenDocument support; expanded enterprise IT support with configuration priorities, test configurations before rollout, and policies for plugin sharing and imports.
- v26.7.3: Added support for the latest OpenAI, Anthropic, and Google models; introduced audio and video transcription, a log viewer assistant, and AI-assisted editing and code management in the Assistant Builder; expanded presentation support with OpenDocument files, speaker notes, comments, and metadata; and improved Linux integration, enterprise update controls, and reliability after waking from sleep.
- v26.7.1: Added the assistant builder as a beta preview for creating assistant plugins without coding; assistants can now keep running in the background; improved provider capability visibility and expert overrides, expanded enterprise controls for data source behavior and trusted assistant plugins, and made chats, assistants, and source links more reliable.
- v26.6.2: Expanded enterprise configuration options with chat defaults, custom introduction panels, trust settings for data security, and managed confidence levels; added auto-backups for app settings & the possibility to view managed profiles and chat templates.
@ -89,7 +90,6 @@ Since March 2025: We have started developing the plugin system. There will be la
- v0.10.0: Added support for newer models like Mistral 3 & GPT 5.2, OpenRouter as LLM and embedding provider, the possibility to use file attachments in chats, and support for images as input.
- v0.9.51: Added support for [Perplexity](https://www.perplexity.ai/); citations added so that LLMs can provide source references (e.g., some OpenAI models, Perplexity); added support for OpenAI's Responses API so that all text LLMs from OpenAI now work in MindWork AI Studio, including Deep Research models; web searches are now possible (some OpenAI models, Perplexity).
- v0.9.50: Added support for self-hosted LLMs using [vLLM](https://blog.vllm.ai/2023/06/20/vllm.html).
- v0.9.46: Released our plugin system, a German language plugin, early support for enterprise environments, and configuration plugins. Additionally, we added the Pandoc integration for future data processing and file generation.
</details>
@ -115,6 +115,7 @@ MindWork AI Studio is a free desktop app for macOS, Windows, and Linux. It provi
- [DeepSeek](https://www.deepseek.com/en)
- [Alibaba Cloud](https://www.alibabacloud.com) (Qwen)
- [OpenRouter](https://openrouter.ai/)
- [Hetzner](https://experiments.hetzner.com) (experimental inference API running open-source models in the EU)
- [Hugging Face](https://huggingface.co/) using their [inference providers](https://huggingface.co/docs/inference-providers/index) such as Cerebras, Nebius, Sambanova, Novita, Hyperbolic, Together AI, Fireworks, Hugging Face
- Self-hosted models using [llama.cpp](https://github.com/ggerganov/llama.cpp), [ollama](https://github.com/ollama/ollama), [LM Studio](https://lmstudio.ai/), and [vLLM](https://github.com/vllm-project/vllm)
- [Groq](https://groq.com/)

View File

@ -14,7 +14,7 @@
<PackageReference Include="Cocona" Version="2.2.0" />
<!-- Pins Cocona's transitive Microsoft.Extensions.Hosting 6.0.0, which pulled in the vulnerable System.Text.Json 6.0.0 (GHSA-8g4q-xg66-9fp4) -->
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.18" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.19" />
</ItemGroup>
<ItemGroup>

View File

@ -91,6 +91,40 @@ public sealed partial class UpdateMetadataCommands
await this.Build(offline);
}
[Command("update-metainfo", Description = "Update the AppStream metainfo entry of one release from its changelog")]
public async Task UpdateMetainfo(
[Option("version", ['v'], Description = "The release version, e.g., 26.1.2. Defaults to the version from the metadata")] string? version = null,
[Option("date", ['d'], Description = "The release date as yyyy-MM-dd. Defaults to the build time from the metadata")] string? date = null)
{
const int APP_VERSION_INDEX = 0;
const int BUILD_TIME_INDEX = 1;
if(!Environment.IsWorkingDirectoryValid())
return;
Console.WriteLine("==============================");
try
{
var metadataLines = SplitLines(await File.ReadAllTextAsync(Environment.GetMetadataPath(), Encoding.UTF8));
var appVersion = string.IsNullOrWhiteSpace(version) ? metadataLines[APP_VERSION_INDEX].Trim() : version.Trim();
if (!ExactAppVersionRegex().IsMatch(appVersion))
throw new InvalidOperationException($"The version '{appVersion}' is not a valid app version.");
DateTime releaseTime;
if (string.IsNullOrWhiteSpace(date))
releaseTime = ParseMetadataBuildTime(metadataLines[BUILD_TIME_INDEX]);
else if (!DateTime.TryParseExact(date.Trim(), "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out releaseTime))
throw new InvalidOperationException($"The release date '{date}' is not a valid date in the yyyy-MM-dd format.");
await WriteMetainfoRelease(appVersion, releaseTime);
}
catch (InvalidOperationException exception)
{
Console.WriteLine($"- Error: {exception.Message}");
}
}
[Command("update-versions", Description = "The command will update the package versions in the metadata file")]
public async Task UpdateVersions()
{
@ -154,10 +188,20 @@ public sealed partial class UpdateMetadataCommands
var appVersion = await this.UpdateAppVersion(action, version);
if (!string.IsNullOrWhiteSpace(appVersion.VersionText))
{
// The changelog is the source for the AppStream description. Check it before we write
// any further metadata, so that a missing changelog cannot leave a half-prepared release:
var changelogPath = GetChangelogPath(appVersion.VersionText);
if (!File.Exists(changelogPath))
{
Console.WriteLine($"- Error: The changelog file '{Path.GetFileName(changelogPath)}' does not exist.");
return;
}
var buildNumber = await this.IncreaseBuildNumber();
var buildTime = await this.UpdateBuildTime();
await this.UpdateChangelog(buildNumber, appVersion.VersionText, buildTime);
await this.CreateNextChangelog(buildNumber, appVersion);
await WriteMetainfoRelease(appVersion.VersionText, ParseMetadataBuildTime(buildTime));
await this.UpdateProjectCommitHash();
await this.UpdateReleaseDependenciesAndLicence();
Console.WriteLine();
@ -413,9 +457,7 @@ public sealed partial class UpdateMetadataCommands
if (!ExactAppVersionRegex().IsMatch(appVersion))
throw new InvalidOperationException($"The metadata version '{appVersion}' is not a valid app version.");
if (!DateTime.TryParseExact(metadataLines[BUILD_TIME_INDEX].Trim(), "yyyy-MM-dd HH:mm:ss 'UTC'", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var buildTime))
throw new InvalidOperationException($"The metadata build time '{metadataLines[BUILD_TIME_INDEX]}' is not a valid UTC build time.");
var buildTime = ParseMetadataBuildTime(metadataLines[BUILD_TIME_INDEX]);
if (!int.TryParse(metadataLines[BUILD_NUMBER_INDEX].Trim(), out var buildNumber))
throw new InvalidOperationException($"The metadata build number '{metadataLines[BUILD_NUMBER_INDEX]}' is not a number.");
@ -455,19 +497,15 @@ public sealed partial class UpdateMetadataCommands
throw new InvalidOperationException($"Expected exactly one future changelog reserving build {nextChangelogBuildNumber}, but found {nextChangelogCandidates.Count}.");
var nextChangelog = nextChangelogCandidates[0];
var metainfoPath = Path.Combine(Environment.GetRustRuntimeDirectory(), "packaging", "linux", "org.mindworkai.AIStudio.metainfo.xml");
// The release entry itself is written by ApplyRebuildReleaseState, which adds it when it is
// missing and moves it to the top otherwise. Here, we only ensure that there is a file to write to:
var metainfoPath = GetMetainfoPath();
if (!File.Exists(metainfoPath))
throw new InvalidOperationException("The AppStream metainfo file does not exist.");
var metainfoContent = await File.ReadAllTextAsync(metainfoPath, Encoding.UTF8);
var releaseTags = ReleaseTagRegex().Matches(metainfoContent).Cast<Match>().ToList();
var matchingReleaseTags = releaseTags.Where(match => ReleaseTagHasVersion(match.Value, appVersion)).ToList();
if (matchingReleaseTags.Count != 1 || releaseTags.Count == 0 || matchingReleaseTags[0].Index != releaseTags[0].Index)
throw new InvalidOperationException($"The AppStream metainfo must contain v{appVersion} exactly once as its first release.");
var metainfoReleaseTag = matchingReleaseTags[0].Value;
if (!StableReleaseTypeRegex().IsMatch(metainfoReleaseTag) || !ReleaseDateRegex().IsMatch(metainfoReleaseTag))
throw new InvalidOperationException($"The AppStream entry for v{appVersion} must be stable and contain a release date.");
if (!ReleasesStartRegex().IsMatch(await File.ReadAllTextAsync(metainfoPath, Encoding.UTF8)))
throw new InvalidOperationException("The AppStream metainfo does not contain a <releases> element.");
var headCommitHash = (await this.ReadCommandOutput(Environment.GetAIStudioDirectory(), "git", "rev-parse HEAD")).Trim();
if (!GitCommitHashRegex().IsMatch(headCommitHash))
@ -489,9 +527,6 @@ public sealed partial class UpdateMetadataCommands
nextChangelog.Content,
nextChangelog.Header,
nextChangelog.Version,
metainfoPath,
metainfoContent,
metainfoReleaseTag,
headCommitHash[..11]);
}
@ -530,11 +565,119 @@ public sealed partial class UpdateMetadataCommands
await File.WriteAllTextAsync(releaseState.NextChangelogPath, updatedNextChangelog, Environment.UTF8_NO_BOM);
Console.WriteLine($"- Reserved build {buildNumber + 1} for '{Path.GetFileName(releaseState.NextChangelogPath)}'.");
var releaseDate = buildTime.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
var updatedMetainfoReleaseTag = ReleaseDateRegex().Replace(releaseState.MetainfoReleaseTag, $"date=\"{releaseDate}\"", 1);
var updatedMetainfo = ReplaceExactlyOnce(releaseState.MetainfoContent, releaseState.MetainfoReleaseTag, updatedMetainfoReleaseTag);
await File.WriteAllTextAsync(releaseState.MetainfoPath, updatedMetainfo, Environment.UTF8_NO_BOM);
Console.WriteLine($"- Updated the AppStream release date to '{releaseDate}'.");
await WriteMetainfoRelease(releaseState.AppVersion, buildTime);
}
private static string GetMetainfoPath() => Path.Combine(Environment.GetRustRuntimeDirectory(), "packaging", "linux", "org.mindworkai.AIStudio.metainfo.xml");
private static string GetChangelogPath(string appVersion) => Path.Combine(Environment.GetAIStudioDirectory(), "wwwroot", "changelog", $"v{appVersion}.md");
/// <summary>
/// Writes the AppStream release entry for the given version, using the changelog of that version as its description.
/// </summary>
/// <remarks>
/// The entry always becomes the first release, and any earlier entry of the same version is replaced. This is what
/// the Flatpak pipeline validates through 'update-metainfo.py --check' before it syncs a release. The release date
/// is derived from the build time, because the pipeline reads it from the second line of the metadata file.
/// </remarks>
private static async Task WriteMetainfoRelease(string appVersion, DateTime releaseTime)
{
const string RELEASE_INDENT = " ";
var metainfoPath = GetMetainfoPath();
if (!File.Exists(metainfoPath))
throw new InvalidOperationException("The AppStream metainfo file does not exist.");
var metainfo = await File.ReadAllTextAsync(metainfoPath, Encoding.UTF8);
if (!ReleasesStartRegex().IsMatch(metainfo))
throw new InvalidOperationException("The AppStream metainfo does not contain a <releases> element.");
var changelogEntries = await ReadChangelogEntries(appVersion);
// Drop any earlier entry of this version, so that the version stays unique and moves to the top.
// We remove from the back, so that the index of the remaining matches stays valid:
foreach (var previousRelease in ReleaseBlockRegex().Matches(metainfo).Cast<Match>().Where(match => ReleaseTagHasVersion(match.Value, appVersion)).Reverse())
metainfo = metainfo.Remove(previousRelease.Index, previousRelease.Length);
var lineEnding = metainfo.Contains("\r\n", StringComparison.Ordinal) ? "\r\n" : "\n";
var releaseDate = releaseTime.ToUniversalTime().ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
var releaseBlock = new StringBuilder();
releaseBlock.Append($"{RELEASE_INDENT}<release type=\"stable\" version=\"{appVersion}\" date=\"{releaseDate}\">{lineEnding}");
releaseBlock.Append($"{RELEASE_INDENT} <description>{lineEnding}");
releaseBlock.Append($"{RELEASE_INDENT} <ul>{lineEnding}");
foreach (var changelogEntry in changelogEntries)
releaseBlock.Append($"{RELEASE_INDENT} <li>{changelogEntry}</li>{lineEnding}");
releaseBlock.Append($"{RELEASE_INDENT} </ul>{lineEnding}");
releaseBlock.Append($"{RELEASE_INDENT} </description>{lineEnding}");
releaseBlock.Append($"{RELEASE_INDENT}</release>{lineEnding}");
var releasesStart = ReleasesStartRegex().Match(metainfo);
var insertionPoint = releasesStart.Index + releasesStart.Length;
if (metainfo.AsSpan(insertionPoint).StartsWith(lineEnding))
insertionPoint += lineEnding.Length;
else
releaseBlock.Insert(0, lineEnding);
metainfo = metainfo.Insert(insertionPoint, releaseBlock.ToString());
await File.WriteAllTextAsync(metainfoPath, metainfo, Environment.UTF8_NO_BOM);
Console.WriteLine($"- Updated the AppStream metainfo for v{appVersion}, released on {releaseDate}, with {changelogEntries.Count} changelog entries.");
}
private static async Task<IReadOnlyList<string>> ReadChangelogEntries(string appVersion)
{
var changelogPath = GetChangelogPath(appVersion);
if (!File.Exists(changelogPath))
throw new InvalidOperationException($"The changelog file '{Path.GetFileName(changelogPath)}' does not exist.");
// The first line is the changelog header, every other non-empty line must be a changelog entry:
var changelogLines = SplitLines(await File.ReadAllTextAsync(changelogPath, Encoding.UTF8));
var changelogEntries = new List<string>();
foreach (var changelogLine in changelogLines.Skip(1))
{
var changelogEntry = changelogLine.Trim();
if (changelogEntry.Length is 0)
continue;
if (!changelogEntry.StartsWith("- ", StringComparison.Ordinal))
throw new InvalidOperationException($"The changelog '{Path.GetFileName(changelogPath)}' contains a line which is no changelog entry: '{changelogEntry}'.");
changelogEntries.Add(ConvertChangelogEntryToAppStream(changelogEntry[2..].Trim()));
}
if (changelogEntries.Count is 0)
throw new InvalidOperationException($"The changelog '{Path.GetFileName(changelogPath)}' does not contain any entry.");
return changelogEntries;
}
private static string ConvertChangelogEntryToAppStream(string changelogEntry)
{
var escapedEntry = changelogEntry
.Replace("&", "&amp;", StringComparison.Ordinal)
.Replace("<", "&lt;", StringComparison.Ordinal)
.Replace(">", "&gt;", StringComparison.Ordinal);
// Markdown code spans become AppStream code elements. Every second segment is inside a code span,
// which requires an even number of markers and therefore an odd number of segments:
var codeSpans = escapedEntry.Split('`');
if (codeSpans.Length % 2 is 0)
throw new InvalidOperationException($"The changelog entry contains an unbalanced code marker: '{changelogEntry}'.");
var convertedEntry = new StringBuilder();
for (var index = 0; index < codeSpans.Length; index++)
convertedEntry.Append(index % 2 is 0 ? codeSpans[index] : $"<code>{codeSpans[index]}</code>");
return convertedEntry.ToString();
}
private static DateTime ParseMetadataBuildTime(string buildTime)
{
if (!DateTime.TryParseExact(buildTime.Trim(), "yyyy-MM-dd HH:mm:ss 'UTC'", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var parsedBuildTime))
throw new InvalidOperationException($"The metadata build time '{buildTime}' is not a valid UTC build time.");
return parsedBuildTime;
}
private static string FormatChangelogHeader(string appVersion, int buildNumber, DateTime buildTime)
@ -983,9 +1126,6 @@ public sealed partial class UpdateMetadataCommands
string NextChangelogContent,
string NextChangelogHeader,
string NextChangelogVersion,
string MetainfoPath,
string MetainfoContent,
string MetainfoReleaseTag,
string HeadCommitHash);
[GeneratedRegex("""(?ms).?(NET\s+SDK|SDK\s+\.NET)\s*:\s+Version:\s+(?<sdkVersion>[0-9.]+).+Commit:\s+(?<sdkCommit>[a-zA-Z0-9]+).+Host:\s+Version:\s+(?<hostVersion>[0-9.]+).+Commit:\s+(?<hostCommit>[a-zA-Z0-9]+)""")]
@ -1015,14 +1155,13 @@ public sealed partial class UpdateMetadataCommands
[GeneratedRegex("""^[0-9]+\.[0-9]+\.[0-9]+$""")]
private static partial Regex ExactAppVersionRegex();
[GeneratedRegex("""<release\b[^>]*>""")]
private static partial Regex ReleaseTagRegex();
[GeneratedRegex("""<releases\b[^>]*>""")]
private static partial Regex ReleasesStartRegex();
[GeneratedRegex("\\btype=\"stable\"")]
private static partial Regex StableReleaseTypeRegex();
[GeneratedRegex("\\bdate=\"[^\"]*\"")]
private static partial Regex ReleaseDateRegex();
// Matches one entire release element, including its indentation and its trailing line break. The
// self-closing form comes first, so that it is never mistaken for the start of a longer element:
[GeneratedRegex("""(?ms)^[ \t]*<release\b[^>]*/>[ \t]*\r?\n?|^[ \t]*<release\b[^>]*>.*?</release>[ \t]*\r?\n?""")]
private static partial Regex ReleaseBlockRegex();
[GeneratedRegex("^[0-9a-fA-F]{40,64}$")]
private static partial Regex GitCommitHashRegex();

View File

@ -1,5 +1,4 @@
using System.Text;
using System.Diagnostics.CodeAnalysis;
using AIStudio.Chat;
using AIStudio.Dialogs;
@ -371,11 +370,10 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
await this.SettingsManager.StoreSettings();
}
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
private void UpdateProviders()
{
this.availableLLMProviders.Clear();
foreach (var provider in this.SettingsManager.ConfigurationData.Providers)
foreach (var provider in this.SettingsManager.GetAllProviders())
this.availableLLMProviders.Add(new ConfigurationSelectData<string>(provider.InstanceName, provider.Id));
}
@ -459,7 +457,6 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
await this.AutoSave(true);
}
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed", Justification = "Policy-specific preselection needs to probe providers by id before falling back to SettingsManager APIs.")]
private void ApplyPolicyPreselection(bool preferPolicyPreselection = false)
{
if (this.selectedPolicy is null)
@ -480,8 +477,8 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
}
// Try to apply the policy preselection:
var policyProvider = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.selectedPolicy.PreselectedProvider);
if (policyProvider is not null && policyProvider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level >= minimumLevel)
var policyProvider = this.SettingsManager.GetProviderById(this.selectedPolicy.PreselectedProvider);
if (policyProvider != Settings.Provider.NONE && policyProvider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level >= minimumLevel)
{
this.ProviderSettings = policyProvider;
this.CurrentProfile = this.ResolveProfileSelection();

View File

@ -4357,9 +4357,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T80509
-- Example text to embed
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T816748904"] = "Example text to embed"
-- Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T900237532"] = "Provider"
-- Export configuration
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T975426229"] = "Export configuration"
@ -4435,9 +4432,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T579100
-- Open Dashboard
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T78223861"] = "Open Dashboard"
-- Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T900237532"] = "Provider"
-- Export configuration
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T975426229"] = "Export configuration"
@ -4498,9 +4492,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T78
-- Are you sure you want to delete the transcription provider '{0}'?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T789660305"] = "Are you sure you want to delete the transcription provider '{0}'?"
-- Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T900237532"] = "Provider"
-- Export configuration
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T975426229"] = "Export configuration"
@ -8170,15 +8161,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T3341379752"] = "Cost-effective"
-- Flexibility
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T3723223888"] = "Flexibility"
-- You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities.
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T3892227145"] = "You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities."
-- Privacy
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T3959064551"] = "Privacy"
-- You can control which providers receive your data using the provider confidence settings. For example, you can set different protection levels for writing emails compared to general chats, etc. Additionally, most providers guarantee that they won't use your data to train new AI systems.
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T457410099"] = "You can control which providers receive your data using the provider confidence settings. For example, you can set different protection levels for writing emails compared to general chats, etc. Additionally, most providers guarantee that they won't use your data to train new AI systems."
-- You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hetzner (experimental, open-source models hosted in the EU), Hugging Face, Groq, Fireworks, and self-hosted models using vLLM, llama.cpp, ollama, or LM Studio. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities.
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T558496815"] = "You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hetzner (experimental, open-source models hosted in the EU), Hugging Face, Groq, Fireworks, and self-hosted models using vLLM, llama.cpp, ollama, or LM Studio. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities."
-- Free of charge
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T617579208"] = "Free of charge"
@ -8905,6 +8896,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T1014558951"] = "The trust leve
-- You or your organization operate the LLM locally or within your trusted network. In terms of data processing and security, this is the best possible way.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T2124364471"] = "You or your organization operate the LLM locally or within your trusted network. In terms of data processing and security, this is the best possible way."
-- The provider operates its service in the EU and is subject to the **GDPR** (General Data Protection Regulation). It provides access to **open source models**. However, the service is currently **experimental**, and performance and availability are not guaranteed. We have no provider-specific information about whether submitted data is used for training.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T2930312134"] = "The provider operates its service in the EU and is subject to the **GDPR** (General Data Protection Regulation). It provides access to **open source models**. However, the service is currently **experimental**, and performance and availability are not guaranteed. We have no provider-specific information about whether submitted data is used for training."
-- The provider is located in the EU and is subject to the **GDPR** (General Data Protection Regulation). Additionally, the provider states that **your data is not used for training**.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T3010553924"] = "The provider is located in the EU and is subject to the **GDPR** (General Data Protection Regulation). Additionally, the provider states that **your data is not used for training**."

View File

@ -1,9 +1,8 @@
using System.Diagnostics.CodeAnalysis;
using AIStudio.Assistants.SlideBuilder;
using AIStudio.Chat;
using AIStudio.Settings;
using ComponentKind = AIStudio.Tools.Components;
using ProviderSettings = AIStudio.Settings.Provider;
namespace AIStudio.Assistants.VisualBriefing;
@ -77,7 +76,6 @@ public sealed class VisualBriefingEditorState
/// <param name="briefing">The manifest to read.</param>
/// <param name="settingsManager">The settings used to resolve the stored provider and profile.</param>
/// <returns>The editor state for the briefing.</returns>
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed", Justification = "A stored briefing references one specific provider and model by id, so it must be looked up directly instead of using the preselection APIs.")]
public static VisualBriefingEditorState FromManifest(VisualBriefingManifest briefing, SettingsManager settingsManager) => new()
{
Name = briefing.Name,
@ -94,8 +92,8 @@ public sealed class VisualBriefingEditorState
ProtectionLevel = briefing.Settings.ProtectionLevel,
CustomProtectionLevel = briefing.Settings.CustomProtectionLevel,
Provider = settingsManager.ConfigurationData.Providers.FirstOrDefault(candidate => candidate.Id == briefing.Settings.ProviderId && candidate.Model.Id == briefing.Settings.ModelId) ?? ProviderSettings.NONE,
Profile = settingsManager.ConfigurationData.Profiles.FirstOrDefault(candidate => candidate.Id == briefing.Settings.ProfileId) ?? Profile.NO_PROFILE,
Provider = ResolveProvider(briefing, settingsManager),
Profile = settingsManager.GetProfileById(briefing.Settings.ProfileId),
SourceMaterial =
[
@ -112,6 +110,43 @@ public sealed class VisualBriefingEditorState
],
};
/// <summary>
/// Resolves the provider a stored briefing refers to.
/// </summary>
/// <remarks>
/// <para>
/// A briefing stores its provider and model as two separate ids, and both must still match: when
/// the user changed the model of that provider, the stored combination no longer exists and the
/// editor starts without a provider.
/// </para>
/// <para>
/// The resolved provider is additionally checked against the minimum confidence level of the
/// visual briefing assistant. This matters because the confidence settings may have become
/// stricter since the briefing was stored: the user may have lowered the confidence of that
/// provider, or may now enforce a global minimum. Without this check, opening an old briefing
/// would silently restore a provider the user no longer trusts, bypassing the filtering that
/// the provider dropdown applies. Note that the component minimum already covers the enforced
/// global minimum as well.
/// </para>
/// </remarks>
/// <param name="briefing">The manifest to read.</param>
/// <param name="settingsManager">The settings used to resolve the provider.</param>
/// <returns>The stored provider, or <see cref="ProviderSettings.NONE"/> when it is unavailable or no longer trusted.</returns>
private static ProviderSettings ResolveProvider(VisualBriefingManifest briefing, SettingsManager settingsManager)
{
var storedProvider = settingsManager.GetProviderById(briefing.Settings.ProviderId);
if (storedProvider == ProviderSettings.NONE)
return ProviderSettings.NONE;
if (storedProvider.Model.Id != briefing.Settings.ModelId)
return ProviderSettings.NONE;
if (!settingsManager.IsProviderConfident(storedProvider, ComponentKind.VISUAL_BRIEFING_ASSISTANT))
return ProviderSettings.NONE;
return storedProvider;
}
/// <summary>
/// Creates the persisted settings for this editor state.
/// </summary>

View File

@ -13,6 +13,7 @@ public partial class Changelog
public static readonly Log[] LOGS =
[
new (251, "v26.8.1, build 251 (2026-08-13 06:01 UTC)", "v26.8.1.md"),
new (250, "v26.7.3, build 250 (2026-07-21 12:45 UTC)", "v26.7.3.md"),
new (244, "v26.7.2, build 244 (2026-07-06 18:35 UTC)", "v26.7.2.md"),
new (243, "v26.7.1, build 243 (2026-07-05 16:39 UTC)", "v26.7.1.md"),

View File

@ -5,11 +5,11 @@
<MudTooltip Text="@T("Shows and hides the confidence card with information about the selected LLM provider.")" Placement="Placement.Top">
@if (this.Mode is PopoverTriggerMode.ICON)
{
<MudIconButton Icon="@Icons.Material.Filled.Security" Class="confidence-icon" Style="@this.LLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager)" OnClick="@(() => this.ToggleConfidence())"/>
<MudIconButton Icon="@Icons.Material.Filled.Security" Class="confidence-icon" Style="@this.LLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager)" OnClick="@this.ToggleConfidence"/>
}
else
{
<MudButton Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.Security" IconClass="confidence-icon" Style="@this.LLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager)" OnClick="@(() => this.ToggleConfidence())">
<MudButton Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.Security" IconClass="confidence-icon" Style="@this.LLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager)" OnClick="@this.ToggleConfidence">
@T("Confidence")
</MudButton>
}
@ -28,7 +28,7 @@
<MudText Typo="Typo.h6">
@T("Description")
</MudText>
<MudMarkdown Value="@this.currentConfidence.Description" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
<MudJustifiedMarkdown Value="@this.currentConfidence.Description" />
@if (this.currentConfidence.Sources.Count > 0)
{
@ -61,7 +61,7 @@
</MudText>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" OnClick="@(() => this.HideConfidence())">
<MudButton Variant="Variant.Filled" OnClick="@this.HideConfidence">
Close
</MudButton>
</MudCardActions>

View File

@ -1,5 +1,3 @@
using System.Diagnostics.CodeAnalysis;
using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Tools.PluginSystem;
@ -35,27 +33,20 @@ public partial class ConfigurationProviderSelection : MSGComponentBase
[Parameter]
public Func<bool> IsLocked { get; set; } = () => false;
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
private IEnumerable<ConfigurationSelectData<string>> FilteredData()
{
if(this.Component is not Tools.Components.NONE and not Tools.Components.APP_SETTINGS)
yield return new(T("Use app default"), string.Empty);
// Get the minimum confidence level for this component, and/or the enforced global minimum confidence level:
var minimumLevel = this.SettingsManager.GetMinimumConfidenceLevel(this.Component);
// Apply the explicit minimum confidence level if set and higher than the current minimum level:
if (this.ExplicitMinimumConfidence is not ConfidenceLevel.UNKNOWN && this.ExplicitMinimumConfidence > minimumLevel)
minimumLevel = this.ExplicitMinimumConfidence;
// Filter the providers based on the minimum confidence level:
//
// Filter the providers based on the minimum confidence level of this component, the enforced
// global minimum, and the explicit minimum level when it is higher. Providers which no longer
// exist resolve to `Provider.NONE` and are dropped by the confidence check as well:
//
foreach (var providerId in this.Data)
{
var provider = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == providerId.Value);
if (provider is null)
continue;
if (provider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level >= minimumLevel)
var provider = this.SettingsManager.GetProviderById(providerId.Value);
if (this.SettingsManager.IsProviderConfident(provider, this.Component, this.ExplicitMinimumConfidence))
yield return providerId;
}
}

View File

@ -1,5 +1,3 @@
using System.Diagnostics.CodeAnalysis;
using AIStudio.Provider;
using AIStudio.Settings;
@ -83,7 +81,6 @@ public partial class ProviderSelection : MSGComponentBase
_ => this.T("Uses reasoning (thinking)"),
};
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
private IEnumerable<AIStudio.Settings.Provider> GetAvailableProviders()
{
switch (this.Component)
@ -91,25 +88,17 @@ public partial class ProviderSelection : MSGComponentBase
case null:
this.Logger.LogError("Component is null! Cannot filter providers based on component settings. Missed CascadingParameter?");
yield break;
case Tools.Components.NONE:
this.Logger.LogError("Component is NONE! Cannot filter providers based on component settings. Used wrong component?");
yield break;
case { } component:
// Get the minimum confidence level for this component, and/or the global minimum if enforced:
var minimumLevel = this.SettingsManager.GetMinimumConfidenceLevel(component);
// Override with the explicit minimum level if set and higher:
if (this.ExplicitMinimumConfidence is not ConfidenceLevel.UNKNOWN && this.ExplicitMinimumConfidence > minimumLevel)
minimumLevel = this.ExplicitMinimumConfidence;
// Filter providers based on the minimum confidence level:
foreach (var provider in this.SettingsManager.ConfigurationData.Providers)
if (provider.UsedLLMProvider != LLMProviders.NONE)
if (provider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level >= minimumLevel)
yield return provider;
// Filter providers based on the minimum confidence level of this component, the
// enforced global minimum, and the explicit minimum level when it is higher:
foreach (var provider in this.SettingsManager.GetConfidentProviders(component, this.ExplicitMinimumConfidence))
yield return provider;
break;
}
}

View File

@ -91,7 +91,7 @@ public partial class SettingsPanelApp : SettingsPanelBase
yield return new(T("Disable dictation and transcription"), string.Empty);
var minimumLevel = this.SettingsManager.GetMinimumConfidenceLevel(Tools.Components.APP_SETTINGS);
foreach (var provider in this.SettingsManager.ConfigurationData.TranscriptionProviders)
foreach (var provider in this.SettingsManager.GetAllTranscriptionProviders())
{
if (provider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level >= minimumLevel)
yield return new(provider.Name, provider.Id);

View File

@ -17,25 +17,24 @@
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
@T("This helps AI Studio understand and compare things in a way that's similar to how humans do. When you're working on something, AI Studio can automatically identify related documents and data by comparing their digital fingerprints. For instance, if you're writing about customer service, AI Studio can instantly find other documents in your data that discuss similar topics or experiences, even if they use different words.")
</MudJustifiedText>
<MudTable Items="@this.SettingsManager.ConfigurationData.EmbeddingProviders" Hover="@true" Class="border-dashed border rounded-lg">
<MudTable Items="@this.SettingsManager.GetAllEmbeddingProviders()" Hover="@true" GroupBy="@GROUP_CONFIG" Class="border-dashed border rounded-lg">
<ColGroup>
<col style="width: 3em;"/>
<col style="width: 12em;"/>
<col style="width: 12em;"/>
<col/>
<col style="width: 22em;"/>
</ColGroup>
<HeaderContent>
<MudTh>#</MudTh>
<MudTh>@T("Name")</MudTh>
<MudTh>@T("Provider")</MudTh>
<MudTh>@T("Model")</MudTh>
<MudTh>@T("Actions")</MudTh>
</HeaderContent>
<GroupHeaderTemplate>
<MudTh Class="provider-group-header" colspan="4">
@context.Key
</MudTh>
</GroupHeaderTemplate>
<RowTemplate>
<MudTd>@context.Num</MudTd>
<MudTd>@context.Name</MudTd>
<MudTd>@context.UsedLLMProvider.ToName()</MudTd>
<MudTd>@this.GetEmbeddingProviderModelName(context)</MudTd>
<MudTd>

View File

@ -11,6 +11,17 @@ namespace AIStudio.Components.Settings;
public partial class SettingsPanelEmbeddings : SettingsPanelProviderBase
{
/// <summary>
/// Groups the table by the used LLM provider. The embedding provider list is already sorted by
/// that provider, so all instances of one LLM provider form a single, coherent group.
/// </summary>
private static readonly TableGroupDefinition<EmbeddingProvider> GROUP_CONFIG = new()
{
Expandable = true,
IsInitiallyExpanded = false,
Selector = provider => provider.UsedLLMProvider.ToName(),
};
[Parameter]
public List<ConfigurationSelectData<string>> AvailableEmbeddingProviders { get; set; } = new();
@ -131,7 +142,7 @@ public partial class SettingsPanelEmbeddings : SettingsPanelProviderBase
private async Task UpdateEmbeddingProviders()
{
this.AvailableEmbeddingProviders.Clear();
foreach (var provider in this.SettingsManager.ConfigurationData.EmbeddingProviders)
foreach (var provider in this.SettingsManager.GetAllEmbeddingProviders())
this.AvailableEmbeddingProviders.Add(new (provider.Name, provider.Id));
await this.AvailableEmbeddingProvidersChanged.InvokeAsync(this.AvailableEmbeddingProviders);

View File

@ -9,25 +9,24 @@
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
@T("What we call a provider is the combination of an LLM provider such as OpenAI and a model like GPT-4o. You can configure as many providers as you want. This way, you can use the appropriate model for each task. As an LLM provider, you can also choose local providers. However, to use this app, you must configure at least one provider.")
</MudJustifiedText>
<MudTable Items="@this.SettingsManager.ConfigurationData.Providers" Hover="@true" Class="border-dashed border rounded-lg">
<MudTable Items="@this.SettingsManager.GetAllProviders()" Hover="@true" GroupBy="@GROUP_CONFIG" Class="border-dashed border rounded-lg">
<ColGroup>
<col style="width: 3em;"/>
<col style="width: 12em;"/>
<col style="width: 12em;"/>
<col/>
<col style="width: 22em;"/>
</ColGroup>
<HeaderContent>
<MudTh>#</MudTh>
<MudTh>@T("Instance Name")</MudTh>
<MudTh>@T("Provider")</MudTh>
<MudTh>@T("Model")</MudTh>
<MudTh>@T("Actions")</MudTh>
</HeaderContent>
<GroupHeaderTemplate>
<MudTh Class="provider-group-header" colspan="4">
@context.Key
</MudTh>
</GroupHeaderTemplate>
<RowTemplate>
<MudTd>@context.Num</MudTd>
<MudTd>@context.InstanceName</MudTd>
<MudTd>@context.UsedLLMProvider.ToName()</MudTd>
<MudTd>@this.GetLLMProviderModelName(context)</MudTd>
<MudTd>
<MudStack Row="true" Class="mb-2 mt-2" Spacing="1" Wrap="Wrap.Wrap">
@ -72,7 +71,7 @@
</RowTemplate>
</MudTable>
@if(this.SettingsManager.ConfigurationData.Providers.Count == 0)
@if(this.SettingsManager.GetAllProviders().Count == 0)
{
<MudText Typo="Typo.h6" Class="mt-3">
@T("No providers configured yet.")

View File

@ -1,6 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using AIStudio.Dialogs;
using AIStudio.Provider;
using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
@ -11,6 +12,17 @@ namespace AIStudio.Components.Settings;
public partial class SettingsPanelProviders : SettingsPanelProviderBase
{
/// <summary>
/// Groups the table by the used LLM provider. The provider list is already sorted by that
/// provider, so all instances of one LLM provider form a single, coherent group.
/// </summary>
private static readonly TableGroupDefinition<AIStudio.Settings.Provider> GROUP_CONFIG = new()
{
Expandable = true,
IsInitiallyExpanded = false,
Selector = provider => provider.UsedLLMProvider.ToName(),
};
[Parameter]
public List<ConfigurationSelectData<string>> AvailableLLMProviders { get; set; } = new();
@ -27,7 +39,7 @@ public partial class SettingsPanelProviders : SettingsPanelProviderBase
#endregion
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed", Justification = "Managing the provider list is the purpose of this settings panel. Reading providers goes through the settings manager, but adding, editing, and removing them stays here on purpose.")]
private async Task AddLLMProvider()
{
var dialogParameters = new DialogParameters<ProviderDialog>
@ -50,7 +62,7 @@ public partial class SettingsPanelProviders : SettingsPanelProviderBase
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
}
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed", Justification = "Managing the provider list is the purpose of this settings panel. Reading providers goes through the settings manager, but adding, editing, and removing them stays here on purpose.")]
private async Task EditLLMProvider(AIStudio.Settings.Provider provider)
{
if(provider == AIStudio.Settings.Provider.NONE)
@ -105,7 +117,7 @@ public partial class SettingsPanelProviders : SettingsPanelProviderBase
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
}
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed", Justification = "Managing the provider list is the purpose of this settings panel. Reading providers goes through the settings manager, but adding, editing, and removing them stays here on purpose.")]
private async Task DeleteLLMProvider(AIStudio.Settings.Provider provider)
{
var dialogParameters = new DialogParameters<ConfirmDialog>
@ -167,11 +179,10 @@ public partial class SettingsPanelProviders : SettingsPanelProviderBase
return modelName.Length > MAX_LENGTH ? "[...] " + modelName[^Math.Min(MAX_LENGTH, modelName.Length)..] : modelName;
}
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
private async Task UpdateProviders()
{
this.AvailableLLMProviders.Clear();
foreach (var provider in this.SettingsManager.ConfigurationData.Providers)
foreach (var provider in this.SettingsManager.GetAllProviders())
this.AvailableLLMProviders.Add(new (provider.InstanceName, provider.Id));
await this.AvailableLLMProvidersChanged.InvokeAsync(this.AvailableLLMProviders);

View File

@ -13,25 +13,24 @@
@T("With the support of transcription models, MindWork AI Studio can convert human speech into text. This is useful, for example, when you need to dictate text. You can choose from dedicated transcription models, but not multimodal LLMs (large language models) that can handle both speech and text. The configuration of multimodal models is done in the 'Configure providers' section.")
</MudJustifiedText>
<MudTable Items="@this.SettingsManager.ConfigurationData.TranscriptionProviders" Hover="@true" Class="border-dashed border rounded-lg">
<MudTable Items="@this.SettingsManager.GetAllTranscriptionProviders()" Hover="@true" GroupBy="@GROUP_CONFIG" Class="border-dashed border rounded-lg">
<ColGroup>
<col style="width: 3em;"/>
<col style="width: 12em;"/>
<col style="width: 12em;"/>
<col/>
<col style="width: 22em;"/>
</ColGroup>
<HeaderContent>
<MudTh>#</MudTh>
<MudTh>@T("Name")</MudTh>
<MudTh>@T("Provider")</MudTh>
<MudTh>@T("Model")</MudTh>
<MudTh>@T("Actions")</MudTh>
</HeaderContent>
<GroupHeaderTemplate>
<MudTh Class="provider-group-header" colspan="4">
@context.Key
</MudTh>
</GroupHeaderTemplate>
<RowTemplate>
<MudTd>@context.Num</MudTd>
<MudTd>@context.Name</MudTd>
<MudTd>@context.UsedLLMProvider.ToName()</MudTd>
<MudTd>@this.GetTranscriptionProviderModelName(context)</MudTd>
<MudTd>

View File

@ -1,4 +1,5 @@
using AIStudio.Dialogs;
using AIStudio.Provider;
using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
@ -9,6 +10,17 @@ namespace AIStudio.Components.Settings;
public partial class SettingsPanelTranscription : SettingsPanelProviderBase
{
/// <summary>
/// Groups the table by the used LLM provider. The transcription provider list is already sorted by
/// that provider, so all instances of one LLM provider form a single, coherent group.
/// </summary>
private static readonly TableGroupDefinition<TranscriptionProvider> GROUP_CONFIG = new()
{
Expandable = true,
IsInitiallyExpanded = false,
Selector = provider => provider.UsedLLMProvider.ToName(),
};
[Parameter]
public List<ConfigurationSelectData<string>> AvailableTranscriptionProviders { get; set; } = new();
@ -129,7 +141,7 @@ public partial class SettingsPanelTranscription : SettingsPanelProviderBase
private async Task UpdateTranscriptionProviders()
{
this.AvailableTranscriptionProviders.Clear();
foreach (var provider in this.SettingsManager.ConfigurationData.TranscriptionProviders)
foreach (var provider in this.SettingsManager.GetAllTranscriptionProviders())
this.AvailableTranscriptionProviders.Add(new (provider.Name, provider.Id));
await this.AvailableTranscriptionProvidersChanged.InvokeAsync(this.AvailableTranscriptionProviders);

View File

@ -209,9 +209,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES);
// Load the used instance names:
#pragma warning disable MWAIS0001
this.UsedInstanceNames = this.SettingsManager.ConfigurationData.Providers.Select(x => x.InstanceName.ToLowerInvariant()).ToList();
#pragma warning restore MWAIS0001
this.UsedInstanceNames = this.SettingsManager.GetAllProviders().Select(x => x.InstanceName.ToLowerInvariant()).ToList();
this.capabilityOverrides = this.DataCapabilityOverrides ?? new();
this.showExpertSettings = !string.IsNullOrWhiteSpace(this.AdditionalJsonApiParameters) || this.capabilityOverrides.HasOverrides;

View File

@ -1,5 +1,3 @@
using System.Diagnostics.CodeAnalysis;
using AIStudio.Components;
using AIStudio.Settings;
using AIStudio.Tools.Services;
@ -40,18 +38,17 @@ public abstract class SettingsDialogBase : MSGComponentBase
protected void Close() => this.MudDialog.Cancel();
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
private void UpdateProviders()
{
this.AvailableLLMProviders.Clear();
foreach (var provider in this.SettingsManager.ConfigurationData.Providers)
foreach (var provider in this.SettingsManager.GetAllProviders())
this.AvailableLLMProviders.Add(new (provider.InstanceName, provider.Id));
}
private void UpdateEmbeddingProviders()
{
this.AvailableEmbeddingProviders.Clear();
foreach (var provider in this.SettingsManager.ConfigurationData.EmbeddingProviders)
foreach (var provider in this.SettingsManager.GetAllEmbeddingProviders())
this.AvailableEmbeddingProviders.Add(new (provider.Name, provider.Id));
}

View File

@ -53,11 +53,11 @@
<ItemGroup>
<PackageReference Include="CodeBeam.MudBlazor.Extensions" Version="8.3.0" />
<PackageReference Include="HtmlAgilityPack" Version="1.12.4" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.18" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.19" />
<PackageReference Include="MudBlazor" Version="8.15.0" />
<PackageReference Include="MudBlazor.Markdown" Version="8.11.0" />
<PackageReference Include="ReverseMarkdown" Version="5.0.0" />
<PackageReference Include="LuaCSharp" Version="0.5.5" />
<PackageReference Include="LuaCSharp" Version="0.5.6" />
</ItemGroup>
<ItemGroup>

View File

@ -63,7 +63,7 @@ public partial class Home : MSGComponentBase
this.itemsAdvantages = [
new(this.T("Free of charge"), this.T("The app is free to use, both for personal and commercial purposes.")),
new(this.T("Democratization of AI"), this.T("We want to contribute to the democratization of AI. MindWork AI Studio runs even on low-cost hardware, including computers around 100 EUR such as Raspberry Pi. This makes the app and its full feature set accessible to people and families with limited budgets. You can start with local LLMs or use affordable cloud models.")),
new(this.T("Independence"), this.T("You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities.")),
new(this.T("Independence"), this.T("You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hetzner (experimental, open-source models hosted in the EU), Hugging Face, Groq, Fireworks, and self-hosted models using vLLM, llama.cpp, ollama, or LM Studio. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities.")),
new(this.T("Assistants"), this.T("You just want to quickly translate a text? AI Studio has so-called assistants for such and other tasks. No prompting is necessary when working with these assistants.")),
new(this.T("Unrestricted usage"), this.T("Unlike services like ChatGPT, which impose limits after intensive use, MindWork AI Studio offers unlimited usage through the providers API.")),
new(this.T("Cost-effective"), this.T("You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit.")),

View File

@ -642,7 +642,7 @@ CONFIG["SETTINGS"] = {}
-- Configure a custom confidence scheme.
-- This is used when DataConfidence.ConfidenceScheme is set to CUSTOM.
-- Allowed provider keys are: OPEN_AI, ANTHROPIC, MISTRAL, GOOGLE, X, DEEP_SEEK, ALIBABA_CLOUD,
-- PERPLEXITY, OPEN_ROUTER, FIREWORKS, GROQ, HUGGINGFACE, SELF_HOSTED, HELMHOLTZ, GWDG
-- PERPLEXITY, OPEN_ROUTER, HETZNER, FIREWORKS, GROQ, HUGGINGFACE, SELF_HOSTED, HELMHOLTZ, GWDG
-- Allowed confidence values are: UNTRUSTED, VERY_LOW, LOW, MODERATE, MEDIUM, HIGH
--
-- Replaces, does not merge: a configuration with a higher priority replaces the whole
@ -659,6 +659,7 @@ CONFIG["SETTINGS"] = {}
-- ["ALIBABA_CLOUD"] = "LOW",
-- ["PERPLEXITY"] = "MODERATE",
-- ["OPEN_ROUTER"] = "MODERATE",
-- ["HETZNER"] = "HIGH",
-- ["FIREWORKS"] = "MODERATE",
-- ["GROQ"] = "MODERATE",
-- ["HUGGINGFACE"] = "MODERATE",

View File

@ -4359,9 +4359,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T80509
-- Example text to embed
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T816748904"] = "Beispieltext zum Einbetten"
-- Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T900237532"] = "Anbieter"
-- Export configuration
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T975426229"] = "Konfiguration exportieren"
@ -4437,9 +4434,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T579100
-- Open Dashboard
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T78223861"] = "Dashboard öffnen"
-- Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T900237532"] = "Anbieter"
-- Export configuration
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T975426229"] = "Konfiguration exportieren"
@ -4500,9 +4494,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T78
-- Are you sure you want to delete the transcription provider '{0}'?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T789660305"] = "Möchten Sie den Anbieter für Transkriptionen „{0}“ wirklich löschen?"
-- Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T900237532"] = "Anbieter"
-- Export configuration
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T975426229"] = "Konfiguration exportieren"
@ -8172,15 +8163,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T3341379752"] = "Kosteneffizient"
-- Flexibility
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T3723223888"] = "Flexibilität"
-- You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities.
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T3892227145"] = "Sie sind an keinen einzelnen Anbieter gebunden. Stattdessen können Sie den Anbieter wählen, der am besten zu ihren Bedürfnissen passt. Derzeit unterstützen wir OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hugging Face und selbst gehostete Modelle mit vLLM, llama.cpp, ollama, LM Studio, Groq oder Fireworks. Für Wissenschaftler und Mitarbeiter von Forschungseinrichtungen unterstützen wir auch die KI-Dienste von Helmholtz und GWDG. Diese sind über föderierte Anmeldungen wie eduGAIN für alle 18 Helmholtz-Zentren, die Max-Planck-Gesellschaft, die meisten deutschen und viele internationale Universitäten verfügbar."
-- Privacy
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T3959064551"] = "Datenschutz"
-- You can control which providers receive your data using the provider confidence settings. For example, you can set different protection levels for writing emails compared to general chats, etc. Additionally, most providers guarantee that they won't use your data to train new AI systems.
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T457410099"] = "Sie können über die Einstellungen zur Anbietervertrauenswürdigkeit steuern, welche Anbieter ihre Daten erhalten. Zum Beispiel können Sie für das Schreiben von E-Mails einen anderen Schutzlevel festlegen als für allgemeine Chats usw. Außerdem garantieren die meisten Anbieter, dass ihre Daten nicht zum Trainieren neuer KI-Systeme verwendet werden."
-- You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hetzner (experimental, open-source models hosted in the EU), Hugging Face, Groq, Fireworks, and self-hosted models using vLLM, llama.cpp, ollama, or LM Studio. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities.
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T558496815"] = "Sie sind nicht an einen einzelnen Anbieter gebunden. Stattdessen können Sie den Anbieter wählen, der am besten zu Ihren Bedürfnissen passt. Derzeit unterstützen wir OpenAI (GPT-5, o1 usw.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hetzner (experimentell, in der EU gehostete Open-Source-Modelle), Hugging Face, Groq, Fireworks sowie selbst gehostete Modelle mit vLLM, llama.cpp, ollama oder LM Studio. Für Forschende und Mitarbeitende von Forschungseinrichtungen unterstützen wir außerdem die KI-Dienste von Helmholtz und GWDG. Diese stehen über föderierte Anmeldungen wie eduGAIN allen 18 Helmholtz-Zentren, der Max-Planck-Gesellschaft, den meisten deutschen und vielen internationalen Universitäten zur Verfügung."
-- Free of charge
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T617579208"] = "Kostenlos"
@ -8907,6 +8898,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T1014558951"] = "Das Vertrauens
-- You or your organization operate the LLM locally or within your trusted network. In terms of data processing and security, this is the best possible way.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T2124364471"] = "Sie oder ihre Organisation betreiben das LLM lokal oder innerhalb ihres vertrauenswürdigen Netzwerks. In Bezug auf Datenverarbeitung und Sicherheit ist dies die bestmögliche Lösung."
-- The provider operates its service in the EU and is subject to the **GDPR** (General Data Protection Regulation). It provides access to **open source models**. However, the service is currently **experimental**, and performance and availability are not guaranteed. We have no provider-specific information about whether submitted data is used for training.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T2930312134"] = "Der Anbieter betreibt seinen Dienst in der EU und unterliegt der **DSGVO** (Datenschutz-Grundverordnung). Er bietet Zugang zu **Open-Source-Modellen**. Der Dienst befindet sich jedoch derzeit in einer **experimentellen** Phase; Leistung und Verfügbarkeit werden nicht garantiert. Uns liegen keine anbieterspezifischen Informationen dazu vor, ob übermittelte Daten für das Training verwendet werden."
-- The provider is located in the EU and is subject to the **GDPR** (General Data Protection Regulation). Additionally, the provider states that **your data is not used for training**.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T3010553924"] = "Der Anbieter hat seinen Sitz in der EU und unterliegt der **DSGVO** (Datenschutz-Grundverordnung). Außerdem gibt der Anbieter an, dass **ihre Daten nicht zum Training verwendet werden**."

View File

@ -4359,9 +4359,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T80509
-- Example text to embed
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T816748904"] = "Example text to embed"
-- Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T900237532"] = "Provider"
-- Export configuration
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T975426229"] = "Export configuration"
@ -4437,9 +4434,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T579100
-- Open Dashboard
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T78223861"] = "Open Dashboard"
-- Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T900237532"] = "Provider"
-- Export configuration
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T975426229"] = "Export configuration"
@ -4500,9 +4494,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T78
-- Are you sure you want to delete the transcription provider '{0}'?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T789660305"] = "Are you sure you want to delete the transcription provider '{0}'?"
-- Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T900237532"] = "Provider"
-- Export configuration
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T975426229"] = "Export configuration"
@ -8172,15 +8163,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T3341379752"] = "Cost-effective"
-- Flexibility
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T3723223888"] = "Flexibility"
-- You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities.
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T3892227145"] = "You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities."
-- Privacy
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T3959064551"] = "Privacy"
-- You can control which providers receive your data using the provider confidence settings. For example, you can set different protection levels for writing emails compared to general chats, etc. Additionally, most providers guarantee that they won't use your data to train new AI systems.
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T457410099"] = "You can control which providers receive your data using the provider confidence settings. For example, you can set different protection levels for writing emails compared to general chats, etc. Additionally, most providers guarantee that they won't use your data to train new AI systems."
-- You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hetzner (experimental, open-source models hosted in the EU), Hugging Face, Groq, Fireworks, and self-hosted models using vLLM, llama.cpp, ollama, or LM Studio. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities.
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T558496815"] = "You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hetzner (experimental, open-source models hosted in the EU), Hugging Face, Groq, Fireworks, and self-hosted models using vLLM, llama.cpp, ollama, or LM Studio. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities."
-- Free of charge
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T617579208"] = "Free of charge"
@ -8907,6 +8898,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T1014558951"] = "The trust leve
-- You or your organization operate the LLM locally or within your trusted network. In terms of data processing and security, this is the best possible way.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T2124364471"] = "You or your organization operate the LLM locally or within your trusted network. In terms of data processing and security, this is the best possible way."
-- The provider operates its service in the EU and is subject to the **GDPR** (General Data Protection Regulation). It provides access to **open source models**. However, the service is currently **experimental**, and performance and availability are not guaranteed. We have no provider-specific information about whether submitted data is used for training.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T2930312134"] = "The provider operates its service in the EU and is subject to the **GDPR** (General Data Protection Regulation). It provides access to **open source models**. However, the service is currently **experimental**, and performance and availability are not guaranteed. We have no provider-specific information about whether submitted data is used for training."
-- The provider is located in the EU and is subject to the **GDPR** (General Data Protection Regulation). Additionally, the provider states that **your data is not used for training**.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T3010553924"] = "The provider is located in the EU and is subject to the **GDPR** (General Data Protection Regulation). Additionally, the provider states that **your data is not used for training**."

View File

@ -269,10 +269,10 @@ public abstract class BaseProvider : IProvider, ISecretId
{
exception = new();
if (!line.StartsWith("data: ", StringComparison.InvariantCulture))
if (!TryGetServerSentEventData(line, out var jsonData))
return false;
var jsonData = line[6..].Trim();
jsonData = jsonData.Trim();
if (string.IsNullOrWhiteSpace(jsonData) || jsonData is "[DONE]")
return false;
@ -304,6 +304,21 @@ public abstract class BaseProvider : IProvider, ISecretId
}
}
private static bool TryGetServerSentEventData(string line, out string data)
{
const string DATA_PREFIX = "data:";
data = string.Empty;
if (!line.StartsWith(DATA_PREFIX, StringComparison.InvariantCulture))
return false;
data = line[DATA_PREFIX.Length..];
if (data.StartsWith(' '))
data = data[1..];
return true;
}
private static bool IsProviderStreamFailure(JsonElement root)
{
var eventType = TryGetString(root, "type");
@ -661,13 +676,13 @@ public abstract class BaseProvider : IProvider, ISecretId
if (this.TryCreateProviderRequestExceptionFromStreamLine(providerName, line, out var providerRequestException))
throw providerRequestException;
// Skip lines that do not start with "data: ". Regard
// Skip lines that do not start with "data:". According
// to the specification, we only want to read the data lines:
if (!line.StartsWith("data: ", StringComparison.InvariantCulture))
if (!TryGetServerSentEventData(line, out var jsonData))
continue;
// Check if the line is the end of the stream:
if (line.StartsWith("data: [DONE]", StringComparison.InvariantCulture))
if (jsonData is "[DONE]")
yield break;
//
@ -681,10 +696,6 @@ public abstract class BaseProvider : IProvider, ISecretId
try
{
// We know that the line starts with "data: ". Hence, we can
// skip the first 6 characters to get the JSON data after that.
var jsonData = line[6..];
// Deserialize the JSON data:
providerResponse = JsonSerializer.Deserialize<TAnnotation>(jsonData, JSON_SERIALIZER_OPTIONS);
@ -713,10 +724,6 @@ public abstract class BaseProvider : IProvider, ISecretId
TDelta? providerResponse;
try
{
// We know that the line starts with "data: ". Hence, we can
// skip the first 6 characters to get the JSON data after that.
var jsonData = line[6..];
// Deserialize the JSON data:
providerResponse = JsonSerializer.Deserialize<TDelta>(jsonData, JSON_SERIALIZER_OPTIONS);
@ -866,20 +873,19 @@ public abstract class BaseProvider : IProvider, ISecretId
if (line.StartsWith("event: response.completed", StringComparison.InvariantCulture))
yield break;
if (!TryGetServerSentEventData(line, out var jsonData))
continue;
//
// Find delta lines:
//
if (line.StartsWith("""
data: {"type":"response.output_text.delta"
""", StringComparison.InvariantCulture))
if (jsonData.StartsWith("""
{"type":"response.output_text.delta"
""", StringComparison.InvariantCulture))
{
TDelta? providerResponse;
try
{
// We know that the line starts with "data: ". Hence, we can
// skip the first 6 characters to get the JSON data after that.
var jsonData = line[6..];
// Deserialize the JSON data:
providerResponse = JsonSerializer.Deserialize<TDelta>(jsonData, JSON_SERIALIZER_OPTIONS);
@ -903,18 +909,14 @@ public abstract class BaseProvider : IProvider, ISecretId
//
// Find annotation added lines:
//
else if (annotationSupported && line.StartsWith(
else if (annotationSupported && jsonData.StartsWith(
"""
data: {"type":"response.output_text.annotation.added"
{"type":"response.output_text.annotation.added"
""", StringComparison.InvariantCulture))
{
TAnnotation? providerResponse;
try
{
// We know that the line starts with "data: ". Hence, we can
// skip the first 6 characters to get the JSON data after that.
var jsonData = line[6..];
// Deserialize the JSON data:
providerResponse = JsonSerializer.Deserialize<TAnnotation>(jsonData, JSON_SERIALIZER_OPTIONS);

View File

@ -64,6 +64,12 @@ public sealed record Confidence
Level = ConfidenceLevel.MEDIUM,
Description = TB("The provider is located in the EU and is subject to the **GDPR** (General Data Protection Regulation). Additionally, the provider states that **your data is not used for training**."),
};
public static readonly Confidence GDPR_EXPERIMENTAL_OPEN_SOURCE = new()
{
Level = ConfidenceLevel.MEDIUM,
Description = TB("The provider operates its service in the EU and is subject to the **GDPR** (General Data Protection Regulation). It provides access to **open source models**. However, the service is currently **experimental**, and performance and availability are not guaranteed. We have no provider-specific information about whether submitted data is used for training."),
};
public static readonly Confidence SELF_HOSTED = new()
{

View File

@ -0,0 +1,93 @@
using System.Runtime.CompilerServices;
using AIStudio.Chat;
using AIStudio.Provider.OpenAI;
using AIStudio.Settings;
namespace AIStudio.Provider.Hetzner;
public sealed class ProviderHetzner() : BaseProvider(LLMProviders.HETZNER, new Uri("https://inference.hetzner.com/api/v1/"), ExternalHttpTrustPolicy.SYSTEM_TRUST_ONLY, LOGGER)
{
private static readonly ILogger<ProviderHetzner> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ProviderHetzner>();
#region Implementation of IProvider
/// <inheritdoc />
public override string Id => LLMProviders.HETZNER.ToSecretId();
/// <inheritdoc />
public override string InstanceName { get; set; } = "Hetzner (Experimental)";
/// <inheritdoc />
public override bool HasModelLoadingCapability => true;
/// <inheritdoc />
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
{
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>(
"Hetzner",
chatModel,
chatThread,
settingsManager,
async (systemPrompt, apiParameters) =>
{
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
return new ChatCompletionAPIRequest
{
Model = chatModel.Id,
Messages = [systemPrompt, ..messages],
Stream = true,
AdditionalApiParameters = apiParameters
};
},
token: token))
yield return content;
}
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc />
public override async IAsyncEnumerable<ImageURL> StreamImageCompletion(Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default)
{
yield break;
}
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc />
public override Task<TranscriptionResult> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
{
return Task.FromResult(TranscriptionResult.Failure());
}
/// <inheritdoc />
public override Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
{
return Task.FromResult<IReadOnlyList<IReadOnlyList<float>>>([]);
}
/// <inheritdoc />
public override Task<ModelLoadResult> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
{
return this.LoadModelsResponse<ModelsResponse>(SecretStoreType.LLM_PROVIDER, "models", modelResponse => modelResponse.Data, token, apiKeyProvisional);
}
/// <inheritdoc />
public override Task<ModelLoadResult> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
{
return Task.FromResult(ModelLoadResult.FromModels([]));
}
/// <inheritdoc />
public override Task<ModelLoadResult> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
{
return Task.FromResult(ModelLoadResult.FromModels([]));
}
/// <inheritdoc />
public override Task<ModelLoadResult> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
{
return Task.FromResult(ModelLoadResult.FromModels([]));
}
#endregion
}

View File

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

View File

@ -6,6 +6,7 @@ using AIStudio.Provider.Google;
using AIStudio.Provider.Groq;
using AIStudio.Provider.GWDG;
using AIStudio.Provider.Helmholtz;
using AIStudio.Provider.Hetzner;
using AIStudio.Provider.HuggingFace;
using AIStudio.Provider.Mistral;
using AIStudio.Provider.OpenAI;
@ -56,6 +57,7 @@ public static class LLMProvidersExtensions
LLMProviders.ALIBABA_CLOUD => "Alibaba Cloud",
LLMProviders.PERPLEXITY => "Perplexity",
LLMProviders.OPEN_ROUTER => "OpenRouter",
LLMProviders.HETZNER => "Hetzner (Experimental)",
LLMProviders.GROQ => "Groq",
LLMProviders.FIREWORKS => "Fireworks.ai",
@ -91,6 +93,7 @@ public static class LLMProvidersExtensions
LLMProviders.ALIBABA_CLOUD => "Alibaba Cloud",
LLMProviders.PERPLEXITY => "Perplexity",
LLMProviders.OPEN_ROUTER => "OpenRouter",
LLMProviders.HETZNER => "Hetzner",
LLMProviders.GROQ => "Groq",
LLMProviders.FIREWORKS => "Fireworks.ai",
@ -144,6 +147,12 @@ 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.HETZNER => Confidence.GDPR_EXPERIMENTAL_OPEN_SOURCE.WithRegion("Europe, Germany").WithSources(
"https://experiments.hetzner.com/docs/inference",
"https://www.hetzner.com/legal/privacy-policy/",
"https://www.hetzner.com/legal/terms-and-conditions/"
).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)),
@ -180,6 +189,7 @@ public static class LLMProvidersExtensions
LLMProviders.HUGGINGFACE => false,
LLMProviders.PERPLEXITY => false,
LLMProviders.OPEN_ROUTER => true,
LLMProviders.HETZNER => false,
//
// Self-hosted providers are treated as a special case anyway.
@ -209,6 +219,7 @@ public static class LLMProvidersExtensions
// Providers that do not support transcription:
//
LLMProviders.OPEN_ROUTER => false,
LLMProviders.HETZNER => false,
LLMProviders.GROQ => false,
LLMProviders.ANTHROPIC => false,
LLMProviders.X => false,
@ -271,6 +282,7 @@ public static class LLMProvidersExtensions
LLMProviders.ALIBABA_CLOUD => new ProviderAlibabaCloud { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.PERPLEXITY => new ProviderPerplexity { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.OPEN_ROUTER => new ProviderOpenRouter { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.HETZNER => new ProviderHetzner { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.GROQ => new ProviderGroq { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.FIREWORKS => new ProviderFireworks { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
@ -302,6 +314,7 @@ public static class LLMProvidersExtensions
LLMProviders.ALIBABA_CLOUD => "https://account.alibabacloud.com/register/intl_register.htm",
LLMProviders.PERPLEXITY => "https://www.perplexity.ai/account/api",
LLMProviders.OPEN_ROUTER => "https://openrouter.ai/keys",
LLMProviders.HETZNER => "https://experiments.hetzner.com",
LLMProviders.GROQ => "https://console.groq.com/",
LLMProviders.FIREWORKS => "https://fireworks.ai/login",
@ -327,6 +340,7 @@ public static class LLMProvidersExtensions
LLMProviders.PERPLEXITY => "https://www.perplexity.ai/account/api/",
LLMProviders.OPEN_ROUTER => "https://openrouter.ai/activity",
LLMProviders.HUGGINGFACE => "https://huggingface.co/settings/billing",
LLMProviders.HETZNER => "https://experiments.hetzner.com",
_ => string.Empty,
};
@ -345,6 +359,7 @@ public static class LLMProvidersExtensions
LLMProviders.PERPLEXITY => true,
LLMProviders.OPEN_ROUTER => true,
LLMProviders.HUGGINGFACE => true,
LLMProviders.HETZNER => true,
_ => false,
};
@ -422,6 +437,7 @@ public static class LLMProvidersExtensions
LLMProviders.ALIBABA_CLOUD => true,
LLMProviders.PERPLEXITY => true,
LLMProviders.OPEN_ROUTER => true,
LLMProviders.HETZNER => true,
LLMProviders.GROQ => true,
LLMProviders.FIREWORKS => true,
@ -445,6 +461,7 @@ public static class LLMProvidersExtensions
LLMProviders.ALIBABA_CLOUD => true,
LLMProviders.PERPLEXITY => true,
LLMProviders.OPEN_ROUTER => true,
LLMProviders.HETZNER => true,
LLMProviders.GROQ => true,
LLMProviders.FIREWORKS => true,

View File

@ -76,6 +76,16 @@ public static partial class ProviderExtensions
//
if (modelName.IndexOf("deepseek") is not -1)
{
if ((modelName.IndexOf("deepseek-v4-flash") is not -1 ||
modelName.IndexOf("deepseek-v4-pro") is not -1) &&
modelName.IndexOf("-base") is -1)
return
[
Capability.TEXT_INPUT, Capability.TEXT_OUTPUT,
Capability.REASONING_BY_DEFAULT, Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
if(modelName.IndexOf("deepseek-r1") is not -1 ||
modelName.IndexOf("deepseek r1") is not -1)
return [
@ -101,6 +111,16 @@ public static partial class ProviderExtensions
Capability.ALWAYS_REASONING,
Capability.CHAT_COMPLETION_API,
];
// Check for the open-weight Qwen 3.8 checkpoint:
if(modelName.IndexOf("qwen3.8-2.4t-a95b") is not -1)
return
[
Capability.TEXT_INPUT, Capability.TEXT_OUTPUT,
Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
// Check for Qwen 3.5:
if(modelName.IndexOf("qwen3.5") is not -1)
@ -117,11 +137,10 @@ public static partial class ProviderExtensions
if(modelName.IndexOf("qwen3.6") is not -1)
return
[
Capability.TEXT_INPUT, Capability.VIDEO_INPUT,
Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.OPTIONAL_REASONING, Capability.FUNCTION_CALLING,
Capability.REASONING_BY_DEFAULT, Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
@ -139,6 +158,29 @@ public static partial class ProviderExtensions
Capability.CHAT_COMPLETION_API,
];
}
//
// Moonshot AI / Kimi models:
//
if (modelName.IndexOf("kimi-k3") is not -1)
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.VIDEO_INPUT,
Capability.TEXT_OUTPUT,
Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
if (modelName.IndexOf("kimi-k2.7-code") is not -1)
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
//
// Mistral models:
@ -335,6 +377,16 @@ public static partial class ProviderExtensions
//
if (modelName.IndexOf("glm") is not -1)
{
if (modelName.IndexOf("glm-5.2") is not -1)
return
[
Capability.TEXT_INPUT,
Capability.TEXT_OUTPUT,
Capability.REASONING_BY_DEFAULT, Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
if(modelName.IndexOf("v") is not -1)
return
[

View File

@ -90,6 +90,7 @@ public static partial class ProviderExtensions
GetQwenReasoningState(parameters)),
LLMProviders.OPEN_ROUTER or
LLMProviders.HETZNER or
LLMProviders.X or
LLMProviders.DEEP_SEEK or
LLMProviders.GROQ or

View File

@ -54,6 +54,7 @@ public static partial class ProviderExtensions
LLMProviders.ALIBABA_CLOUD => GetModelCapabilitiesAlibaba(model),
LLMProviders.PERPLEXITY => GetModelCapabilitiesPerplexity(model),
LLMProviders.OPEN_ROUTER => GetModelCapabilitiesOpenRouter(model),
LLMProviders.HETZNER => GetModelCapabilitiesOpenSource(model),
LLMProviders.GROQ => GetModelCapabilitiesOpenSource(model),
LLMProviders.FIREWORKS => GetModelCapabilitiesOpenSource(model),

View File

@ -1,4 +1,3 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using System.Text.Json;
@ -434,7 +433,6 @@ public sealed class SettingsManager
return localeTag[..separatorIndex];
}
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
public Provider GetPreselectedProvider(Tools.Components component, string? currentProviderId = null, bool usePreselectionBeforeCurrentProvider = false)
{
var minimumLevel = this.GetMinimumConfidenceLevel(component);
@ -486,15 +484,27 @@ public sealed class SettingsManager
return this.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.ConfigurationData.App.PreselectedProvider && x.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel) ?? Provider.NONE;
}
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
public Provider GetChatProviderForLoadedChat(string? chatProviderId = null)
{
var minimumLevel = this.GetMinimumConfidenceLevel(Tools.Components.CHAT);
bool IsSelectableProvider(Provider provider) =>
provider != Provider.NONE
&& provider.UsedLLMProvider != LLMProviders.NONE
&& provider.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel;
var chatProvider = FindProviderById(chatProviderId);
if (chatProvider is not null)
return chatProvider;
var defaultChatProvider = this.ConfigurationData.Chat.PreselectOptions
? FindProviderById(this.ConfigurationData.Chat.PreselectedProvider)
: null;
if (defaultChatProvider is not null)
return defaultChatProvider;
var defaultAppProvider = FindProviderById(this.ConfigurationData.App.PreselectedProvider);
if (defaultAppProvider is not null)
return defaultAppProvider;
var selectableProviders = this.ConfigurationData.Providers.Where(IsSelectableProvider).ToList();
return selectableProviders.Count == 1 ? selectableProviders[0] : Provider.NONE;
Provider? FindProviderById(string? providerId)
{
@ -505,24 +515,141 @@ public sealed class SettingsManager
return provider is not null && IsSelectableProvider(provider) ? provider : null;
}
var chatProvider = FindProviderById(chatProviderId);
if (chatProvider is not null)
return chatProvider;
var defaultChatProvider = this.ConfigurationData.Chat.PreselectOptions
? FindProviderById(this.ConfigurationData.Chat.PreselectedProvider)
: null;
if (defaultChatProvider is not null)
return defaultChatProvider;
var defaultAppProvider = FindProviderById(this.ConfigurationData.App.PreselectedProvider);
if (defaultAppProvider is not null)
return defaultAppProvider;
var selectableProviders = this.ConfigurationData.Providers.Where(IsSelectableProvider).ToList();
return selectableProviders.Count == 1 ? selectableProviders[0] : Provider.NONE;
bool IsSelectableProvider(Provider provider) =>
provider != Provider.NONE
&& provider.UsedLLMProvider != LLMProviders.NONE
&& provider.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel;
}
/// <summary>
/// Returns all configured providers without applying any confidence filtering.
/// </summary>
/// <remarks>
/// <para>
/// This method applies neither the global minimum confidence level (see
/// <see cref="Data.Confidence"/> with <c>EnforceGlobalMinimumConfidence</c>) nor any
/// component-specific minimum. Even when the user enforces a global minimum of, say,
/// <see cref="ConfidenceLevel.HIGH"/>, this method still returns every configured provider.
/// That is intentional: this method serves the provider management UI, duplicate-name checks,
/// and the raw select data of provider dropdowns. The dropdowns are filtered afterward by
/// ConfigurationProviderSelection, which calls IsProviderConfident.
/// </para>
/// <para>
/// Whenever a provider is about to be used for an LLM request, do not use this method. Use
/// GetConfidentProviders, GetPreselectedProvider, or GetChatProviderForLoadedChat instead,
/// since they honor the confidence levels.
/// </para>
/// <para>
/// The returned list is a sorted copy of the provider list, ordered by the used LLM provider and
/// then by the instance name. This way, all providers of the same LLM provider stay together, and
/// newly added providers appear at their alphabetical position instead of at the end. Callers must
/// not mutate the returned list: adding, editing, or removing providers stays inside the settings UI.
/// </para>
/// </remarks>
/// <returns>All configured providers, unfiltered.</returns>
public IReadOnlyList<Provider> GetAllProviders() => this.ConfigurationData.Providers
.OrderBy(x => x.UsedLLMProvider.ToName(), StringComparer.OrdinalIgnoreCase)
.ThenBy(x => x.InstanceName, StringComparer.OrdinalIgnoreCase)
.ThenBy(x => x.Num)
.ToList();
/// <summary>
/// Returns the provider with the given id, without applying any confidence filtering.
/// </summary>
/// <remarks>
/// This method resolves a stored provider reference by its id. It applies neither the global
/// minimum confidence level nor any component-specific minimum, so it returns the requested
/// provider even when the user enforces a higher global minimum. Callers that intend to use the
/// returned provider for an LLM request must check it themselves through
/// IsProviderConfident or fall back to GetPreselectedProvider.
/// </remarks>
/// <param name="providerId">The id of the provider to look up.</param>
/// <returns>The provider, or <see cref="Provider.NONE"/> when no provider with that id exists.</returns>
public Provider GetProviderById(string? providerId)
{
if (string.IsNullOrWhiteSpace(providerId))
return Provider.NONE;
if (string.Equals(providerId, Provider.NONE.Id, StringComparison.OrdinalIgnoreCase))
return Provider.NONE;
return this.ConfigurationData.Providers.FirstOrDefault(x => x.Id.Equals(providerId, StringComparison.OrdinalIgnoreCase)) ?? Provider.NONE;
}
/// <summary>
/// Determines the minimum confidence level a provider must have for the given component.
/// </summary>
/// <param name="component">The component for which the providers get filtered.</param>
/// <param name="explicitMinimum">An explicit minimum level, which is applied when it is higher than the component's minimum.</param>
/// <returns>The effective minimum confidence level.</returns>
public ConfidenceLevel GetEffectiveMinimumConfidenceLevel(Tools.Components component, ConfidenceLevel explicitMinimum = ConfidenceLevel.UNKNOWN)
{
var minimumLevel = this.GetMinimumConfidenceLevel(component);
if (explicitMinimum is not ConfidenceLevel.UNKNOWN && explicitMinimum > minimumLevel)
return explicitMinimum;
return minimumLevel;
}
/// <summary>
/// Checks whether the given provider satisfies the minimum confidence level of the given component.
/// </summary>
/// <param name="provider">The provider to check.</param>
/// <param name="component">The component for which the provider gets checked.</param>
/// <param name="explicitMinimum">An explicit minimum level, which is applied when it is higher than the component's minimum.</param>
/// <returns>True, when the provider may be used by the component, false otherwise.</returns>
public bool IsProviderConfident(Provider provider, Tools.Components component, ConfidenceLevel explicitMinimum = ConfidenceLevel.UNKNOWN)
{
if (provider.UsedLLMProvider is LLMProviders.NONE)
return false;
return provider.UsedLLMProvider.GetConfidence(this).Level >= this.GetEffectiveMinimumConfidenceLevel(component, explicitMinimum);
}
/// <summary>
/// Returns all providers that satisfy the minimum confidence level of the given component.
/// </summary>
/// <param name="component">The component for which the providers get filtered.</param>
/// <param name="explicitMinimum">An explicit minimum level, which is applied when it is higher than the component's minimum.</param>
/// <returns>All providers the component may use, in the same order as GetAllProviders.</returns>
public IEnumerable<Provider> GetConfidentProviders(Tools.Components component, ConfidenceLevel explicitMinimum = ConfidenceLevel.UNKNOWN)
{
var minimumLevel = this.GetEffectiveMinimumConfidenceLevel(component, explicitMinimum);
foreach (var provider in this.GetAllProviders())
if (provider.UsedLLMProvider is not LLMProviders.NONE && provider.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel)
yield return provider;
}
/// <summary>
/// Returns all configured embedding providers.
/// </summary>
/// <remarks>
/// The returned list is a sorted copy of the embedding provider list, ordered by the used LLM
/// provider and then by the name. Callers must not mutate the returned list: adding, editing, or
/// removing embedding providers stays inside the settings UI.
/// </remarks>
/// <returns>All configured embedding providers.</returns>
public IReadOnlyList<EmbeddingProvider> GetAllEmbeddingProviders() => this.ConfigurationData.EmbeddingProviders
.OrderBy(x => x.UsedLLMProvider.ToName(), StringComparer.OrdinalIgnoreCase)
.ThenBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
.ThenBy(x => x.Num)
.ToList();
/// <summary>
/// Returns all configured transcription providers.
/// </summary>
/// <remarks>
/// The returned list is a sorted copy of the transcription provider list, ordered by the used LLM
/// provider and then by the name. Callers must not mutate the returned list: adding, editing, or
/// removing transcription providers stays inside the settings UI.
/// </remarks>
/// <returns>All configured transcription providers.</returns>
public IReadOnlyList<TranscriptionProvider> GetAllTranscriptionProviders() => this.ConfigurationData.TranscriptionProviders
.OrderBy(x => x.UsedLLMProvider.ToName(), StringComparer.OrdinalIgnoreCase)
.ThenBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
.ThenBy(x => x.Num)
.ToList();
public Profile GetPreselectedProfile(Tools.Components component)
{
var preselection = component.GetProfilePreselection(this);
@ -599,8 +726,9 @@ public sealed class SettingsManager
{
LLMProviders.SELF_HOSTED => ConfidenceLevel.HIGH,
LLMProviders.DEEP_SEEK => ConfidenceLevel.LOW,
LLMProviders.ALIBABA_CLOUD => ConfidenceLevel.LOW,
_ => ConfidenceLevel.MEDIUM,
_ => ConfidenceLevel.MEDIUM,
};
case ConfidenceSchemes.TRUST_USA:
@ -610,7 +738,9 @@ public sealed class SettingsManager
LLMProviders.MISTRAL => ConfidenceLevel.LOW,
LLMProviders.HELMHOLTZ => ConfidenceLevel.LOW,
LLMProviders.GWDG => ConfidenceLevel.LOW,
LLMProviders.HETZNER => ConfidenceLevel.LOW,
LLMProviders.DEEP_SEEK => ConfidenceLevel.LOW,
LLMProviders.ALIBABA_CLOUD => ConfidenceLevel.LOW,
_ => ConfidenceLevel.MEDIUM,
};
@ -622,6 +752,7 @@ public sealed class SettingsManager
LLMProviders.MISTRAL => ConfidenceLevel.MEDIUM,
LLMProviders.HELMHOLTZ => ConfidenceLevel.MEDIUM,
LLMProviders.GWDG => ConfidenceLevel.MEDIUM,
LLMProviders.HETZNER => ConfidenceLevel.MEDIUM,
_ => ConfidenceLevel.LOW,
};
@ -631,6 +762,7 @@ public sealed class SettingsManager
{
LLMProviders.SELF_HOSTED => ConfidenceLevel.HIGH,
LLMProviders.DEEP_SEEK => ConfidenceLevel.MEDIUM,
LLMProviders.ALIBABA_CLOUD => ConfidenceLevel.MEDIUM,
_ => ConfidenceLevel.LOW,
};

View File

@ -1,4 +1,3 @@
using System.Diagnostics.CodeAnalysis;
using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
@ -164,48 +163,42 @@ public static class ComponentsExtensions
_ => default,
};
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
public static AIStudio.Settings.Provider PreselectedProvider(this Components component, SettingsManager settingsManager)
public static AIStudio.Settings.Provider PreselectedProvider(this Components component, SettingsManager settingsManager) => component switch
{
var preselectedProvider = component switch
{
Components.GRAMMAR_SPELLING_ASSISTANT => settingsManager.ConfigurationData.GrammarSpelling.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.GrammarSpelling.PreselectedProvider) : null,
Components.ICON_FINDER_ASSISTANT => settingsManager.ConfigurationData.IconFinder.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.IconFinder.PreselectedProvider) : null,
Components.REWRITE_ASSISTANT => settingsManager.ConfigurationData.RewriteImprove.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.RewriteImprove.PreselectedProvider) : null,
Components.PROMPT_OPTIMIZER_ASSISTANT => settingsManager.ConfigurationData.PromptOptimizer.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.PromptOptimizer.PreselectedProvider) : null,
Components.TRANSLATION_ASSISTANT => settingsManager.ConfigurationData.Translation.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Translation.PreselectedProvider) : null,
Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Agenda.PreselectedProvider) : null,
Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Coding.PreselectedProvider) : null,
Components.TEXT_SUMMARIZER_ASSISTANT => settingsManager.ConfigurationData.TextSummarizer.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.TextSummarizer.PreselectedProvider) : null,
Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.EMail.PreselectedProvider) : null,
Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.LegalCheck.PreselectedProvider) : null,
Components.SYNONYMS_ASSISTANT => settingsManager.ConfigurationData.Synonyms.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Synonyms.PreselectedProvider) : null,
Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.MyTasks.PreselectedProvider) : null,
Components.JOB_POSTING_ASSISTANT => settingsManager.ConfigurationData.JobPostings.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.JobPostings.PreselectedProvider) : null,
Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider) : null,
Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.ERI.PreselectedProvider) : null,
Components.I18N_ASSISTANT => settingsManager.ConfigurationData.I18N.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.I18N.PreselectedProvider) : null,
Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.SlideBuilder.PreselectedProvider) : null,
Components.VISUAL_BRIEFING_ASSISTANT => settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.VisualBriefing.PreselectedProvider),
// The Document Analysis Assistant does not have a preselected provider at the component level.
// The provider is selected per policy instead. We do this inside the Document Analysis Assistant component.
Components.DOCUMENT_ANALYSIS_ASSISTANT => Settings.Provider.NONE,
Components.GRAMMAR_SPELLING_ASSISTANT => settingsManager.ConfigurationData.GrammarSpelling.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.GrammarSpelling.PreselectedProvider) : Settings.Provider.NONE,
Components.ICON_FINDER_ASSISTANT => settingsManager.ConfigurationData.IconFinder.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.IconFinder.PreselectedProvider) : Settings.Provider.NONE,
Components.REWRITE_ASSISTANT => settingsManager.ConfigurationData.RewriteImprove.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.RewriteImprove.PreselectedProvider) : Settings.Provider.NONE,
Components.PROMPT_OPTIMIZER_ASSISTANT => settingsManager.ConfigurationData.PromptOptimizer.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.PromptOptimizer.PreselectedProvider) : Settings.Provider.NONE,
Components.TRANSLATION_ASSISTANT => settingsManager.ConfigurationData.Translation.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Translation.PreselectedProvider) : Settings.Provider.NONE,
Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Agenda.PreselectedProvider) : Settings.Provider.NONE,
Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Coding.PreselectedProvider) : Settings.Provider.NONE,
Components.TEXT_SUMMARIZER_ASSISTANT => settingsManager.ConfigurationData.TextSummarizer.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.TextSummarizer.PreselectedProvider) : Settings.Provider.NONE,
Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.EMail.PreselectedProvider) : Settings.Provider.NONE,
Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.LegalCheck.PreselectedProvider) : Settings.Provider.NONE,
Components.SYNONYMS_ASSISTANT => settingsManager.ConfigurationData.Synonyms.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Synonyms.PreselectedProvider) : Settings.Provider.NONE,
Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.MyTasks.PreselectedProvider) : Settings.Provider.NONE,
Components.JOB_POSTING_ASSISTANT => settingsManager.ConfigurationData.JobPostings.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.JobPostings.PreselectedProvider) : Settings.Provider.NONE,
Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider) : Settings.Provider.NONE,
Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.ERI.PreselectedProvider) : Settings.Provider.NONE,
Components.I18N_ASSISTANT => settingsManager.ConfigurationData.I18N.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.I18N.PreselectedProvider) : Settings.Provider.NONE,
Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.SlideBuilder.PreselectedProvider) : Settings.Provider.NONE,
Components.VISUAL_BRIEFING_ASSISTANT => settingsManager.GetProviderById(settingsManager.ConfigurationData.VisualBriefing.PreselectedProvider),
Components.BATCH_PROCESSING_ASSISTANT => settingsManager.ConfigurationData.BatchProcessing.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.BatchProcessing.PreselectedProvider) : null,
// The Document Analysis Assistant does not have a preselected provider at the component level.
// The provider is selected per policy instead. We do this inside the Document Analysis Assistant component.
Components.DOCUMENT_ANALYSIS_ASSISTANT => Settings.Provider.NONE,
Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Chat.PreselectedProvider) : null,
Components.BATCH_PROCESSING_ASSISTANT => settingsManager.ConfigurationData.BatchProcessing.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.BatchProcessing.PreselectedProvider) : Settings.Provider.NONE,
Components.AGENT_TEXT_CONTENT_CLEANER => settingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.TextContentCleaner.PreselectedAgentProvider) : null,
Components.AGENT_DATA_SOURCE_SELECTION => settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectedAgentProvider) : null,
Components.AGENT_RETRIEVAL_CONTEXT_VALIDATION => settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectAgentOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectedAgentProvider) : null,
Components.AGENT_ASSISTANT_PLUGIN_AUDIT => settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.AssistantPluginAudit.PreselectedAgentProvider),
Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Chat.PreselectedProvider) : Settings.Provider.NONE,
_ => Settings.Provider.NONE,
};
return preselectedProvider ?? Settings.Provider.NONE;
}
Components.AGENT_TEXT_CONTENT_CLEANER => settingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.TextContentCleaner.PreselectedAgentProvider) : Settings.Provider.NONE,
Components.AGENT_DATA_SOURCE_SELECTION => settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectedAgentProvider) : Settings.Provider.NONE,
Components.AGENT_RETRIEVAL_CONTEXT_VALIDATION => settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectAgentOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectedAgentProvider) : Settings.Provider.NONE,
Components.AGENT_ASSISTANT_PLUGIN_AUDIT => settingsManager.GetProviderById(settingsManager.ConfigurationData.AssistantPluginAudit.PreselectedAgentProvider),
_ => Settings.Provider.NONE,
};
public static ProfilePreselection GetProfilePreselection(this Components component, SettingsManager settingsManager)
{

View File

@ -22,28 +22,28 @@
},
"LuaCSharp": {
"type": "Direct",
"requested": "[0.5.5, )",
"resolved": "0.5.5",
"contentHash": "IL44DCbMtEafyiy8DzHFd/f+1pXuDUVFJMCJPAu8vQHNfO3ADSoWSOKMg9Py1za/ZE1K0gs0jll1viInoN+19Q==",
"requested": "[0.5.6, )",
"resolved": "0.5.6",
"contentHash": "ncwP3iXeonYM7bILBjsPPIk12mfvCO4tBFnrt9bjnKEZ8Ugdp+MlKskRUGEbo/WHGKU2dRlhxEyuN17L2PHYfg==",
"dependencies": {
"LuaCSharp.Annotations": "0.5.5",
"LuaCSharp.SourceGenerator": "0.5.5"
"LuaCSharp.Annotations": "0.5.6",
"LuaCSharp.SourceGenerator": "0.5.6"
}
},
"Microsoft.Extensions.FileProviders.Embedded": {
"type": "Direct",
"requested": "[9.0.18, )",
"resolved": "9.0.18",
"contentHash": "+t0Bq5qZZ/zbmO4X70nDMC+anTsNSCxNvjtqXmRiUwh53cNfMoXkB/R95rUO9+yFYhsTR7B302ys9LqXDdIt6g==",
"requested": "[9.0.19, )",
"resolved": "9.0.19",
"contentHash": "Q8pv8Md+VH64ZJM2d3nbLLChTZK7WOq+5Ykp6GpDe11cDSCSYhEiHbw69CigoAT5VYJPNxriZMpPL7dY8UW93Q==",
"dependencies": {
"Microsoft.Extensions.FileProviders.Abstractions": "9.0.18"
"Microsoft.Extensions.FileProviders.Abstractions": "9.0.19"
}
},
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[9.0.18, )",
"resolved": "9.0.18",
"contentHash": "ztGVXB28bi8SeplFmAx+4MkqP1ieA4UNzj/M3qyyz5tLa37Ln8x8LuaXdxzzoOdaucjQBKXSdCMFSbpQaNGIEg=="
"requested": "[9.0.19, )",
"resolved": "9.0.19",
"contentHash": "I9GkKrCVjzxGU1hsKSurOW6P/ABPPHARfc/MTnzIgDb8YjJ/votxKN2z7K+J3DvlQXGH0O7KqdtBseRj8j7eNQ=="
},
"MudBlazor": {
"type": "Direct",
@ -82,13 +82,13 @@
},
"LuaCSharp.Annotations": {
"type": "Transitive",
"resolved": "0.5.5",
"contentHash": "5VcwcTNGCY5YXLz2BRko5/Z0YGd6MZqNsnnfPOsGHHpAtqWPFbD0vtOZR4jUqaQLtQUvl2+WRfmIOhp6L2S0rw=="
"resolved": "0.5.6",
"contentHash": "tb8JLViDSSHpmMmBpxqbr+y+4Dpu6v5p3in7RlAUIi+YD7CMi7BEEI7fTc6o5lzoJxSMbIdzzGzjzXXMF6aaSg=="
},
"LuaCSharp.SourceGenerator": {
"type": "Transitive",
"resolved": "0.5.5",
"contentHash": "2xHKGc1bYXTsmSzZCNmKkuAU6A+1azulNiPY/ICKBSHIgEPMNRQ7JS6PvAClrHe6bk8SKcC/fbba6igtDzDaAw=="
"resolved": "0.5.6",
"contentHash": "IJLlWaIYdpvZ5zO20DidDusqMl5pnoHzrmNmB7UnWMWqOfz1t8LeGfDH8kFTDpGcveFE8YUNeEUa4j6SmWGUPg=="
},
"Markdig": {
"type": "Transitive",
@ -159,10 +159,10 @@
},
"Microsoft.Extensions.FileProviders.Abstractions": {
"type": "Transitive",
"resolved": "9.0.18",
"contentHash": "YqkFlTwnVSMuunsf8IT9b+KySfm6vnMBBM+CKYCfXfjRMQ62uFggVOEu4C2cgR4fXpEO1rZ6utUZC1KoYKgiSg==",
"resolved": "9.0.19",
"contentHash": "raArfuC+4kERkNxWrlCXO7H+QAk5yUtmNxiymr+ItP5HKQfMjLhQ68zdajqmd5kgwIIUiMpPGIhUpf7t9+cC0w==",
"dependencies": {
"Microsoft.Extensions.Primitives": "9.0.18"
"Microsoft.Extensions.Primitives": "9.0.19"
}
},
"Microsoft.Extensions.Localization": {
@ -200,8 +200,8 @@
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
"resolved": "9.0.18",
"contentHash": "hfHudMC5zDlwMrC0HiHOJesSHMvM+CdqjomjcV/YVzFq5dfSpBRvyRLm1n1Bfh41ZpQnyJzqX+YEo95BAmcDAQ=="
"resolved": "9.0.19",
"contentHash": "9hIg8PQiMnpVFIsEHm25Wi1gBrPa2pS1G75uveyDUVLXrNKqBap9WGwQIgO0s6LUgRAOdsSYnbKbNC5b1G5Pcg=="
},
"Microsoft.JSInterop": {
"type": "Transitive",

View File

@ -410,4 +410,25 @@
.code-editor .lua-variable {
color: var(--mw-code-editor-variable, #267f99);
}
/*
* Group headers of the provider tables in the settings (LLM providers, embedding providers,
* transcription providers). The rule targets the entire group row, so that the expand button
* gets the same compact padding and shading as the header cell itself.
*/
tr:has(> .provider-group-header) > .mud-table-cell {
padding-top: 0.25em;
padding-bottom: 0.25em;
background-color: var(--mud-palette-background-gray);
border-top: 1px solid var(--mud-palette-lines-default);
border-bottom: 1px solid var(--mud-palette-lines-default);
}
tr:has(> .provider-group-header) .mud-icon-button {
padding: 0.15em;
}
.provider-group-header {
font-weight: 600;
}

View File

@ -1,4 +1,6 @@
# v26.8.1, build 251 (2026-08-xx xx:xx UTC)
# v26.8.1, build 251 (2026-08-13 06:01 UTC)
- Added Hetzner's experimental inference API as an LLM provider. It runs open-source models in the EU and supports text and image chats through its OpenAI-compatible API.
- Added support for the new open source models DeepSeek V4 Flash and Pro, GLM 5.2, Kimi K2.7 Code and K3, as well as Qwen 3.6 and 3.8.
- Added a prototype Visual Briefing Assistant that turns documents, data, images, audio, and video into self-contained interactive HTML briefings. When you want to test it, you have to enable this preview feature in your app settings.
- Added organization-configurable defaults and visibility controls for the Visual Briefing Assistant.
- Added a share button for assistants, configurations, and language plugins. It uses the native share dialog on Windows and macOS. For Linux, we added an export option, which stores the plugin archive at a location of your choice. When you work on a translation for a new language, you can now hand your current state to testers or to us with one click.
@ -6,12 +8,13 @@
- Added a delete button for assistants, configurations, and language plugins you installed or placed yourself. Until now, such a plugin could only be removed from the data directory by hand, which was especially painful for configurations because they have no on/off switch. Before deleting a configuration, AI Studio lists what disappears with it, such as providers, data sources, and settings that return to their default. When you delete the language plugin you had chosen, AI Studio returns to choosing your language automatically. Plugins shipped with AI Studio and plugins deployed by your IT department cannot be deleted.
- Added options for organizations to disable importing, sharing, and exporting plugins, with a separate option for configuration plugins. Organizations can now let people import assistants while keeping configurations to their IT department.
- Added a priority for configuration plugins. Organizations that deploy several configurations can now decide which one wins: a configuration with a higher priority overrides the settings and providers of a lower one. This allows a company-wide base configuration that each department refines for itself.
- Added the Batch Processing Assistant: process all documents of a folder in one run. Each document is sent to the AI along with your instructions - either a free prompt, one of your document analysis policies, or instructions you import from a file. You choose whether each answer is stored as its own Markdown file or whether all answers are collected in one CSV results table, which you can name yourself. Every run writes a log that lists each document with its processing time, the model, the status, and the reason for any error. When you start another run on the same output folder, we ask whether you want to continue it: documents that failed or are missing from the log are processed again, which is helpful after a crash or when documents exceeded the context window of your model. A single failing document never stops the run, and you can cancel at any time. The assistant was contributed by Jan Erler (`j-erler`) and marks his first contribution to AI Studio. Thank you, Jan, for this wonderful and useful contribution.
- Added the Batch Processing Assistant: process all documents of a folder in one run. Each document is sent to the AI along with your instructions either a free prompt, one of your document analysis policies, or instructions you import from a file. You choose whether each answer is stored as its own Markdown file or whether all answers are collected in one CSV results table, which you can name yourself. Every run writes a log that lists each document with its processing time, the model, the status, and the reason for any error. When you start another run on the same output folder, we ask whether you want to continue it: documents that failed or are missing from the log are processed again, which is helpful after a crash or when documents exceeded the context window of your model. A single failing document never stops the run, and you can cancel at any time. The assistant was contributed by Jens Erler (`j-erler`) and marks his first contribution to AI Studio. Thank you, Jens, for this wonderful and useful contribution.
- Added a way for IT departments to try out a configuration before rolling it out. A configuration placed in the new `.config-tests` directory below the plugins directory acts like one your organization deployed, including the approval of assistant plugins, so a test shows exactly what colleagues will see later. No configuration server is needed for this. AI Studio empties that directory every time it starts, so a test configuration is valid for one session, and the information page reports it while it is active. The Enterprise IT documentation describes the whole procedure.
- Added CSV and TSV files to the file types you can attach. AI Studio was already able to read them, but they could not be selected.
- Improved how your organization's configuration behaves when a configuration plugin is present but cannot be loaded, e.g. because of an error in the plugin. Such a plugin still manages your app, so its settings, providers, data sources, profiles, and chat templates now stay in place instead of being removed.
- Improved reading large files from slow locations such as network drives. AI Studio now waits considerably longer before it gives up, and it tells you when it does.
- Improved how Word documents (`.docx`) and OpenDocument text files (`.odt`) are read. AI Studio now reads them itself instead of handing them to Pandoc, so these documents no longer need a Pandoc installation. It reads them section by section, which keeps even large documents responsive, and it now picks up more of the document: the title, the author, headers and footers, footnotes, endnotes, and comments. This was contributed by Nils Kruthoff (`nilskruthoff`), who also wrote the library behind it. Thank you, Nils, for this great contribution.
- Improved how Word documents (`.docx`) and OpenDocument files (`.odt`) are read. AI Studio now reads them itself instead of handing them to Pandoc, so these documents no longer need a Pandoc installation. It reads them section by section, which keeps even large documents responsive, and it now picks up more of the document: the title, the author, headers and footers, footnotes, endnotes, and comments. This was contributed by Nils Kruthoff (`nilskruthoff`), who also wrote the library behind it. Thank you, Nils, for this great contribution.
- Improved the order of your models. Every list of models is now sorted by provider and name, so all models of one provider stay together. Until now, models appeared in the order they were created, which meant that a model added later always showed up at the end of the list. This was especially confusing when your organization rolled out new models. In the settings, the tables for LLM providers, embeddings, and transcription are now grouped by provider and start collapsed, so even a long list stays easy to survey.
- Changed how approvals for assistant plugins combine when your organization deploys several configurations. They now add up, so a department can approve additional assistant plugins without repeating the approvals of the company-wide configuration. Previously, the last configuration replaced all earlier approvals, which silently required a new security check for those assistants.
- Fixed attached files reaching the AI as empty documents when AI Studio could not read them. The AI then answered as if your file had no content, and nothing pointed to a problem. AI Studio now names the cause instead, for example, an unavailable network drive, a file another program is blocking, a protected PDF, or a scanned PDF without a text layer, and it no longer attaches such a file.
- Fixed files that are open in another program being reported as an unrecognized file type. AI Studio now tells you that the file is currently open elsewhere and asks you to close it. This also works for files on shared network drives, where a colleague might have the file open.
@ -37,4 +40,4 @@
- Fixed which configuration wins when two configuration plugins collide, e.g. by claiming the same plugin ID, by managing the same setting, or by defining the same provider. Previously, this was down to chance, so a local configuration plugin could take over parts of the configuration your IT department deployed. Configurations from your organization now always win, and every ignored attempt is reported in the log.
- Fixed the assistant categories when your organization hides individual assistants. A category heading could stay visible above an empty area, and the Log Viewer could disappear together with the Localization assistant. Each heading now follows the assistants actually shown below it.
- Removed the legacy PowerPoint format (`.ppt`) from the selectable file types. AI Studio has no reader for it, so such a file could be attached but never read. The modern `.pptx` format is not affected.
- Upgraded dependencies to their latest versions to improve security and stability.
- Upgraded dependencies to their latest versions to improve security and stability.

View File

@ -0,0 +1 @@
# v26.8.2, build 252 (2026-08-xx xx:xx UTC)

View File

@ -17,11 +17,16 @@ public sealed class ProviderAccessAnalyzer : DiagnosticAnalyzer
private static readonly string TITLE = "Direct access to `Providers` is not allowed";
private static readonly string MESSAGE_FORMAT = "Direct access to `SettingsManager.ConfigurationData.Providers` is not allowed. Instead, use APIs like `SettingsManager.GetPreselectedProvider`, etc.";
private static readonly string MESSAGE_FORMAT = "Direct access to `SettingsManager.ConfigurationData.Providers` is not allowed. Instead, use APIs like `SettingsManager.GetAllProviders`, `GetProviderById`, `GetConfidentProviders`, `GetPreselectedProvider`, or `GetChatProviderForLoadedChat`.";
private static readonly string DESCRIPTION = MESSAGE_FORMAT;
private const string CATEGORY = "Usage";
/// <summary>
/// The one type which owns the provider list and is therefore allowed to access it directly.
/// </summary>
private const string OWNING_TYPE = "AIStudio.Settings.SettingsManager";
private static readonly DiagnosticDescriptor RULE = new(DIAGNOSTIC_ID, TITLE, MESSAGE_FORMAT, CATEGORY, DiagnosticSeverity.Error, isEnabledByDefault: true, description: DESCRIPTION);
@ -29,7 +34,12 @@ public sealed class ProviderAccessAnalyzer : DiagnosticAnalyzer
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
//
// We analyze generated code as well, because Razor markup ends up in generated files. Without
// this, any `ConfigurationData.Providers` access written directly in a `.razor` file would
// bypass this rule entirely. The Razor compiler maps the diagnostic back to the `.razor` line:
//
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(this.AnalyzeMemberAccess, SyntaxKind.SimpleMemberAccessExpression);
}
@ -42,8 +52,17 @@ public sealed class ProviderAccessAnalyzer : DiagnosticAnalyzer
if (memberAccess.Name.Identifier.Text != "Providers")
return;
//
// The settings manager owns the provider list: it implements the very APIs which all other
// code is meant to use, so it must access `Providers` directly. Exempting it here keeps
// those implementations free of suppression attributes, which would otherwise read as if
// suppressing this rule was a normal thing to do:
//
if (IsOwningType(context.ContainingSymbol))
return;
// Get the full path of the member access:
var fullPath = this.GetFullMemberAccessPath(memberAccess);
var fullPath = GetFullMemberAccessPath(memberAccess);
// Check for the forbidden pattern:
if (fullPath.EndsWith("ConfigurationData.Providers"))
@ -53,7 +72,30 @@ public sealed class ProviderAccessAnalyzer : DiagnosticAnalyzer
}
}
private string GetFullMemberAccessPath(ExpressionSyntax expression)
/// <summary>
/// Checks whether the analyzed node sits inside the type which owns the provider list.
/// </summary>
/// <remarks>
/// The containing symbol is the member the node belongs to, e.g. a method or a property. We walk
/// the chain of containing types so that nested types of the owning type are covered as well.
/// </remarks>
/// <param name="containingSymbol">The symbol containing the analyzed node, which may be null.</param>
/// <returns>True, when the node belongs to the owning type.</returns>
private static bool IsOwningType(ISymbol? containingSymbol)
{
var containingType = containingSymbol as INamedTypeSymbol ?? containingSymbol?.ContainingType;
while (containingType != null)
{
if (containingType.ToDisplayString() == OWNING_TYPE)
return true;
containingType = containingType.ContainingType;
}
return false;
}
private static string GetFullMemberAccessPath(ExpressionSyntax expression)
{
var parts = new List<string>();
while (expression is MemberAccessExpressionSyntax memberAccess)

View File

@ -1,12 +1,12 @@
26.7.3
2026-07-21 12:45:10 UTC
250
9.0.119 (commit 32cc3bdf5e)
9.0.18 (commit d839c41c85)
26.8.1
2026-08-13 06:01:54 UTC
251
9.0.120 (commit 3f97250e38)
9.0.19 (commit 8381bdb01f)
1.97.1 (commit 8bab26f4f)
8.15.0
2.11.5
1e5f07cb010, release
3c789da4654, release
osx-arm64
148.0.7763.0
0.7.2

2
runtime/Cargo.lock generated
View File

@ -4260,7 +4260,7 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mindwork-ai-studio"
version = "26.7.3"
version = "26.8.1"
dependencies = [
"aes 0.9.1",
"apple-native-keyring-store",

View File

@ -1,6 +1,6 @@
[package]
name = "mindwork-ai-studio"
version = "26.7.3"
version = "26.8.1"
edition = "2024"
description = "MindWork AI Studio"
authors = ["Thorsten Sommer"]

View File

@ -102,9 +102,87 @@
</screenshots>
<releases>
<release type="stable" version="26.8.1" date="2026-08-13">
<description>
<ul>
<li>Added Hetzner's experimental inference API as an LLM provider. It runs open-source models in the EU and supports text and image chats through its OpenAI-compatible API.</li>
<li>Added support for the new open source models DeepSeek V4 Flash and Pro, GLM 5.2, Kimi K2.7 Code and K3, as well as Qwen 3.6 and 3.8.</li>
<li>Added a prototype Visual Briefing Assistant that turns documents, data, images, audio, and video into self-contained interactive HTML briefings. When you want to test it, you have to enable this preview feature in your app settings.</li>
<li>Added organization-configurable defaults and visibility controls for the Visual Briefing Assistant.</li>
<li>Added a share button for assistants, configurations, and language plugins. It uses the native share dialog on Windows and macOS. For Linux, we added an export option, which stores the plugin archive at a location of your choice. When you work on a translation for a new language, you can now hand your current state to testers or to us with one click.</li>
<li>Added the option to install plugin archives from your files: use the import button on the plugin page or simply drop an archive onto that page. Assistants, configurations, and language plugins are supported, and plugin archives now have their own file extension <code>.mwplugin</code>. Before installing a configuration, AI Studio shows what it sets up: which LLM providers and data sources it adds and where each of them sends your data, plus how many settings it takes control of. A configuration takes effect right away and has no on/off switch, so please install one only when you trust its source. You can remove it again at any time.</li>
<li>Added a delete button for assistants, configurations, and language plugins you installed or placed yourself. Until now, such a plugin could only be removed from the data directory by hand, which was especially painful for configurations because they have no on/off switch. Before deleting a configuration, AI Studio lists what disappears with it, such as providers, data sources, and settings that return to their default. When you delete the language plugin you had chosen, AI Studio returns to choosing your language automatically. Plugins shipped with AI Studio and plugins deployed by your IT department cannot be deleted.</li>
<li>Added options for organizations to disable importing, sharing, and exporting plugins, with a separate option for configuration plugins. Organizations can now let people import assistants while keeping configurations to their IT department.</li>
<li>Added a priority for configuration plugins. Organizations that deploy several configurations can now decide which one wins: a configuration with a higher priority overrides the settings and providers of a lower one. This allows a company-wide base configuration that each department refines for itself.</li>
<li>Added the Batch Processing Assistant: process all documents of a folder in one run. Each document is sent to the AI along with your instructions either a free prompt, one of your document analysis policies, or instructions you import from a file. You choose whether each answer is stored as its own Markdown file or whether all answers are collected in one CSV results table, which you can name yourself. Every run writes a log that lists each document with its processing time, the model, the status, and the reason for any error. When you start another run on the same output folder, we ask whether you want to continue it: documents that failed or are missing from the log are processed again, which is helpful after a crash or when documents exceeded the context window of your model. A single failing document never stops the run, and you can cancel at any time. The assistant was contributed by Jens Erler (<code>j-erler</code>) and marks his first contribution to AI Studio. Thank you, Jens, for this wonderful and useful contribution.</li>
<li>Added a way for IT departments to try out a configuration before rolling it out. A configuration placed in the new <code>.config-tests</code> directory below the plugins directory acts like one your organization deployed, including the approval of assistant plugins, so a test shows exactly what colleagues will see later. No configuration server is needed for this. AI Studio empties that directory every time it starts, so a test configuration is valid for one session, and the information page reports it while it is active. The Enterprise IT documentation describes the whole procedure.</li>
<li>Added CSV and TSV files to the file types you can attach. AI Studio was already able to read them, but they could not be selected.</li>
<li>Improved how your organization's configuration behaves when a configuration plugin is present but cannot be loaded, e.g. because of an error in the plugin. Such a plugin still manages your app, so its settings, providers, data sources, profiles, and chat templates now stay in place instead of being removed.</li>
<li>Improved reading large files from slow locations such as network drives. AI Studio now waits considerably longer before it gives up, and it tells you when it does.</li>
<li>Improved how Word documents (<code>.docx</code>) and OpenDocument files (<code>.odt</code>) are read. AI Studio now reads them itself instead of handing them to Pandoc, so these documents no longer need a Pandoc installation. It reads them section by section, which keeps even large documents responsive, and it now picks up more of the document: the title, the author, headers and footers, footnotes, endnotes, and comments. This was contributed by Nils Kruthoff (<code>nilskruthoff</code>), who also wrote the library behind it. Thank you, Nils, for this great contribution.</li>
<li>Improved the order of your models. Every list of models is now sorted by provider and name, so all models of one provider stay together. Until now, models appeared in the order they were created, which meant that a model added later always showed up at the end of the list. This was especially confusing when your organization rolled out new models. In the settings, the tables for LLM providers, embeddings, and transcription are now grouped by provider and start collapsed, so even a long list stays easy to survey.</li>
<li>Changed how approvals for assistant plugins combine when your organization deploys several configurations. They now add up, so a department can approve additional assistant plugins without repeating the approvals of the company-wide configuration. Previously, the last configuration replaced all earlier approvals, which silently required a new security check for those assistants.</li>
<li>Fixed attached files reaching the AI as empty documents when AI Studio could not read them. The AI then answered as if your file had no content, and nothing pointed to a problem. AI Studio now names the cause instead, for example, an unavailable network drive, a file another program is blocking, a protected PDF, or a scanned PDF without a text layer, and it no longer attaches such a file.</li>
<li>Fixed files that are open in another program being reported as an unrecognized file type. AI Studio now tells you that the file is currently open elsewhere and asks you to close it. This also works for files on shared network drives, where a colleague might have the file open.</li>
<li>Fixed files with a wrong file extension being reported as empty. AI Studio now recognizes what a file really is by looking at its content, for example, a PowerPoint presentation that was renamed to <code>.txt</code>, and reads it accordingly. It also points out the wrong extension, so you can correct it.</li>
<li>Fixed files whose content is not text being sent as an empty document. AI Studio now tells you that the file is not readable as text, which usually means it carries a wrong file extension.</li>
<li>Fixed executable programs with a harmless file extension being read as text. They are now recognized by their content and refused.</li>
<li>Fixed a single unreadable page of a PDF silently cutting off the rest of the document. The remaining pages are now used, and AI Studio tells you which pages are missing.</li>
<li>Fixed a single unreadable sheet of a spreadsheet silently dropping all remaining sheets.</li>
<li>Fixed PDFs, text files, spreadsheets, and presentations requiring Pandoc. Only HTML files need Pandoc now, so every other file can be attached and read without it.</li>
<li>Fixed attached files that are temporarily unavailable, disappearing from your message without a word. This could happen when a file was stored on a network drive.</li>
<li>Fixed the file preview showing an empty document when reading the file failed. It now shows what went wrong, so the preview again answers what AI Studio will hand to the AI.</li>
<li>Fixed the file preview looking like an empty file while AI Studio was still reading it. Larger documents and PDFs need a moment to be read, and until now that moment looked like a file without any content. The preview now says that it is still loading and shows the content as soon as it is ready.</li>
<li>Fixed problems while reading files being missing from the log file after the first one. This made exactly those issues hard to track down that only appeared later on.</li>
<li>Fixed reset buttons in assistants. As you may have noticed in the Document Analysis Assistant, resetting it could leave content from the previous analysis visible. Reset buttons now clear previous results completely.</li>
<li>Fixed dropping files after you closed a dialog that accepts files itself. Such a dialog takes over dropped files while it is open, but never handed that role back when you closed it. Afterward, the chat and the assistants silently ignored dropped files until you switched to another page. Each time you opened such a dialog again, the problem got worse.</li>
<li>Fixed configuration-managed settings, remaining active after their configuration plugin was removed.</li>
<li>Fixed settings not returning to your own value after a configuration was removed. When a configuration takes control of a setting, AI Studio now remembers the value you had chosen before and hands it back once no configuration manages that setting anymore. This covers an IT department withdrawing a configuration, deleting one yourself, and an administrator ending a test configuration. When a configuration only suggested a value, and you changed it afterward, your choice stays as it is.</li>
<li>Fixed the integrated code editor to keep errors and other issues in plugin code visible in the footer while scrolling.</li>
<li>Fixed the trusted badge so you can now see at a glance which models are trusted. It is shown consistently for self-hosted models and models from trusted providers.</li>
<li>Fixed approvals for assistant plugins being accepted from any configuration plugin. An approval marks an assistant as safe without a security check, and the app states that your organization approved it. Only configurations your IT department deploys, or that an administrator stages for a test, can do that now; approvals from any other locally placed configuration plugin are ignored and reported in the log.</li>
<li>Fixed withdrawing a configuration your organization deployed. A configuration that declared itself as locally managed stayed on the device even after the IT department stopped deploying it, and it kept every right of an organization configuration, such as approving assistant plugins. Where a configuration is stored now decides this instead of what the configuration says about itself, so withdrawing one always takes effect. This also applies to a device that was offline while the organization changed its policy: the withdrawal is applied when AI Studio starts again.</li>
<li>Fixed preview features contributed by several configuration plugins at once. Only the most recent contribution was recognized as coming from your organization, so features enabled by another configuration looked as if you had switched them on yourself. Each configuration is now tracked separately, which lets your organization enable one preview feature company-wide and another one for a single department.</li>
<li>Fixed which configuration wins when two configuration plugins collide, e.g. by claiming the same plugin ID, by managing the same setting, or by defining the same provider. Previously, this was down to chance, so a local configuration plugin could take over parts of the configuration your IT department deployed. Configurations from your organization now always win, and every ignored attempt is reported in the log.</li>
<li>Fixed the assistant categories when your organization hides individual assistants. A category heading could stay visible above an empty area, and the Log Viewer could disappear together with the Localization assistant. Each heading now follows the assistants actually shown below it.</li>
<li>Removed the legacy PowerPoint format (<code>.ppt</code>) from the selectable file types. AI Studio has no reader for it, so such a file could be attached but never read. The modern <code>.pptx</code> format is not affected.</li>
<li>Upgraded dependencies to their latest versions to improve security and stability.</li>
</ul>
</description>
</release>
<release type="stable" version="26.7.3" date="2026-07-21">
<description>
<p>Update</p>
<ul>
<li>Added support for OpenAI GPT-5.6 Sol, Terra, and Luna; Anthropic Claude Fable 5 and Mythos 5; and Google Gemini 3 Flash, Gemini 3.1 Flash-Lite, Gemini 3.1 Pro, and Gemini 3.5 Flash.</li>
<li>Added support for OpenDocument presentations (<code>.odp</code>) when attaching and reading presentation files.</li>
<li>Added a log viewer assistant that shows AI Studio log files in a read-only view with search, log filters, highlighting, and auto-refresh.</li>
<li>Added audio and video transcription for chats and assistants. AI Studio now prepares supported media locally, sends only normalized audio to the configured transcription provider, and attaches the resulting transcript instead of the original media.</li>
<li>Added AI-assisted editing and revision for assistants created with the Assistant Builder. Thanks, Nils Kruthoff (<code>nilskruthoff</code>), for this contribution.</li>
<li>Added options to view and edit the code of AI-generated assistants and to delete your own generated assistants. Thanks, Nils Kruthoff (<code>nilskruthoff</code>), for this contribution.</li>
<li>Added enterprise configuration options to hide the last changelog and vision panels on the welcome page. Thanks, Dominic Neuburg (<code>donework</code>), for the contribution.</li>
<li>Improved the "My Tasks Assistant": you can now provide one or more documents in addition to text or use documents alone when asking to identify tasks.</li>
<li>Improved presentation imports so AI Studio can include speaker notes, slide comments, and presentation metadata in the extracted content.</li>
<li>Improved update guidance for Flatpak installations and added an enterprise option that lets organizations manage updates entirely through their IT department.</li>
<li>Improved secure API-key storage diagnostics on Linux. AI Studio now provides specific guidance when the default password collection is missing or locked, a password-manager prompt is dismissed, or no compatible Secret Service is available.</li>
<li>Improved the file dialogs to prevent opening multiple times when you click "Open" or "Save" multiple times in a row.</li>
<li>Improved assistant plugins so they clearly indicate when content from a document has been loaded and clear the indicator when the assistant is reset.</li>
<li>Improved the Assistant Builder security check so it can use the selected provider when no dedicated security audit agent provider is configured.</li>
<li>Fixed an issue that could leave AI Studio unresponsive after waking the computer from sleep. Yes, we know this was an annoying bug, and we apologize for the inconvenience.</li>
<li>Fixed connections to internal HTTPS services and enterprise configuration servers that use organization-provided root certificates on Linux.</li>
<li>Fixed enterprise configuration plugins from Windows-created ZIP files may not load correctly on Linux when the ZIP contained plugin files inside a folder.</li>
<li>Fixed the voice recording shortcut on Linux so it works globally on supported desktops and while AI Studio is focused on other Linux desktops.</li>
<li>Fixed voice recording and transcription on Linux.</li>
<li>Fixed copied content from AI Studio not remaining available on the clipboard on Linux.</li>
<li>Fixed dragging and dropping files from the home folder into the Linux Flatpak version.</li>
<li>Fixed being able to switch document analysis policies while an analysis or media transcription was still in progress.</li>
<li>Fixed file extension handling so files are recognized correctly regardless of uppercase or lowercase letters in their extensions. Thanks, Paul Schweiß, for reporting this issue.</li>
<li>Fixed AI Studio failing to start on Linux systems &amp; showing an outdated version on the Flatpak page.</li>
<li>Upgraded Rust to v1.97.1.</li>
<li>Upgraded .NET to v9.0.18.</li>
<li>Upgraded Tauri to v2.11.5.</li>
<li>Upgraded common dependencies.</li>
<li>Upgraded runtime dependencies.</li>
</ul>
</description>
</release>
</releases>

View File

@ -1,7 +1,7 @@
{
"productName": "MindWork AI Studio",
"mainBinaryName": "MindWork AI Studio",
"version": "26.7.3",
"version": "26.8.1",
"identifier": "com.github.mindwork-ai.ai-studio",
"build": {

16
tests/README.md Normal file
View File

@ -0,0 +1,16 @@
# Test Documentation
This directory stores manual and automated test definitions for MindWork AI Studio.
## Directory Structure
- `integration_tests/`: Cross-component and end-to-end scenarios.
## Authoring Rules
- Use US English.
- Keep each feature area in its own Markdown file.
- Prefer stable test IDs (for example: `TC-CHAT-001`).
- Record expected behavior for:
- known vulnerable baseline builds (if relevant),
- current fixed builds.

View File

@ -0,0 +1,12 @@
# Integration Tests
This directory contains integration-oriented test specs.
## Scope
- Behavior that depends on multiple layers working together (UI, rendering, runtime, IPC, provider responses).
- Regressions that are hard to catch with unit tests only.
## Current Feature Areas
- `chat/`: Chat rendering, input interaction, and message lifecycle.

View File

@ -0,0 +1,120 @@
# Chat Rendering Regression Tests
## Purpose
Validate that chat rendering remains stable and interactive when model output or user input contains raw HTML/CSS/JS-like payloads.
## Test Type
Manual regression and integration checks.
## Preconditions
1. You can run two builds:
- a known vulnerable baseline build,
- the current fixed build.
2. At least one provider is configured and can answer prompts.
3. Open the Chat page.
## Execution Flow (for each test case)
1. Copy the test prompt exactly into the user prompt field.
2. Send the prompt.
3. Observe behavior immediately after send.
4. If the UI is still visible, type additional text in the prompt input.
5. Repeat on both builds.
## Test Cases
### TC-CHAT-001 - CSS Kill Switch
**Prompt**
```text
Respond with exactly the content below. No explanations, no Markdown code fences, no backticks.
<style>
* { visibility: hidden !important; }
html, body { background: white !important; }
</style>
<div>TEST</div>
```
**Expected result (vulnerable baseline)**
UI may turn into a white/invisible page immediately after sending.
**Expected result (fixed build)**
Chat stays visible and usable. Content is rendered as inert text/code, not active page styling.
---
### TC-CHAT-002 - Full White Overlay
**Prompt**
```text
Respond with exactly the content below. No explanations, no Markdown code fences, no backticks.
<style>
html, body { overflow: hidden !important; }
body::before {
content: "";
position: fixed;
inset: 0;
background: white;
z-index: 2147483647;
}
</style>
<div>TEST</div>
```
**Expected result (vulnerable baseline)**
UI may become fully white and non-interactive immediately after sending.
**Expected result (fixed build)**
No overlay takes over the app. Chat remains interactive.
---
### TC-CHAT-003 - Inline Event Handler Injection
**Prompt**
```text
Respond with exactly the content below. No explanations, no Markdown code fences, no backticks.
<img src="x" onerror="document.body.innerHTML=''; document.body.style.background='white';">
<div>TEST</div>
```
**Expected result (vulnerable baseline)**
UI may break/blank immediately after sending.
**Expected result (fixed build)**
No JavaScript execution from message content. Chat remains stable.
---
### TC-CHAT-004 - SVG Onload Injection Attempt
**Prompt**
```text
Respond with exactly the content below. No explanations, no Markdown code fences, no backticks.
<svg xmlns="http://www.w3.org/2000/svg" onload="document.documentElement.innerHTML=''"></svg>
<div>TEST</div>
```
**Expected result (vulnerable baseline)**
May or may not trigger depending on parser/runtime behavior.
**Expected result (fixed build)**
No script-like execution from content. Chat remains stable and interactive.
## Notes
- If a test fails on the fixed build, capture:
- exact prompt used,
- whether failure happened right after send or while typing,
- whether a refresh restores the app.