From cd42c48caf7c6def15dcf4e3f74595fa67aaf654 Mon Sep 17 00:00:00 2001 From: Dominic Neuburg Date: Wed, 16 Sep 2026 18:53:23 +0200 Subject: [PATCH] Fixed self-hosted providers not using their stored API key (#959) Co-authored-by: Dominic Neuburg Co-authored-by: Thorsten Sommer --- .../Dialogs/EmbeddingProviderDialog.razor.cs | 15 ++++----------- .../Dialogs/ProviderDialog.razor.cs | 15 ++++----------- .../TranscriptionProviderDialog.razor.cs | 17 +++++------------ .../Provider/LLMProvidersExtensions.cs | 6 +++++- .../Provider/SelfHosted/ProviderSelfHosted.cs | 9 +++++++-- .../Tools/Validation/ProviderValidation.cs | 12 ++++++++---- .../wwwroot/changelog/v26.9.1.md | 4 ++++ 7 files changed, 37 insertions(+), 41 deletions(-) diff --git a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs index bde93e45..1ed37e94 100644 --- a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs @@ -209,17 +209,10 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId if (this.DataLLMProvider is LLMProviders.SELF_HOSTED) this.dataManuallyModel = this.DataModel.Id; - // - // We cannot load the API key for self-hosted providers: - // - if (this.DataLLMProvider is LLMProviders.SELF_HOSTED && this.DataHost is not Host.OLLAMA && this.DataHost is not Host.VLLM) - { - await this.ReloadModels(); - await base.OnInitializedAsync(); - return; - } - - // Load the API key: + // Load the API key. A self-hosted server may well need one: LM Studio can ask for a + // token of its own, and any of these servers can sit behind an authenticating proxy. + // So we try for every host and treat a missing key as the normal case (isTrying). + // ReloadModels() below reads dataAPIKey, so the key has to be here before it runs: var requestedSecret = await this.RustService.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER, isTrying: this.DataLLMProvider is LLMProviders.SELF_HOSTED); if (requestedSecret.Success) { diff --git a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs index 993a5c7e..26666691 100644 --- a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs @@ -264,17 +264,10 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId if (this.DataLLMProvider.IsLLMModelProvidedManually()) this.dataManuallyModel = this.DataModel.Id; - // - // We cannot load the API key for self-hosted providers: - // - if (this.DataLLMProvider is LLMProviders.SELF_HOSTED && this.DataHost is not Host.OLLAMA && this.DataHost is not Host.VLLM) - { - await this.ReloadModels(); - await base.OnInitializedAsync(); - return; - } - - // Load the API key: + // Load the API key. A self-hosted server may well need one: LM Studio can ask for a + // token of its own, and any of these servers can sit behind an authenticating proxy. + // So we try for every host and treat a missing key as the normal case (isTrying). + // ReloadModels() below reads dataAPIKey, so the key has to be here before it runs: var requestedSecret = await this.RustService.GetAPIKey(this, SecretStoreType.LLM_PROVIDER, isTrying: this.DataLLMProvider is LLMProviders.SELF_HOSTED); if (requestedSecret.Success) { diff --git a/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor.cs index b596fcdd..1f3bc0de 100644 --- a/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor.cs @@ -198,18 +198,11 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId // When using self-hosted models, we must copy the model name: if (this.DataLLMProvider is LLMProviders.SELF_HOSTED) this.dataManuallyModel = this.DataModel.Id; - - // - // We cannot load the API key for self-hosted providers: - // - if (this.DataLLMProvider is LLMProviders.SELF_HOSTED && this.DataHost is not Host.OLLAMA) - { - await this.ReloadModels(); - await base.OnInitializedAsync(); - return; - } - - // Load the API key: + + // Load the API key. A self-hosted server may well need one: LM Studio can ask for a + // token of its own, and any of these servers can sit behind an authenticating proxy. + // So we try for every host and treat a missing key as the normal case (isTrying). + // ReloadModels() below reads dataAPIKey, so the key has to be here before it runs: var requestedSecret = await this.RustService.GetAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER, isTrying: this.DataLLMProvider is LLMProviders.SELF_HOSTED); if (requestedSecret.Success) { diff --git a/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs b/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs index 9e8b2745..a7992cb2 100644 --- a/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs +++ b/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs @@ -491,7 +491,11 @@ public static class LLMProvidersExtensions LLMProviders.GWDG => true, LLMProviders.HUGGINGFACE => true, - LLMProviders.SELF_HOSTED => host is (Host.OLLAMA or Host.VLLM), + // Every self-hosted engine can ask for a key: LM Studio brings its own tokens, and any of + // them can sit behind a proxy which authenticates. The field is labeled as optional for + // them, so offering it costs nothing where no key is needed, while leaving it out means + // the user cannot enter the one their server expects: + LLMProviders.SELF_HOSTED => host is not Host.NONE, _ => false, }; diff --git a/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs b/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs index ffe8ead7..c24e7238 100644 --- a/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs +++ b/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs @@ -173,12 +173,17 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide private async Task LoadModels(SecretStoreType storeType, string[] ignorePhrases, string[] filterPhrases, string? apiKeyProvisional, CancellationToken token) { - var secretKey = await this.GetModelLoadingSecretKey(storeType, apiKeyProvisional, true); + var secretKey = await this.GetModelLoadingSecretKey(storeType, apiKeyProvisional, isTryingSecret: true); try { using var lmStudioRequest = new HttpRequestMessage(HttpMethod.Get, "models"); - if(secretKey is not null) + + // An empty token is worse than none at all: a proxy which enforces authentication + // rejects an empty bearer with 401, where it would have let a request without any + // authorization header through. The dialogs hand us their key field as it stands, so + // an empty string arrives here whenever the user stored no key: + if(!string.IsNullOrWhiteSpace(secretKey)) lmStudioRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey); using var lmStudioResponse = await this.HttpClient.SendAsync(lmStudioRequest, token); diff --git a/app/MindWork AI Studio/Tools/Validation/ProviderValidation.cs b/app/MindWork AI Studio/Tools/Validation/ProviderValidation.cs index 3db0418f..10b775e6 100644 --- a/app/MindWork AI Studio/Tools/Validation/ProviderValidation.cs +++ b/app/MindWork AI Studio/Tools/Validation/ProviderValidation.cs @@ -49,16 +49,20 @@ public sealed class ProviderValidation public string? ValidatingAPIKey(string apiKey) { - if(this.GetProvider() is LLMProviders.SELF_HOSTED) - return null; - + // A key which could not be stored in or removed from the operating system has to reach the + // user for every provider. Self-hosted providers are exempt from having to name a key at + // all, not from being told that the one they named was lost on the way: var apiKeyStorageIssue = this.GetAPIKeyStorageIssue(); if(!string.IsNullOrWhiteSpace(apiKeyStorageIssue)) return apiKeyStorageIssue; + // A self-hosted server may well run without any key, so an empty field is fine for it: + if(this.GetProvider() is LLMProviders.SELF_HOSTED) + return null; + if(string.IsNullOrWhiteSpace(apiKey)) return TB("Please enter an API key."); - + return null; } diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.9.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.9.1.md index 88aea0f4..fef07221 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.9.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.9.1.md @@ -24,6 +24,7 @@ - Added support for several drop areas on the same page. More complex assistants can now receive files or folders by drag and drop at more than one place. - Added drag and drop to the input and output folder of the Batch Processing assistant: drop a folder onto either field to choose it. - Added ways to load text from a file and drop zones for them, throughout the assistants and dialogs. We went through them one by one, so many fields that used to accept typed text only now take the content of a file as well. +- Added an optional API key to every server you host yourself, among them LM Studio, llama.cpp, and whisper.cpp. Such a server may ask for one itself or sit behind a login your organization placed in front of it. So far, only Ollama and vLLM could be given a key. - Improved loading web content in the assistants: it now uses the same reader as the Read Web Page tool, which extracts the main content of a page more reliably and skips navigation and boilerplate. Pages from your own network, including local servers, keep working as before. When a page cannot be read, AI Studio now says why instead of leaving the field empty. - Improved the app icon. The previous one was generated by an image model; the new one was created based on it and keeps the familiar green landscape with the chat bubble. Because it is now a vector drawing, it stays sharp everywhere it appears: in your taskbar or dock, in the window list, and on the start screen while AI Studio is loading. - Improved how AI Studio works out what a model can do. Every model family now stands on its own, together with the page it was read from, and our build refuses rules which contradict each other or name no source. That way, mistakes are caught before they ever reach you. @@ -53,4 +54,7 @@ - Fixed the silence when the step that picks the fitting passages out of your documents fails. You are told that the answer rests on everything that was found. - Fixed the regenerate button taking an answer away without producing a new one. This happened in chats started from a template that holds no question of your own. - Fixed the counter above an answer, which shows how many sources it rests on, doing nothing when you clicked it. It now takes you down to the sources. +- Fixed the list of models staying empty at a server you host yourself, which made the model you had picked look as if it had vanished. Your key was there all along, it just was not read when the settings opened. +- Fixed AI Studio asking such a server for its models with an empty key attached when you had stored none at all. Servers behind a login turn those requests down. +- Fixed a key that could not be saved going unmentioned for the servers you host yourself. You are now told what went wrong, instead of the settings simply staying open. - Upgraded the Visual Briefing Assistant (in preview) from the prototype to the beta state. The assistant is now completely implemented and is undergoing a deeper testing phase in preparation for release. To try it, open the app settings, allow preview features down to beta, and then enable the Visual Briefing Assistant there.