From 8950acb6547ce328fe6bfd5d8981e1e532d63397 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 6 Sep 2026 11:20:54 +0200 Subject: [PATCH] Keep provider management working when tokenizer storage fails --- .../Settings/SettingsPanelEmbeddings.razor.cs | 25 ++++++--------- .../Settings/SettingsPanelProviders.razor.cs | 25 ++++++--------- .../Dialogs/ProviderDialog.razor.cs | 32 +++++++++++++++---- app/MindWork AI Studio/Provider/IProvider.cs | 3 +- 4 files changed, 46 insertions(+), 39 deletions(-) diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs index ca6ac258..09de943a 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs @@ -3,7 +3,6 @@ using AIStudio.Dialogs; using AIStudio.Provider; using AIStudio.Settings; using AIStudio.Tools.Services; -using AIStudio.Tools.Rust; using Microsoft.AspNetCore.Components; @@ -144,8 +143,14 @@ public partial class SettingsPanelEmbeddings : SettingsPanelProviderBase return; var deleteSecretResponse = await this.RustService.DeleteAPIKey(provider, SecretStoreType.EMBEDDING_PROVIDER); - var deleteTokenizerResponse = await this.RustService.DeleteTokenizer(TokenizerModelId.ForEmbeddingProvider(provider)); - if(deleteSecretResponse.Success && deleteTokenizerResponse.Success) + + // + // Removing the tokenizer is best effort: it leaves an unused file behind when it fails, + // which is not worth bothering the user about while they are deleting the provider. The + // API key is different, though, because a leftover secret is a secret we promised to remove. + // + _ = await this.RustService.DeleteTokenizer(TokenizerModelId.ForEmbeddingProvider(provider)); + if(deleteSecretResponse.Success) { this.SettingsManager.ConfigurationData.EmbeddingProviders.Remove(provider); await this.SettingsManager.StoreSettings(); @@ -154,7 +159,7 @@ public partial class SettingsPanelEmbeddings : SettingsPanelProviderBase { var issueDialogParameters = new DialogParameters { - { x => x.Message, string.Format(T("Couldn't delete the embedding provider '{0}'. The issue: {1}. We can ignore this issue and delete the embedding provider anyway. Do you want to ignore it and delete this embedding provider?"), provider.Name, BuildDeleteIssue(deleteSecretResponse, deleteTokenizerResponse)) }, + { x => x.Message, string.Format(T("Couldn't delete the embedding provider '{0}'. The issue: {1}. We can ignore this issue and delete the embedding provider anyway. Do you want to ignore it and delete this embedding provider?"), provider.Name, deleteSecretResponse.Issue) }, }; var issueDialogReference = await this.DialogService.ShowAsync(T("Delete Embedding Provider"), issueDialogParameters, DialogOptions.FULLSCREEN); @@ -171,18 +176,6 @@ public partial class SettingsPanelEmbeddings : SettingsPanelProviderBase await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED); } - private static string BuildDeleteIssue(DeleteSecretResponse deleteSecretResponse, TokenizerResponse deleteTokenizerResponse) - { - var issues = new List(); - if (!deleteSecretResponse.Success) - issues.Add(deleteSecretResponse.Issue); - - if (!deleteTokenizerResponse.Success) - issues.Add(deleteTokenizerResponse.Message); - - return string.Join(" | ", issues); - } - private async Task ExportEmbeddingProvider(EmbeddingProvider provider) { if (!this.SettingsManager.ConfigurationData.App.ShowAdminSettings) diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs index fa9df947..64464e24 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs @@ -2,7 +2,6 @@ using System.Diagnostics.CodeAnalysis; using AIStudio.Dialogs; using AIStudio.Settings; -using AIStudio.Tools.Rust; using AIStudio.Tools.Services; using Microsoft.AspNetCore.Components; @@ -134,8 +133,14 @@ public partial class SettingsPanelProviders : SettingsPanelProviderBase return; var deleteSecretResponse = await this.RustService.DeleteAPIKey(provider, SecretStoreType.LLM_PROVIDER); - var deleteTokenizerResponse = await this.RustService.DeleteTokenizer(TokenizerModelId.ForProvider(provider)); - if(deleteSecretResponse.Success && deleteTokenizerResponse.Success) + + // + // Removing the tokenizer is best effort: it leaves an unused file behind when it fails, + // which is not worth bothering the user about while they are deleting the provider. The + // API key is different, though, because a leftover secret is a secret we promised to remove. + // + _ = await this.RustService.DeleteTokenizer(TokenizerModelId.ForProvider(provider)); + if(deleteSecretResponse.Success) { this.SettingsManager.ConfigurationData.Providers.Remove(provider); await this.SettingsManager.StoreSettings(); @@ -144,7 +149,7 @@ public partial class SettingsPanelProviders : SettingsPanelProviderBase { var issueDialogParameters = new DialogParameters { - { x => x.Message, string.Format(T("Couldn't delete the provider '{0}'. The issue: {1}. We can ignore this issue and delete the provider anyway. Do you want to ignore it and delete this provider?"), provider.InstanceName, BuildDeleteIssue(deleteSecretResponse, deleteTokenizerResponse)) }, + { x => x.Message, string.Format(T("Couldn't delete the provider '{0}'. The issue: {1}. We can ignore this issue and delete the provider anyway. Do you want to ignore it and delete this provider?"), provider.InstanceName, deleteSecretResponse.Issue) }, }; var issueDialogReference = await this.DialogService.ShowAsync(T("Delete LLM Provider"), issueDialogParameters, DialogOptions.FULLSCREEN); @@ -161,18 +166,6 @@ public partial class SettingsPanelProviders : SettingsPanelProviderBase await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED); } - private static string BuildDeleteIssue(DeleteSecretResponse deleteSecretResponse, TokenizerResponse deleteTokenizerResponse) - { - var issues = new List(); - if (!deleteSecretResponse.Success) - issues.Add(deleteSecretResponse.Issue); - - if (!deleteTokenizerResponse.Success) - issues.Add(deleteTokenizerResponse.Message); - - return string.Join(" | ", issues); - } - private async Task ExportLLMProvider(AIStudio.Settings.Provider provider) { if (!this.SettingsManager.ConfigurationData.App.ShowAdminSettings) diff --git a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs index 57eb99fb..a436a9e0 100644 --- a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs @@ -327,10 +327,22 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId var tokenizerResponse = await this.StoreOrDeleteTokenizerAsync(); if (!tokenizerResponse.Success) { - this.dataCustomTokenizerValidationIssue = tokenizerResponse.Message; - await this.form.Validate(); - return; + // + // Storing a tokenizer the user has chosen must succeed: otherwise the provider would + // silently work without the tokenizer the user asked for. Removing a tokenizer the + // user has cleared is best effort, though. A failed cleanup leaves an unused file + // behind, which is no reason to refuse saving the provider itself. + // + if (!string.IsNullOrWhiteSpace(this.dataFilePath)) + { + this.dataCustomTokenizerValidationIssue = tokenizerResponse.Message; + await this.form.Validate(); + return; + } + + this.Logger.LogWarning($"Failed to remove the tokenizer of provider '{this.DataInstanceName}'. The provider is stored anyway. The message was: {tokenizerResponse.Message}"); } + this.dataFilePath = tokenizerResponse.StoredPath; // Use the data model to store the provider. @@ -465,10 +477,18 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId private Task StoreOrDeleteTokenizerAsync() { var tokenizerId = TokenizerModelId.ForProviderId(this.DataId); - if (string.IsNullOrWhiteSpace(this.dataFilePath)) - return this.RustService.DeleteTokenizer(tokenizerId); + if (!string.IsNullOrWhiteSpace(this.dataFilePath)) + return this.RustService.StoreTokenizer(tokenizerId, this.dataFilePath); - return this.RustService.StoreTokenizer(tokenizerId, this.dataFilePath); + // + // A provider which never had a tokenizer has nothing to clean up. Calling the runtime + // anyway could only fail here, and that failure would block saving a provider which has + // nothing to do with tokenizers at all. + // + if (string.IsNullOrWhiteSpace(this.DataTokenizerPath)) + return Task.FromResult(new TokenizerResponse(true, 0, string.Empty)); + + return this.RustService.DeleteTokenizer(tokenizerId); } private void OnProviderChanged(LLMProviders selectedProvider) diff --git a/app/MindWork AI Studio/Provider/IProvider.cs b/app/MindWork AI Studio/Provider/IProvider.cs index edf3d73b..71923273 100644 --- a/app/MindWork AI Studio/Provider/IProvider.cs +++ b/app/MindWork AI Studio/Provider/IProvider.cs @@ -38,7 +38,8 @@ public interface IProvider /// The tokenizer path associated with this provider configuration. /// public string TokenizerPath { get; } - + + /// /// Whether this provider instance can load available models from the backend/API. /// This capability may differ by provider type, host, or modality. ///