diff --git a/README.md b/README.md index 1ba7c4b4..7fe0c046 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ MindWork AI Studio is a free desktop app for macOS, Windows, and Linux. It provi - [LiteLLM](https://www.litellm.ai/) (an AI gateway you run yourself, in front of models from many providers) - [Requesty](https://www.requesty.ai/) (a hosted AI gateway from the UK, in front of models from many providers) - [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) + - Self-hosted models using [llama.cpp](https://github.com/ggerganov/llama.cpp), [ollama](https://github.com/ollama/ollama), [LM Studio](https://lmstudio.ai/), [vLLM](https://github.com/vllm-project/vllm), and [llmman](https://github.com/llmmanorg/llmman) - [Groq](https://groq.com/) - [Fireworks](https://fireworks.ai/) - For scientists and employees of research institutions, we also support [Helmholtz](https://helmholtz.cloud/services/?serviceID=d7d5c597-a2f6-4bd1-b71e-4d6499d98570) and [GWDG](https://gwdg.de/services/application-services/ai-services/) 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. diff --git a/app/Build/Commands/UpdateMetadataCommands.cs b/app/Build/Commands/UpdateMetadataCommands.cs index 26fd9641..e0145f9b 100644 --- a/app/Build/Commands/UpdateMetadataCommands.cs +++ b/app/Build/Commands/UpdateMetadataCommands.cs @@ -679,11 +679,33 @@ public sealed partial class UpdateMetadataCommands var convertedEntry = new StringBuilder(); for (var index = 0; index < codeSpans.Length; index++) - convertedEntry.Append(index % 2 is 0 ? codeSpans[index] : $"{codeSpans[index]}"); + convertedEntry.Append(index % 2 is 0 ? ConvertLinksToText(codeSpans[index], changelogEntry) : $"{codeSpans[index]}"); return convertedEntry.ToString(); } + /// + /// Reduces the Markdown links of a changelog entry to their text. + /// + /// + /// AppStream descriptions know no links, and the software centers show whatever markup is left as it + /// stands: brackets, parentheses, and the whole address. The address is not lost by dropping it here, + /// because the changelog in the app and the release notes on GitHub still show the link. A link this + /// cannot read, such as one with a code span in its text, stops the conversion rather than reaching + /// the software centers half converted. + /// + /// A part of the entry outside of any code span, already escaped for XML. + /// The whole entry, for the error message. + /// The part with each link replaced by its text. + private static string ConvertLinksToText(string text, string changelogEntry) + { + var convertedText = MarkdownLinkRegex().Replace(text, "${text}"); + if (convertedText.Contains("](", StringComparison.Ordinal)) + throw new InvalidOperationException($"The changelog entry contains a link which cannot be converted: '{changelogEntry}'."); + + return convertedText; + } + 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)) @@ -1177,4 +1199,8 @@ public sealed partial class UpdateMetadataCommands [GeneratedRegex("^[0-9a-fA-F]{40,64}$")] private static partial Regex GitCommitHashRegex(); + + // Matches one Markdown link. The address may hold one level of parentheses, as Wikipedia addresses do: + [GeneratedRegex("""\[(?[^\[\]]+)\]\((?
(?:[^()\s]|\([^()\s]*\))+)\)""")] + private static partial Regex MarkdownLinkRegex(); } diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index e2ef58f1..3cd6a56f 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -9793,6 +9793,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2348849647"] = "Last Changelog" -- Tools UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2499909372"] = "Tools" +-- You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT-6, GPT-5, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hetzner (experimental), IONOS, LiteLLM, Requesty, Hugging Face, Groq, Fireworks, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, or llmman. 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::T251243691"] = "You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT-6, GPT-5, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hetzner (experimental), IONOS, LiteLLM, Requesty, Hugging Face, Groq, Fireworks, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, or llmman. 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." + -- AI models can use tools on their own, such as Web Search, Read Web Page, or Search Confluence. When you ask something a model cannot answer from what it knows, it looks it up and answers with the sources it used. You decide which tools a model may use, and you can watch it work. UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2587191175"] = "AI models can use tools on their own, such as Web Search, Read Web Page, or Search Confluence. When you ask something a model cannot answer from what it knows, it looks it up and answers with the sources it used. You decide which tools a model may use, and you can watch it work." @@ -9823,9 +9826,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T3959064551"] = "Privacy" -- Voice input UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T4194938377"] = "Voice input" --- You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT-6, GPT-5, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hetzner (experimental), IONOS, LiteLLM, Requesty, 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::T4287735840"] = "You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT-6, GPT-5, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hetzner (experimental), IONOS, LiteLLM, Requesty, 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." - -- 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." @@ -10570,6 +10570,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::SUPPORTERS::T3364384944"] = "The first 10 supp -- Thank you, Dominic, for supporting us not only financially but also with code: a smoother chat, the audio quality setting for transcriptions, personal API keys for providers of an organization, sharing feature usage with self-hosted servers, and a better Confluence search. UI_TEXT_CONTENT["AISTUDIO::PAGES::SUPPORTERS::T348735370"] = "Thank you, Dominic, for supporting us not only financially but also with code: a smoother chat, the audio quality setting for transcriptions, personal API keys for providers of an organization, sharing feature usage with self-hosted servers, and a better Confluence search." +-- Thanks, Eric, for bringing llmman to AI Studio as a new option for self-hosted models. +UI_TEXT_CONTENT["AISTUDIO::PAGES::SUPPORTERS::T3520213035"] = "Thanks, Eric, for bringing llmman to AI Studio as a new option for self-hosted models." + -- Thanks, Luc, for your build script contribution. UI_TEXT_CONTENT["AISTUDIO::PAGES::SUPPORTERS::T3597955013"] = "Thanks, Luc, for your build script contribution." diff --git a/app/MindWork AI Studio/Models/Hosting/Hosts/HostSelfHosted.cs b/app/MindWork AI Studio/Models/Hosting/Hosts/HostSelfHosted.cs index a45f601b..36423276 100644 --- a/app/MindWork AI Studio/Models/Hosting/Hosts/HostSelfHosted.cs +++ b/app/MindWork AI Studio/Models/Hosting/Hosts/HostSelfHosted.cs @@ -4,12 +4,16 @@ using AIStudio.Provider; namespace AIStudio.Models.Hosting.Hosts; /// -/// Somebody's own engine: Ollama, LM Studio, vLLM, llama.cpp, or a proxy in front of them. +/// Somebody's own engine: Ollama, LM Studio, vLLM, llama.cpp, llmman, or a proxy in front of them. /// /// /// vLLM serves whatever it was pointed at, and what it was pointed at is usually a hub repository: /// "meta-llama/Llama-3.3-70B-Instruct", "01-ai/yi-large". So the organization comes off here too. /// +/// llmman goes one further and names the registry it pulled the model from: +/// "hf.co/unsloth/Qwen3.5-0.8B-GGUF:latest", "docker.io/ai/qwen3.5:0.8b". The registry is one more +/// path segment and comes off the same way, one segment at a time. +/// /// The colon does not. Ollama writes the variant after it -- "qwen3.8:27b-mlx" -- and taking that /// off would leave a name which no longer says which build of the model is running. Only the host /// which actually has a router treats a colon as routing. diff --git a/app/MindWork AI Studio/Pages/Home.razor.cs b/app/MindWork AI Studio/Pages/Home.razor.cs index a912a3cb..6031cc81 100644 --- a/app/MindWork AI Studio/Pages/Home.razor.cs +++ b/app/MindWork AI Studio/Pages/Home.razor.cs @@ -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 (GPT-6, GPT-5, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hetzner (experimental), IONOS, LiteLLM, Requesty, 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("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 (GPT-6, GPT-5, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hetzner (experimental), IONOS, LiteLLM, Requesty, Hugging Face, Groq, Fireworks, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, or llmman. 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(T("Tools"), T("AI models can use tools on their own, such as Web Search, Read Web Page, or Search Confluence. When you ask something a model cannot answer from what it knows, it looks it up and answers with the sources it used. You decide which tools a model may use, and you can watch it work.")), new(T("Voice input"), T("Dictate your messages instead of typing them, and have your recordings as well as audio and video files transcribed. You choose the transcription provider, including one you host yourself.")), diff --git a/app/MindWork AI Studio/Pages/Supporters.razor b/app/MindWork AI Studio/Pages/Supporters.razor index 703044b7..227bb5a4 100644 --- a/app/MindWork AI Studio/Pages/Supporters.razor +++ b/app/MindWork AI Studio/Pages/Supporters.razor @@ -98,6 +98,7 @@ + diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua index 6085dcae..77218cda 100644 --- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua +++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua @@ -85,7 +85,7 @@ CONFIG["LLM_PROVIDERS"] = {} -- ["InstanceName"] = "", -- ["UsedLLMProvider"] = "SELF_HOSTED", -- --- -- Allowed values for Host are: LM_STUDIO, LLAMACPP, OLLAMA, and VLLM +-- -- Allowed values for Host are: LM_STUDIO, LLAMA_CPP, OLLAMA, VLLM, and LLMMAN -- ["Host"] = "OLLAMA", -- ["Hostname"] = "", -- @@ -184,7 +184,7 @@ CONFIG["TRANSCRIPTION_PROVIDERS"] = {} -- ["Name"] = "", -- ["UsedLLMProvider"] = "SELF_HOSTED", -- --- -- Allowed values for Host are: LM_STUDIO, LLAMACPP, OLLAMA, VLLM, and WHISPER_CPP +-- -- Allowed values for Host are: OLLAMA, VLLM, WHISPER_CPP, and LLMMAN -- ["Host"] = "WHISPER_CPP", -- ["Hostname"] = "", -- @@ -221,7 +221,7 @@ CONFIG["EMBEDDING_PROVIDERS"] = {} -- ["Name"] = "", -- ["UsedLLMProvider"] = "SELF_HOSTED", -- --- -- Allowed values for Host are: LM_STUDIO, LLAMACPP, OLLAMA, and VLLM +-- -- Allowed values for Host are: LM_STUDIO, OLLAMA, VLLM, and LLMMAN -- ["Host"] = "OLLAMA", -- ["Hostname"] = "", -- diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index c8d82e39..a6c7ebf7 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -9795,6 +9795,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2348849647"] = "Letztes Änderungsproto -- Tools UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2499909372"] = "Werkzeuge" +-- You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT-6, GPT-5, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hetzner (experimental), IONOS, LiteLLM, Requesty, Hugging Face, Groq, Fireworks, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, or llmman. 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::T251243691"] = "Sie sind nicht an einen einzelnen Anbieter gebunden, sondern können den Anbieter wählen, der am besten zu Ihren Anforderungen passt. Derzeit unterstützen wir OpenAI (GPT-6, GPT-5 usw.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hetzner (experimentell), IONOS, LiteLLM, Requesty, Hugging Face, Groq, Fireworks sowie selbst gehostete Modelle mit vLLM, llama.cpp, ollama, LM Studio oder llmman. Für Forschende und Mitarbeitende von Forschungseinrichtungen unterstützen wir außerdem die KI-Dienste von Helmholtz und der GWDG. Angehörige aller 18 Helmholtz-Zentren, der Max-Planck-Gesellschaft, der meisten deutschen und vieler internationaler Universitäten können diese Dienste über föderierte Anmeldedienste wie eduGAIN nutzen." + -- AI models can use tools on their own, such as Web Search, Read Web Page, or Search Confluence. When you ask something a model cannot answer from what it knows, it looks it up and answers with the sources it used. You decide which tools a model may use, and you can watch it work. UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2587191175"] = "KI-Modelle können selbstständig Werkzeuge nutzen, etwa „Websuche“, „Webseite lesen“ oder „Confluence durchsuchen“. Wenn Sie etwas fragen, das ein Modell nicht mit seinem vorhandenen Wissen beantworten kann, schlägt es nach und nennt in seiner Antwort die verwendeten Quellen. Sie entscheiden, welche Werkzeuge ein Modell nutzen darf, und können ihm bei der Arbeit zusehen." @@ -9825,9 +9828,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T3959064551"] = "Datenschutz" -- Voice input UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T4194938377"] = "Spracheingabe" --- You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT-6, GPT-5, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hetzner (experimental), IONOS, LiteLLM, Requesty, 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::T4287735840"] = "Sie sind nicht an einen einzigen Anbieter gebunden. Stattdessen können Sie den Anbieter wählen, der am besten zu Ihren Anforderungen passt. Derzeit unterstützen wir OpenAI (GPT-6, GPT-5 usw.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hetzner (experimentell), IONOS, LiteLLM, Requesty, Hugging Face, Groq, Fireworks sowie selbst gehostete Modelle mit vLLM, llama.cpp, ollama oder LM Studio. Für Wissenschaftlerinnen und Wissenschaftler sowie Mitarbeitende von Forschungseinrichtungen unterstützen wir außerdem die KI-Dienste von Helmholtz und GWDG. Diese sind über föderierte Logins wie eduGAIN für alle 18 Helmholtz-Zentren, die Max-Planck-Gesellschaft, die meisten deutschen sowie viele internationale Universitäten verfügbar." - -- 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." @@ -10572,6 +10572,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::SUPPORTERS::T3364384944"] = "Die ersten 10 Spe -- Thank you, Dominic, for supporting us not only financially but also with code: a smoother chat, the audio quality setting for transcriptions, personal API keys for providers of an organization, sharing feature usage with self-hosted servers, and a better Confluence search. UI_TEXT_CONTENT["AISTUDIO::PAGES::SUPPORTERS::T348735370"] = "Vielen Dank, Dominic, dass du uns nicht nur finanziell, sondern auch mit Code unterstützt: mit einem reibungsloseren Chat, der Einstellung für die Audioqualität der Transkription, persönlichen API-Schlüsseln für Anbieter einer Organisation, der Möglichkeit, die Funktionsnutzung mit selbst gehosteten Servern zu teilen, und einer besseren Confluence-Suche." +-- Thanks, Eric, for bringing llmman to AI Studio as a new option for self-hosted models. +UI_TEXT_CONTENT["AISTUDIO::PAGES::SUPPORTERS::T3520213035"] = "Danke, Eric, dass du llmman als neue Option für selbst gehostete Modelle in AI Studio integriert hast." + -- Thanks, Luc, for your build script contribution. UI_TEXT_CONTENT["AISTUDIO::PAGES::SUPPORTERS::T3597955013"] = "Danke, Luc, für deinen Beitrag zum Build-Skript." diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index f6fe7469..f33ca92e 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -9795,6 +9795,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2348849647"] = "Last Changelog" -- Tools UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2499909372"] = "Tools" +-- You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT-6, GPT-5, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hetzner (experimental), IONOS, LiteLLM, Requesty, Hugging Face, Groq, Fireworks, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, or llmman. 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::T251243691"] = "You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT-6, GPT-5, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hetzner (experimental), IONOS, LiteLLM, Requesty, Hugging Face, Groq, Fireworks, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, or llmman. 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." + -- AI models can use tools on their own, such as Web Search, Read Web Page, or Search Confluence. When you ask something a model cannot answer from what it knows, it looks it up and answers with the sources it used. You decide which tools a model may use, and you can watch it work. UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2587191175"] = "AI models can use tools on their own, such as Web Search, Read Web Page, or Search Confluence. When you ask something a model cannot answer from what it knows, it looks it up and answers with the sources it used. You decide which tools a model may use, and you can watch it work." @@ -9825,9 +9828,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T3959064551"] = "Privacy" -- Voice input UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T4194938377"] = "Voice input" --- You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT-6, GPT-5, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hetzner (experimental), IONOS, LiteLLM, Requesty, 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::T4287735840"] = "You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT-6, GPT-5, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hetzner (experimental), IONOS, LiteLLM, Requesty, 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." - -- 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." @@ -10572,6 +10572,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::SUPPORTERS::T3364384944"] = "The first 10 supp -- Thank you, Dominic, for supporting us not only financially but also with code: a smoother chat, the audio quality setting for transcriptions, personal API keys for providers of an organization, sharing feature usage with self-hosted servers, and a better Confluence search. UI_TEXT_CONTENT["AISTUDIO::PAGES::SUPPORTERS::T348735370"] = "Thank you, Dominic, for supporting us not only financially but also with code: a smoother chat, the audio quality setting for transcriptions, personal API keys for providers of an organization, sharing feature usage with self-hosted servers, and a better Confluence search." +-- Thanks, Eric, for bringing llmman to AI Studio as a new option for self-hosted models. +UI_TEXT_CONTENT["AISTUDIO::PAGES::SUPPORTERS::T3520213035"] = "Thanks, Eric, for bringing llmman to AI Studio as a new option for self-hosted models." + -- Thanks, Luc, for your build script contribution. UI_TEXT_CONTENT["AISTUDIO::PAGES::SUPPORTERS::T3597955013"] = "Thanks, Luc, for your build script contribution." diff --git a/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs b/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs index b5174b5d..dd42b3cb 100644 --- a/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs +++ b/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs @@ -557,21 +557,7 @@ public static class LLMProvidersExtensions public static bool CanLoadModels(this LLMProviders provider, Host host, string? apiKey) { if (provider is LLMProviders.SELF_HOSTED) - { - switch (host) - { - case Host.NONE: - case Host.WHISPER_CPP: - default: - return false; - - case Host.LLAMA_CPP: - case Host.OLLAMA: - case Host.LM_STUDIO: - case Host.VLLM: - return true; - } - } + return host.IsModelListSupported(); if(provider is LLMProviders.NONE) return false; diff --git a/app/MindWork AI Studio/Provider/Reasoning/ReasoningDispatcher.cs b/app/MindWork AI Studio/Provider/Reasoning/ReasoningDispatcher.cs index 0695495a..5d6cf973 100644 --- a/app/MindWork AI Studio/Provider/Reasoning/ReasoningDispatcher.cs +++ b/app/MindWork AI Studio/Provider/Reasoning/ReasoningDispatcher.cs @@ -125,6 +125,13 @@ public static class ReasoningDispatcher Host.VLLM => [ReasoningDialect.OPEN_AI_COMPATIBLE, ReasoningDialect.REASONING_EFFORT, ReasoningDialect.QWEN_THINKING, ReasoningDialect.GOOGLE_THINKING, ReasoningDialect.VLLM], + // + // llmman starts llama.cpp for GGUF models and vLLM, SGLang, or MLX for the others, and + // hands their fields through unchanged. Which engine serves a model is up to llmman, so + // it is read in everything llama.cpp and vLLM are read in: + // + Host.LLMMAN => [ReasoningDialect.OPEN_AI_COMPATIBLE, ReasoningDialect.REASONING_EFFORT, ReasoningDialect.QWEN_THINKING, ReasoningDialect.GOOGLE_THINKING, ReasoningDialect.LLAMA_CPP, ReasoningDialect.VLLM], + _ => EVERYTHING_A_GATEWAY_MIGHT_SERVE, }, diff --git a/app/MindWork AI Studio/Provider/SelfHosted/Host.cs b/app/MindWork AI Studio/Provider/SelfHosted/Host.cs index d0dde806..f565bcc3 100644 --- a/app/MindWork AI Studio/Provider/SelfHosted/Host.cs +++ b/app/MindWork AI Studio/Provider/SelfHosted/Host.cs @@ -9,4 +9,5 @@ public enum Host WHISPER_CPP, OLLAMA, VLLM, + LLMMAN, } \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/SelfHosted/HostExtensions.cs b/app/MindWork AI Studio/Provider/SelfHosted/HostExtensions.cs index 25dc07ca..de711008 100644 --- a/app/MindWork AI Studio/Provider/SelfHosted/HostExtensions.cs +++ b/app/MindWork AI Studio/Provider/SelfHosted/HostExtensions.cs @@ -11,6 +11,7 @@ public static class HostExtensions Host.WHISPER_CPP => "whisper.cpp", Host.OLLAMA => "ollama", Host.VLLM => "vLLM", + Host.LLMMAN => "llmman", _ => "Unknown", }; @@ -47,6 +48,7 @@ public static class HostExtensions case Host.VLLM: case Host.LM_STUDIO: case Host.LLAMA_CPP: + case Host.LLMMAN: return true; } } @@ -58,6 +60,7 @@ public static class HostExtensions case Host.LM_STUDIO: case Host.OLLAMA: case Host.VLLM: + case Host.LLMMAN: return true; default: @@ -73,12 +76,44 @@ public static class HostExtensions case Host.OLLAMA: case Host.VLLM: case Host.WHISPER_CPP: + case Host.LLMMAN: return true; - + default: case Host.LM_STUDIO: case Host.LLAMA_CPP: return false; } } + + /// + /// Whether the engine answers the models route with the models it serves. + /// + /// + /// This is the only place which says so. The provider asks it before it loads the models of a + /// chat, embedding, or transcription list, and the dialogs ask it before they offer to reload + /// them; a new engine therefore needs no more than its line here. whisper.cpp is not among + /// them: it serves the one model it was started with and has no route to list it, so the + /// provider offers that model under a fixed name instead. llama.cpp is, even though builds + /// older than the route answer it with 404; the provider handles those builds itself. + /// + /// The engine. + /// True when the models can be loaded from the engine; otherwise, false. + public static bool IsModelListSupported(this Host host) + { + switch (host) + { + case Host.LLAMA_CPP: + case Host.OLLAMA: + case Host.LM_STUDIO: + case Host.VLLM: + case Host.LLMMAN: + return true; + + default: + case Host.NONE: + case Host.WHISPER_CPP: + return false; + } + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/SelfHosted/Model.cs b/app/MindWork AI Studio/Provider/SelfHosted/Model.cs index ce1db8e7..87906f94 100644 --- a/app/MindWork AI Studio/Provider/SelfHosted/Model.cs +++ b/app/MindWork AI Studio/Provider/SelfHosted/Model.cs @@ -8,8 +8,8 @@ namespace AIStudio.Provider.SelfHosted; /// /// The context window is vLLM's addition to that route: it reports the window the operator started /// the engine with, which is the one number no rule about the weights could ever know. Ollama, -/// LM Studio, and llama.cpp answer the same route without it, so it stays unknown there instead of -/// being guessed. +/// LM Studio, llama.cpp, and llmman answer the same route without it, so it stays unknown there +/// instead of being guessed. /// /// vLLM calls that field max_model_len, which reads like a limit on the model rather than on a /// conversation. The wire keeps their spelling, and this record says what the number means, so that diff --git a/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs b/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs index 07468c3c..95a6ccc4 100644 --- a/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs +++ b/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs @@ -25,7 +25,7 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide public override string InstanceName { get; set; } = "Self-hosted"; /// - public override bool HasModelLoadingCapability => host is Host.OLLAMA or Host.LM_STUDIO or Host.VLLM or Host.LLAMA_CPP; + public override bool HasModelLoadingCapability => host.IsModelListSupported(); /// public override async IAsyncEnumerable StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) @@ -40,7 +40,7 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide { // Build the list of messages. The image format depends on the host: // - Ollama uses the direct image URL format: { "type": "image_url", "image_url": "data:..." } - // - LM Studio, vLLM, and llama.cpp use the nested image URL format: { "type": "image_url", "image_url": { "url": "data:..." } } + // - LM Studio, vLLM, llama.cpp, and llmman use the nested image URL format: { "type": "image_url", "image_url": { "url": "data:..." } } var messages = host switch { Host.OLLAMA => await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.CreateSettingsProvider(effectiveChatModel)), @@ -94,22 +94,17 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide { try { - switch (host) + if (host is Host.LLAMA_CPP) + return await this.LoadLlamaCppTextModels(apiKeyProvisional, token); + + if (!host.IsChatSupported() || !host.IsModelListSupported()) + return ModelLoadResult.FromModels([]); + + var result = await this.LoadModels(SecretStoreType.LLM_PROVIDER, apiKeyProvisional, token); + return result with { - case Host.LLAMA_CPP: - return await this.LoadLlamaCppTextModels(apiKeyProvisional, token); - - case Host.LM_STUDIO: - case Host.OLLAMA: - case Host.VLLM: - var result = await this.LoadModels(SecretStoreType.LLM_PROVIDER, apiKeyProvisional, token); - return result with - { - Models = [..result.Models.Where(model => model.IsChatModel(this.Provider))] - }; - } - - return ModelLoadResult.FromModels([]); + Models = [..result.Models.Where(model => model.IsChatModel(this.Provider))] + }; } catch(Exception e) { @@ -128,19 +123,14 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide { try { - switch (host) - { - case Host.LM_STUDIO: - case Host.OLLAMA: - case Host.VLLM: - var result = await this.LoadModels(SecretStoreType.EMBEDDING_PROVIDER, apiKeyProvisional, token); - return result with - { - Models = [..result.Models.Where(model => model.IsEmbeddingModel(this.Provider))] - }; - } + if (!host.IsEmbeddingSupported() || !host.IsModelListSupported()) + return ModelLoadResult.FromModels([]); - return ModelLoadResult.FromModels([]); + var result = await this.LoadModels(SecretStoreType.EMBEDDING_PROVIDER, apiKeyProvisional, token); + return result with + { + Models = [..result.Models.Where(model => model.IsEmbeddingModel(this.Provider))] + }; } catch(Exception e) { @@ -154,33 +144,28 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide { try { - switch (host) - { - case Host.WHISPER_CPP: - return ModelLoadResult.FromModels( - [ - new Provider.Model("loaded-model", TB("Model as configured by whisper.cpp")), - ]); - - // - // These two answer the models endpoint with everything they serve, and nothing in - // that answer says which of them listens. Asking what each model is made for is the - // only thing standing between this list and every chat and embedding model of the - // installation, which is what it used to hold. An engine running no speech model at - // all therefore offers nothing here, and says so, rather than offering models which - // would fail the moment audio reaches them. - // - case Host.OLLAMA: - case Host.VLLM: - var result = await this.LoadModels(SecretStoreType.TRANSCRIPTION_PROVIDER, apiKeyProvisional, token); - return result with - { - Models = [..result.Models.Where(model => model.IsTranscriptionModel(this.Provider))] - }; + if (host is Host.WHISPER_CPP) + return ModelLoadResult.FromModels( + [ + new Provider.Model("loaded-model", TB("Model as configured by whisper.cpp")), + ]); - default: - return ModelLoadResult.FromModels([]); - } + if (!host.IsTranscriptionSupported() || !host.IsModelListSupported()) + return ModelLoadResult.FromModels([]); + + // + // The engines which get here answer the models endpoint with everything they serve, and + // nothing in that answer says which of them listens. Asking what each model is made for + // is the only thing standing between this list and every chat and embedding model of + // the installation, which is what it used to hold. An engine running no speech model at + // all therefore offers nothing here, and says so, rather than offering models which + // would fail the moment audio reaches them. + // + var result = await this.LoadModels(SecretStoreType.TRANSCRIPTION_PROVIDER, apiKeyProvisional, token); + return result with + { + Models = [..result.Models.Where(model => model.IsTranscriptionModel(this.Provider))] + }; } catch (Exception e) { diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.10.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.10.1.md index ea4f496d..23a8b7a3 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.10.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.10.1.md @@ -8,6 +8,7 @@ - Added a setting to share which features of AI Studio you use with the operators of self-hosted AI servers, usually your organization. When you switch it on, requests to self-hosted providers and LiteLLM name the feature that sent them, such as the chat, an assistant, or an agent, together with the version of AI Studio and your operating system. Cloud providers never receive these details, and AI Studio sends nothing to its developers. The setting is off by default, and you find it in the app settings. Thanks, Dominic Neuburg (`donework`), for this contribution. - Added a way for IT departments to let requests to the self-hosted AI servers of your organization name the feature of AI Studio that sent them, either for everybody or as a default that users may change. This shows which features are used. The Enterprise IT documentation explains what is sent and what to clarify before you switch it on, for example, with your works council. - Added Requesty as a new LLM provider for chats, embeddings, and transcriptions. Requesty is an AI gateway from the United Kingdom that gives you models from many providers with a single API key. Because where your data is processed depends on the model you choose, you set its trust level yourself. Thanks, Thibault Jaigu (`Thibaultjaigu`), for this first contribution. +- Added [llmman](https://github.com/llmmanorg/llmman) as a new option for the models you host yourself. llmman runs open models on your own computer or server, and AI Studio can use them for chats, embeddings, and transcriptions. Thanks, Eric Curtin (`ericcurtin`), for this first contribution. - Improved the Read Web Page tool. When you have not allowed the AI to choose web addresses freely, the tool now refuses every address that does not appear in your chat or in what the tools returned, instead of only asking the AI not to read it. - Improved the installation on macOS: AI Studio is now signed and checked by Apple, so it opens like any other app. You no longer need the Terminal. - Improved the installation on Windows: AI Studio is now signed, so Windows shows who published it. For a while, Windows may still ask you to confirm the installation until it knows the app better. @@ -50,6 +51,7 @@ - Fixed the warning about unsaved changes in a chat appearing only by chance when you save your chats manually or have turned off workspaces. AI Studio now reliably asks before you open another chat, create a new one, or leave the chat page, so you no longer lose your changes by accident. - Fixed the security notice about removed instructions (prompt injection) appearing only minutes later while AI Studio updated a data source in the background. The notice also mixed your own attachments with files of the data source. Now each notice appears as soon as AI Studio has read the content, for example, the attachments of your message, and it names only that content. - Fixed the security notice about removed instructions (prompt injection) appearing several times for one answer when AI Studio checks which passages of your data sources fit your question: once for every passage it checked, and once more for all of them together. You now get one notice per answer, and a file counts once, however many of its passages were affected. +- Fixed the example configuration for IT departments, which spelled llama.cpp in a way AI Studio does not accept. AI Studio ignored a self-hosted provider set up with that spelling. The example now also names only the self-hosted servers which can create embeddings or transcribe audio. - Updated the code contributions on the supporters page, which now thank everyone who has contributed code to AI Studio so far. - Upgraded several libraries to improve security. - Upgraded to Rust v1.99.0 \ No newline at end of file diff --git a/app/Tests/Models/Corpus/ModelKindCorpus.cs b/app/Tests/Models/Corpus/ModelKindCorpus.cs index e91d27bf..6e7b4ef1 100644 --- a/app/Tests/Models/Corpus/ModelKindCorpus.cs +++ b/app/Tests/Models/Corpus/ModelKindCorpus.cs @@ -66,6 +66,14 @@ public static class ModelKindCorpus new(SELF_HOSTED, "LaBSE", EMBEDDING), new(SELF_HOSTED, "instructor-xl", EMBEDDING), new(SELF_HOSTED, "gtr-t5-large", EMBEDDING), + + // + // llmman lists every model by the full reference it pulled it from: a registry, a namespace, + // the repository, and a tag. Whatever is in front of the model has to come off before the + // word which says what it is for can be found: + // + new(SELF_HOSTED, "docker.io/ai/embeddinggemma:latest", EMBEDDING), + new(SELF_HOSTED, "docker.io/ai/nomic-embed-text-v1.5:latest", EMBEDDING), ]; /// @@ -144,6 +152,9 @@ public static class ModelKindCorpus new(ALIBABA_CLOUD, "qwen3-asr-flash", TRANSCRIPTION), new(ALIBABA_CLOUD, "qwen3-asr-1.7b", TRANSCRIPTION), new(ALIBABA_CLOUD, "qwen-audio-3.0-asr-flash-streaming", TRANSCRIPTION), + + // Voxtral as llmman lists it, from the GGUF build on Hugging Face which llama.cpp serves: + new(SELF_HOSTED, "hf.co/ggml-org/Voxtral-Mini-3B-2507-GGUF:latest", TRANSCRIPTION), ]; /// @@ -395,6 +406,14 @@ public static class ModelKindCorpus new(GROQ, "groq/compound", CHAT), new(GROQ, "groq/compound-mini", CHAT), new(GOOGLE, "gemini-robotics-er-1.5-preview", CHAT), + + // + // Chat models as llmman lists them, with the registry, the namespace, and the tag still in + // front of and behind the model. None of these parts may turn one into something else: + // + new(SELF_HOSTED, "hf.co/unsloth/Qwen3.5-0.8B-GGUF:latest", CHAT), + new(SELF_HOSTED, "docker.io/ai/qwen3.5:0.8b", CHAT), + new(SELF_HOSTED, "hf.co/HuggingFaceTB/SmolLM2-135M-Instruct:latest", CHAT), ]; /// diff --git a/app/Tests/Models/Hosting/ModelHostTests.cs b/app/Tests/Models/Hosting/ModelHostTests.cs index 42f8bd4e..3e8d4365 100644 --- a/app/Tests/Models/Hosting/ModelHostTests.cs +++ b/app/Tests/Models/Hosting/ModelHostTests.cs @@ -172,6 +172,25 @@ public sealed class ModelHostTests Assert.That(unwrapped.Original, Is.EqualTo("qwen3.8:27b-mlx")); } + [TestCase("hf.co/HuggingFaceTB/SmolLM2-135M-Instruct:latest", "SmolLM2-135M-Instruct:latest", ModelVendor.HUGGING_FACE, TestName = "Behind the registry, the organization from the hub names the vendor")] + [TestCase("hf.co/unsloth/Qwen3.5-0.8B-GGUF:latest", "Qwen3.5-0.8B-GGUF:latest", null, TestName = "An organization which only repackaged the weights names nobody")] + [TestCase("docker.io/ai/qwen3.5:0.8b", "qwen3.5:0.8b", null, TestName = "The registry and its namespace come off and the variant stays")] + public void AnLlmmanReferenceLosesItsRegistryAndKeepsItsTag(string modelId, string expectedModel, ModelVendor? expectedVendor) + { + // + // llmman lists the reference it pulled a model from, with the registry in front and the tag + // behind. The registry is a path segment like any organization and comes off the same way. + // The tag is what Ollama writes after its colon, too, and stays for the same reason. + // + var unwrapped = Unwrap(LLMProviders.SELF_HOSTED, modelId, out var vendor); + + Assert.Multiple(() => + { + Assert.That(unwrapped.Original, Is.EqualTo(expectedModel)); + Assert.That(vendor, Is.EqualTo(expectedVendor)); + }); + } + [Test] public void AResellerLeavesTheNameAloneAndOnlyTakesTheApiAway() { diff --git a/app/Tests/Provider/Reasoning/ReasoningDispatcherTests.cs b/app/Tests/Provider/Reasoning/ReasoningDispatcherTests.cs index f9dfe2f0..d0475eb1 100644 --- a/app/Tests/Provider/Reasoning/ReasoningDispatcherTests.cs +++ b/app/Tests/Provider/Reasoning/ReasoningDispatcherTests.cs @@ -46,11 +46,29 @@ public sealed class ReasoningDispatcherTests [TestCase(Host.LLAMA_CPP, """ "reasoning_budget": 0 """, ReasoningConfigurationState.EXPLICITLY_DISABLED)] [TestCase(Host.VLLM, """ "thinking_token_budget": 2048 """, ReasoningConfigurationState.EXPLICITLY_ENABLED)] [TestCase(Host.VLLM, """ "chat_template_kwargs": { "thinking": true } """, ReasoningConfigurationState.EXPLICITLY_ENABLED)] + [TestCase(Host.LLMMAN, """ "reasoning_budget": 0 """, ReasoningConfigurationState.EXPLICITLY_DISABLED, Description = "llmman serves GGUF models with llama.cpp and hands the budget to it.")] + [TestCase(Host.LLMMAN, """ "thinking_token_budget": 2048 """, ReasoningConfigurationState.EXPLICITLY_ENABLED, Description = "llmman serves other models with vLLM and hands the budget to it.")] public void EachSelfHostedEngineIsReadInItsOwn(Host host, string parameters, ReasoningConfigurationState wanted) { Assert.That(ReasoningDispatcher.WhatTheParametersSay(LLMProviders.SELF_HOSTED, host, parameters), Is.EqualTo(wanted)); } + [Test] + public void LlmmanIsReadInEverythingItsEnginesAreReadIn() + { + // + // llmman decides by itself whether llama.cpp or vLLM serves a model. A dialect either of + // them learns later has to reach llmman as well, or a switch set for llmman goes unseen: + // + var llmman = ReasoningDispatcher.DialectsOf(LLMProviders.SELF_HOSTED, Host.LLMMAN); + + Assert.Multiple(() => + { + Assert.That(llmman, Is.SupersetOf(ReasoningDispatcher.DialectsOf(LLMProviders.SELF_HOSTED, Host.LLAMA_CPP))); + Assert.That(llmman, Is.SupersetOf(ReasoningDispatcher.DialectsOf(LLMProviders.SELF_HOSTED, Host.VLLM))); + }); + } + [Test] public void AProviderIsNotReadInADialectItDoesNotSpeak() { diff --git a/app/Tests/Provider/SelfHosted/HostExtensionsTests.cs b/app/Tests/Provider/SelfHosted/HostExtensionsTests.cs new file mode 100644 index 00000000..a1187f0d --- /dev/null +++ b/app/Tests/Provider/SelfHosted/HostExtensionsTests.cs @@ -0,0 +1,69 @@ +using AIStudio.Provider; +using AIStudio.Provider.SelfHosted; + +namespace AIStudio.Tests.Provider.SelfHosted; + +/// +/// Checks that what each self-hosted engine can do is stated once, and that the statements fit together. +/// +/// +/// Which engines can list their models used to be written down twice, once for the provider and once +/// for the dialogs, and the loading of the chat, embedding, and transcription lists repeated the +/// engines a third time. A new engine had to be added to each of these lists, and one it was +/// forgotten in left a list empty or a reload button disabled without anything failing. Now the +/// host extensions state it, and these cases check that every other answer follows from them. +/// +[TestFixture] +public sealed class HostExtensionsTests +{ + private static readonly Host[] ALL_HOSTS = Enum.GetValues(); + + [TestCaseSource(nameof(ALL_HOSTS))] + public void TheDialogsOfferToLoadModelsExactlyWhereTheEngineCanListThem(Host host) + { + Assert.That(LLMProviders.SELF_HOSTED.CanLoadModels(host, null), Is.EqualTo(host.IsModelListSupported())); + } + + [Test] + public void EveryEngineForChatsCanListItsModels() + { + // + // There is no fixed chat model to offer instead, so such an engine would offer an empty list: + // + var unlisted = ALL_HOSTS.Where(host => host is not Host.NONE && host.IsChatSupported() && !host.IsModelListSupported()); + Assert.That(unlisted, Is.Empty); + } + + [Test] + public void EveryEngineForEmbeddingsCanListItsModels() + { + // + // There is no fixed embedding model to offer instead, so such an engine would offer an empty list: + // + var unlisted = ALL_HOSTS.Where(host => host.IsEmbeddingSupported() && !host.IsModelListSupported()); + Assert.That(unlisted, Is.Empty); + } + + [Test] + public void OnlyWhisperCppTranscribesWithoutListingItsModels() + { + // + // The provider offers the one model of whisper.cpp under a fixed name. Any other engine + // which transcribes but cannot list its models would offer an empty list: + // + var unlisted = ALL_HOSTS.Where(host => host.IsTranscriptionSupported() && !host.IsModelListSupported()); + Assert.That(unlisted, Is.EqualTo(new[] { Host.WHISPER_CPP })); + } + + [Test] + public void TheHostNoneLoadsNoModels() + { + Assert.Multiple(() => + { + Assert.That(Host.NONE.IsModelListSupported(), Is.False); + Assert.That(Host.NONE.IsEmbeddingSupported(), Is.False); + Assert.That(Host.NONE.IsTranscriptionSupported(), Is.False); + Assert.That(LLMProviders.SELF_HOSTED.CanLoadModels(Host.NONE, "a key"), Is.False); + }); + } +} \ No newline at end of file