From 5cef42a756842f50d2d7d1c66f7b63c1c2dd0856 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sat, 19 Sep 2026 15:18:28 +0200 Subject: [PATCH] Recognize the embedding models whose name does not say so --- .../Dialogs/EmbeddingProviderDialog.razor | 6 ++++++ .../Dialogs/EmbeddingProviderDialog.razor.cs | 19 +++++++++++++++++ .../Models/Kinds/EmbeddingModelsFamily.cs | 21 ++++++++++++++++++- app/Tests/Models/Corpus/ModelKindCorpus.cs | 14 +++++++++++++ 4 files changed, 59 insertions(+), 1 deletion(-) diff --git a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor index ff857f61..566e0640 100644 --- a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor +++ b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor @@ -115,6 +115,12 @@ @T("This server does not offer the selected model right now. It stays selected, so the documents you already prepared keep working. Choosing another model means every document of the data sources behind this provider is prepared again.") } + @if (this.ServerNamedNoEmbeddingModel) + { + + @T("Your server answered, but none of the models it serves is one we know to create embeddings. Either there is none installed, or it runs under a name we do not recognize. In the latter case, your organization can describe the model in a model plugin, and it will show up here.") + + } @if (!string.IsNullOrWhiteSpace(this.dataLoadingModelsIssue)) { diff --git a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs index 674a451e..765e7aa5 100644 --- a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs @@ -137,6 +137,7 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId private string dataEditingPreviousInstanceName = string.Empty; private string dataLoadingModelsIssue = string.Empty; private bool dataConfiguredModelIsNotOffered; + private bool dataServerWasAskedForItsModels; private string dataFilePath = string.Empty; private string dataTokenizerFingerprint = string.Empty; private string dataCustomTokenizerValidationIssue = string.Empty; @@ -517,6 +518,7 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId private async Task ReloadModels() { this.dataLoadingModelsIssue = string.Empty; + this.dataServerWasAskedForItsModels = true; var currentEmbeddingProviderSettings = this.CreateEmbeddingProviderSettings(); var provider = currentEmbeddingProviderSettings.CreateProvider(); if (provider is NoProvider) @@ -573,6 +575,23 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId this.availableModels.Insert(0, this.DataModel); } + /// + /// Whether the server answered without naming a single embedding model. + /// + /// + /// Two situations end up here, and the user is the only one who can tell them apart: a server + /// running no embedding model at all, and one running an embedding model under a name no rule + /// covers. Saying so beats the bare "No models loaded or available.", which reads like a + /// failure and leaves nobody anywhere to go -- the field for typing a name is gone, on purpose. + /// Describing such a model in a model plugin is the way out, and naming it here is what turns + /// a dead end into one. + /// + private bool ServerNamedNoEmbeddingModel => + this.DataLLMProvider is LLMProviders.SELF_HOSTED && + this.dataServerWasAskedForItsModels && + string.IsNullOrWhiteSpace(this.dataLoadingModelsIssue) && + this.availableModels.Count is 0; + private string APIKeyText => this.DataLLMProvider switch { LLMProviders.SELF_HOSTED => T("(Optional) API Key"), diff --git a/app/MindWork AI Studio/Models/Kinds/EmbeddingModelsFamily.cs b/app/MindWork AI Studio/Models/Kinds/EmbeddingModelsFamily.cs index 0fb1988a..3d65d109 100644 --- a/app/MindWork AI Studio/Models/Kinds/EmbeddingModelsFamily.cs +++ b/app/MindWork AI Studio/Models/Kinds/EmbeddingModelsFamily.cs @@ -27,7 +27,7 @@ public sealed class EmbeddingModelsFamily : ModelFamily public override ModelVendor Vendor => ModelVendor.UNKNOWN; /// - public override ModelSource Source => new("https://huggingface.co/models?pipeline_tag=feature-extraction", new DateOnly(2026, 9, 12), "Ported from the embedding markers of Provider/ModelKindExtensions.cs. The e5 line says it in its own family, so it is not repeated here."); + public override ModelSource Source => new("https://huggingface.co/models?pipeline_tag=feature-extraction", new DateOnly(2026, 9, 19), "Ported from the embedding markers of Provider/ModelKindExtensions.cs. The e5 line says it in its own family, so it is not repeated here. The four names at the end came later, from going through the widely used embedding models whose name carries none of the words above."); /// protected override void Declare(ModelFamilyBuilder builder) @@ -55,5 +55,24 @@ public sealed class EmbeddingModelsFamily : ModelFamily // General Text Embeddings, from Alibaba. Written as a name part rather than as a substring, // because three letters appear inside far too many unrelated words: builder.Modifier("gte").AsSegment().Inherits(); + + // + // Four that say nothing about embedding in their name, and are embedding models all the + // same. Every one of them is widely used, so leaving them out does not cost an exotic case: + // it puts them among the chat models, where somebody picks one and waits for an answer it + // cannot give. All four are written as name parts rather than as substrings, for the reason + // gte above gives -- short words which appear inside unrelated names. + // + // Note that "instructor" is a different word from the "instruct" which half the chat models + // carry, and a name part never matches half of one. + // + builder.Modifier("stella").AsSegment().Inherits(); + + builder.Modifier("labse").AsSegment().Inherits(); + + builder.Modifier("instructor").AsSegment().Inherits(); + + // Generalizable T5 Retrieval, from Google: + builder.Modifier("gtr").AsSegment().Inherits(); } } \ No newline at end of file diff --git a/app/Tests/Models/Corpus/ModelKindCorpus.cs b/app/Tests/Models/Corpus/ModelKindCorpus.cs index 611ab7f5..54cd29a9 100644 --- a/app/Tests/Models/Corpus/ModelKindCorpus.cs +++ b/app/Tests/Models/Corpus/ModelKindCorpus.cs @@ -51,6 +51,16 @@ public static class ModelKindCorpus new(SELF_HOSTED, "nomic-embed-text:latest", EMBEDDING), new(SELF_HOSTED, "bge-m3:latest", EMBEDDING), new(SELF_HOSTED, "all-minilm:latest", EMBEDDING), + + // + // The four whose names say nothing about embedding at all. They are here because the list + // above answers them through a word they happen to carry, and these carry none: without a + // rule of their own they would count as chat models, which is where they stood. + // + new(SELF_HOSTED, "stella_en_400M_v5", EMBEDDING), + new(SELF_HOSTED, "LaBSE", EMBEDDING), + new(SELF_HOSTED, "instructor-xl", EMBEDDING), + new(SELF_HOSTED, "gtr-t5-large", EMBEDDING), ]; /// @@ -213,6 +223,10 @@ public static class ModelKindCorpus new(SELF_HOSTED, "qwen3.8:latest", CHAT), new(SELF_HOSTED, "gpt-oss:latest", CHAT), + // Half the chat models of the world carry this word, and one of the embedding names above + // is one letter longer than it. A name part is what keeps the two apart: + new(SELF_HOSTED, "mistral-7b-instruct", CHAT), + // // Three which were questioned while testing and stay all the same. Grok Build is the coding // model behind the xAI CLI and answers like any other Grok. The Groq compound systems are