From 53da222562b3b9986e4ddb931332cf96e5d0537b Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sat, 29 Aug 2026 20:45:39 +0200 Subject: [PATCH] Added IONOS as a provider and unified how providers sort their model lists (#941) --- README.md | 1 + app/MindWork AI Studio.sln.DotSettings | 2 + .../Assistants/I18N/allTexts.lua | 6 +- app/MindWork AI Studio/Pages/Home.razor.cs | 2 +- .../Plugins/configuration/plugin.lua | 4 +- .../plugin.lua | 6 +- .../plugin.lua | 6 +- .../AlibabaCloud/ProviderAlibabaCloud.cs | 8 +- .../Provider/Anthropic/ProviderAnthropic.cs | 4 +- .../Provider/DeepSeek/ProviderDeepSeek.cs | 2 +- .../Provider/GWDG/ProviderGWDG.cs | 6 +- .../Provider/Helmholtz/ProviderHelmholtz.cs | 9 +- .../Provider/Hetzner/ProviderHetzner.cs | 2 +- .../Provider/IONOS/ProviderIONOS.cs | 133 ++++++++++++++++++ .../Provider/LLMProviders.cs | 1 + .../Provider/LLMProvidersExtensions.cs | 16 +++ .../Provider/LLMProvidersIconExtensions.cs | 1 + .../Provider/Mistral/ProviderMistral.cs | 8 +- app/MindWork AI Studio/Provider/Model.cs | 34 +++-- app/MindWork AI Studio/Provider/ModelKind.cs | 69 +++++++++ .../Provider/ModelKindExtensions.cs | 116 +++++++++++++++ .../Provider/OpenRouter/ProviderOpenRouter.cs | 12 +- .../Settings/ProviderExtensions.Reasoning.cs | 1 + .../Settings/ProviderExtensions.cs | 1 + .../Settings/SettingsManager.cs | 2 + .../wwwroot/changelog/v26.8.2.md | 1 + .../wwwroot/images/provider-icons/README.md | 2 +- .../wwwroot/images/provider-icons/ionos.svg | 1 + .../org.mindworkai.AIStudio.metainfo.xml | 4 +- 29 files changed, 410 insertions(+), 50 deletions(-) create mode 100644 app/MindWork AI Studio/Provider/IONOS/ProviderIONOS.cs create mode 100644 app/MindWork AI Studio/Provider/ModelKind.cs create mode 100644 app/MindWork AI Studio/Provider/ModelKindExtensions.cs create mode 100644 app/MindWork AI Studio/wwwroot/images/provider-icons/ionos.svg diff --git a/README.md b/README.md index 81914d68..af1247df 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,7 @@ MindWork AI Studio is a free desktop app for macOS, Windows, and Linux. It provi - [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) + - [IONOS](https://cloud.ionos.com/managed/ai-model-hub) (AI Model Hub running open-source models in Germany) - [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/) diff --git a/app/MindWork AI Studio.sln.DotSettings b/app/MindWork AI Studio.sln.DotSettings index 8919d73e..d764983c 100644 --- a/app/MindWork AI Studio.sln.DotSettings +++ b/app/MindWork AI Studio.sln.DotSettings @@ -8,6 +8,7 @@ HF IERI IMIME + IONOS LLM LM MSG @@ -27,6 +28,7 @@ True True True + True True True True diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 4250b2ba..4323319b 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -8413,15 +8413,15 @@ 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" -- Independence UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T649448159"] = "Independence" +-- 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), IONOS, 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::T662688722"] = "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), IONOS, 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." + -- No bloatware UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T858047957"] = "No bloatware" diff --git a/app/MindWork AI Studio/Pages/Home.razor.cs b/app/MindWork AI Studio/Pages/Home.razor.cs index 2814a3e8..878c6a2d 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 (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("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), IONOS, 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.")), diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua index 13cd5d97..11459c12 100644 --- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua +++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua @@ -711,7 +711,8 @@ 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, HETZNER, FIREWORKS, GROQ, HUGGINGFACE, SELF_HOSTED, HELMHOLTZ, GWDG +-- PERPLEXITY, OPEN_ROUTER, HETZNER, IONOS, 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 @@ -729,6 +730,7 @@ CONFIG["SETTINGS"] = {} -- ["PERPLEXITY"] = "MODERATE", -- ["OPEN_ROUTER"] = "MODERATE", -- ["HETZNER"] = "HIGH", +-- ["IONOS"] = "HIGH", -- ["FIREWORKS"] = "MODERATE", -- ["GROQ"] = "MODERATE", -- ["HUGGINGFACE"] = "MODERATE", 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 7bd9022e..9dbf5820 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 @@ -8415,15 +8415,15 @@ 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" -- Independence UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T649448159"] = "Unabhängigkeit" +-- 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), IONOS, 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::T662688722"] = "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), IONOS, 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 und viele internationale Universitäten verfügbar." + -- No bloatware UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T858047957"] = "Keinen unnötigen Software-Ballast" 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 9c9fd1f6..88b7c11f 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 @@ -8415,15 +8415,15 @@ 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" -- Independence UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T649448159"] = "Independence" +-- 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), IONOS, 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::T662688722"] = "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), IONOS, 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." + -- No bloatware UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T858047957"] = "No bloatware" diff --git a/app/MindWork AI Studio/Provider/AlibabaCloud/ProviderAlibabaCloud.cs b/app/MindWork AI Studio/Provider/AlibabaCloud/ProviderAlibabaCloud.cs index 2382f95f..10d77ed8 100644 --- a/app/MindWork AI Studio/Provider/AlibabaCloud/ProviderAlibabaCloud.cs +++ b/app/MindWork AI Studio/Provider/AlibabaCloud/ProviderAlibabaCloud.cs @@ -105,7 +105,9 @@ public sealed class ProviderAlibabaCloud() : BaseProvider(LLMProviders.ALIBABA_C var result = await this.LoadModels(["q"], SecretStoreType.LLM_PROVIDER, token, apiKeyProvisional); return result with { - Models = [..result.Models.Concat(additionalModels).OrderBy(x => x.Id)] + // The API is the authority: when it reports a model we also keep as a fallback above, + // its entry comes first and the fallback is dropped. + Models = [..result.Models.Concat(additionalModels).DistinctBy(x => x.Id).OrderBy(x => x.Id)] }; } @@ -127,7 +129,9 @@ public sealed class ProviderAlibabaCloud() : BaseProvider(LLMProviders.ALIBABA_C var result = await this.LoadModels(["text-embedding-"], SecretStoreType.EMBEDDING_PROVIDER, token, apiKeyProvisional); return result with { - Models = [..result.Models.Concat(additionalModels).OrderBy(x => x.Id)] + // The API is the authority: when it reports a model we also keep as a fallback above, + // its entry comes first and the fallback is dropped. + Models = [..result.Models.Concat(additionalModels).DistinctBy(x => x.Id).OrderBy(x => x.Id)] }; } diff --git a/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs b/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs index c1277911..99eefd8e 100644 --- a/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs +++ b/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs @@ -143,7 +143,9 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, n var result = await this.LoadModels(SecretStoreType.LLM_PROVIDER, token, apiKeyProvisional); return result with { - Models = [..result.Models.Concat(additionalModels).OrderBy(x => x.Id)] + // The API is the authority: when it reports a model we also keep as a fallback above, + // its entry comes first and the fallback is dropped. + Models = [..result.Models.Concat(additionalModels).DistinctBy(x => x.Id).OrderBy(x => x.Id)] }; } diff --git a/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs b/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs index 03e10255..42a87ebc 100644 --- a/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs +++ b/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs @@ -102,7 +102,7 @@ public sealed class ProviderDeepSeek() : BaseProvider(LLMProviders.DEEP_SEEK, ne return this.LoadModelsResponse( storeType, "models", - modelResponse => modelResponse.Data, + modelResponse => modelResponse.Data.Where(model => model.IsChatModel()), token, apiKeyProvisional); } diff --git a/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs b/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs index ac44d28e..ccca041c 100644 --- a/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs +++ b/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs @@ -78,7 +78,7 @@ public sealed class ProviderGWDG() : BaseProvider(LLMProviders.GWDG, new Uri("ht var result = await this.LoadModels(SecretStoreType.LLM_PROVIDER, token, apiKeyProvisional); return result with { - Models = [..result.Models.Where(model => !model.Id.StartsWith("e5-mistral-7b-instruct", StringComparison.InvariantCultureIgnoreCase))] + Models = [..result.Models.Where(model => model.IsChatModel())] }; } @@ -94,7 +94,7 @@ public sealed class ProviderGWDG() : BaseProvider(LLMProviders.GWDG, new Uri("ht var result = await this.LoadModels(SecretStoreType.EMBEDDING_PROVIDER, token, apiKeyProvisional); return result with { - Models = [..result.Models.Where(model => model.Id.StartsWith("e5-", StringComparison.InvariantCultureIgnoreCase))] + Models = [..result.Models.Where(model => model.IsEmbeddingModel())] }; } @@ -124,4 +124,4 @@ public sealed class ProviderGWDG() : BaseProvider(LLMProviders.GWDG, new Uri("ht return result; } -} +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs b/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs index 27aa4b05..d7b975b7 100644 --- a/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs +++ b/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs @@ -82,9 +82,7 @@ public sealed class ProviderHelmholtz() : BaseProvider(LLMProviders.HELMHOLTZ, n { Models = [ - ..result.Models.Where(model => !model.Id.StartsWith("text-", StringComparison.InvariantCultureIgnoreCase) && - !model.Id.Contains("-embedding", StringComparison.InvariantCultureIgnoreCase) - ) + ..result.Models.Where(model => model.IsChatModel()) ] }; } @@ -103,10 +101,7 @@ public sealed class ProviderHelmholtz() : BaseProvider(LLMProviders.HELMHOLTZ, n { Models = [ - ..result.Models.Where(model => - model.Id.Contains("-embedding", StringComparison.InvariantCultureIgnoreCase) || - model.Id.StartsWith("text-", StringComparison.InvariantCultureIgnoreCase) || - model.Id.Contains("gritlm", StringComparison.InvariantCultureIgnoreCase)) + ..result.Models.Where(model => model.IsEmbeddingModel()) ] }; } diff --git a/app/MindWork AI Studio/Provider/Hetzner/ProviderHetzner.cs b/app/MindWork AI Studio/Provider/Hetzner/ProviderHetzner.cs index a2004f23..85ee42f5 100644 --- a/app/MindWork AI Studio/Provider/Hetzner/ProviderHetzner.cs +++ b/app/MindWork AI Studio/Provider/Hetzner/ProviderHetzner.cs @@ -68,7 +68,7 @@ public sealed class ProviderHetzner() : BaseProvider(LLMProviders.HETZNER, new U /// public override Task GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) { - return this.LoadModelsResponse(SecretStoreType.LLM_PROVIDER, "models", modelResponse => modelResponse.Data, token, apiKeyProvisional); + return this.LoadModelsResponse(SecretStoreType.LLM_PROVIDER, "models", modelResponse => modelResponse.Data.Where(model => model.IsChatModel()), token, apiKeyProvisional); } /// diff --git a/app/MindWork AI Studio/Provider/IONOS/ProviderIONOS.cs b/app/MindWork AI Studio/Provider/IONOS/ProviderIONOS.cs new file mode 100644 index 00000000..9de0df27 --- /dev/null +++ b/app/MindWork AI Studio/Provider/IONOS/ProviderIONOS.cs @@ -0,0 +1,133 @@ +using System.Runtime.CompilerServices; + +using AIStudio.Chat; +using AIStudio.Provider.OpenAI; +using AIStudio.Settings; + +namespace AIStudio.Provider.IONOS; + +public sealed class ProviderIONOS() : BaseProvider(LLMProviders.IONOS, new Uri("https://openai.inference.de-txl.ionos.com/v1/"), ExternalHttpTrustPolicy.SYSTEM_TRUST_ONLY, LOGGER) +{ + /// + /// IONOS keeps an alias of some embedding models around, so that customers can migrate away from + /// the previous naming. Those aliases point to the very same models we already offer, which is + /// why we hide them instead of listing every embedding model twice. + /// + private const string MIGRATION_ALIAS_SUFFIX = "-migration"; + + private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(); + + #region Implementation of IProvider + + /// + public override string Id => LLMProviders.IONOS.ToSecretId(); + + /// + public override string InstanceName { get; set; } = "IONOS"; + + /// + public override bool HasModelLoadingCapability => true; + + /// + public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) + { + await foreach (var content in this.StreamOpenAICompatibleChatCompletion( + "IONOS", + chatModel, + chatThread, + settingsManager, + async (systemPrompt, apiParameters) => + { + // Build the list of messages: + var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel); + + return new ChatCompletionAPIRequest + { + Model = chatModel.Id, + + // Build the messages: + // - First of all the system prompt + // - Then none-empty user and AI messages + Messages = [systemPrompt, ..messages], + + // Right now, we only support streaming completions: + Stream = true, + AdditionalApiParameters = apiParameters + }; + }, + token: token)) + yield return content; + } + + #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + /// + public override async IAsyncEnumerable 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 + + /// + public override Task TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default) + { + return Task.FromResult(TranscriptionResult.Failure()); + } + + /// + public override async Task>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) + { + var requestedSecret = await Program.RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER); + return await this.PerformStandardTextEmbeddingRequest(requestedSecret, embeddingModel, token: token, texts: texts); + } + + /// + public override Task GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) + { + return this.LoadModels(SecretStoreType.LLM_PROVIDER, model => model.IsChatModel(), token, apiKeyProvisional); + } + + /// + public override Task GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default) + { + return Task.FromResult(ModelLoadResult.FromModels([])); + } + + /// + public override Task GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default) + { + return this.LoadModels(SecretStoreType.EMBEDDING_PROVIDER, model => model.IsEmbeddingModel(), token, apiKeyProvisional); + } + + /// + public override Task GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default) + { + return Task.FromResult(ModelLoadResult.FromModels([])); + } + + #endregion + + /// + /// Loads the models of one kind from IONOS. + /// + /// + /// IONOS serves chat, embedding, reranking, OCR, and image models through one endpoint, and its + /// response tells us nothing but the model's name. We therefore let the shared model kind + /// detection sort them apart. + /// + /// The secret store to read the API key from. + /// Decides whether a model belongs to the requested kind. + /// The cancellation token. + /// An API key which was not stored yet. + /// The models of the requested kind. + private Task LoadModels(SecretStoreType storeType, Func isWantedKind, CancellationToken token, string? apiKeyProvisional = null) + { + return this.LoadModelsResponse( + storeType, + "models", + modelResponse => modelResponse.Data + .Where(model => !model.Id.EndsWith(MIGRATION_ALIAS_SUFFIX, StringComparison.OrdinalIgnoreCase)) + .Where(isWantedKind), + token, + apiKeyProvisional); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/LLMProviders.cs b/app/MindWork AI Studio/Provider/LLMProviders.cs index 740b2f4b..499e2659 100644 --- a/app/MindWork AI Studio/Provider/LLMProviders.cs +++ b/app/MindWork AI Studio/Provider/LLMProviders.cs @@ -17,6 +17,7 @@ public enum LLMProviders PERPLEXITY = 14, OPEN_ROUTER = 15, HETZNER = 16, + IONOS = 17, FIREWORKS = 5, GROQ = 6, diff --git a/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs b/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs index 4cb91ac1..7cde6206 100644 --- a/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs +++ b/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs @@ -8,6 +8,7 @@ using AIStudio.Provider.GWDG; using AIStudio.Provider.Helmholtz; using AIStudio.Provider.Hetzner; using AIStudio.Provider.HuggingFace; +using AIStudio.Provider.IONOS; using AIStudio.Provider.Mistral; using AIStudio.Provider.OpenAI; using AIStudio.Provider.OpenRouter; @@ -58,6 +59,7 @@ public static class LLMProvidersExtensions LLMProviders.PERPLEXITY => "Perplexity", LLMProviders.OPEN_ROUTER => "OpenRouter", LLMProviders.HETZNER => "Hetzner (Experimental)", + LLMProviders.IONOS => "IONOS", LLMProviders.GROQ => "Groq", LLMProviders.FIREWORKS => "Fireworks.ai", @@ -94,6 +96,7 @@ public static class LLMProvidersExtensions LLMProviders.PERPLEXITY => "Perplexity", LLMProviders.OPEN_ROUTER => "OpenRouter", LLMProviders.HETZNER => "Hetzner", + LLMProviders.IONOS => "IONOS", LLMProviders.GROQ => "Groq", LLMProviders.FIREWORKS => "Fireworks.ai", @@ -153,6 +156,11 @@ public static class LLMProvidersExtensions "https://www.hetzner.com/legal/terms-and-conditions/" ).WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), + LLMProviders.IONOS => Confidence.GDPR_NO_TRAINING.WithRegion("Europe, Germany").WithSources( + "https://docs.ionos.com/cloud/ai/ai-model-hub/governance-and-compliance/data-handling", + "https://www.ionos.com/terms-gtc/privacy-policy/" + ).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)), @@ -176,6 +184,7 @@ public static class LLMProvidersExtensions LLMProviders.GOOGLE => true, LLMProviders.HELMHOLTZ => true, LLMProviders.ALIBABA_CLOUD => true, + LLMProviders.IONOS => true, // // Providers that do not support embeddings: @@ -220,6 +229,7 @@ public static class LLMProvidersExtensions // LLMProviders.OPEN_ROUTER => false, LLMProviders.HETZNER => false, + LLMProviders.IONOS => false, LLMProviders.GROQ => false, LLMProviders.ANTHROPIC => false, LLMProviders.X => false, @@ -283,6 +293,7 @@ public static class LLMProvidersExtensions 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.IONOS => new ProviderIONOS { 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 }, @@ -315,6 +326,7 @@ public static class LLMProvidersExtensions LLMProviders.PERPLEXITY => "https://www.perplexity.ai/account/api", LLMProviders.OPEN_ROUTER => "https://openrouter.ai/keys", LLMProviders.HETZNER => "https://experiments.hetzner.com", + LLMProviders.IONOS => "https://cloud.ionos.com/compute/sign-up", LLMProviders.GROQ => "https://console.groq.com/", LLMProviders.FIREWORKS => "https://fireworks.ai/login", @@ -341,6 +353,7 @@ public static class LLMProvidersExtensions LLMProviders.OPEN_ROUTER => "https://openrouter.ai/activity", LLMProviders.HUGGINGFACE => "https://huggingface.co/settings/billing", LLMProviders.HETZNER => "https://experiments.hetzner.com", + LLMProviders.IONOS => "https://dcd.ionos.com/latest/?page=dcd-ai-model-hub", _ => string.Empty, }; @@ -360,6 +373,7 @@ public static class LLMProvidersExtensions LLMProviders.OPEN_ROUTER => true, LLMProviders.HUGGINGFACE => true, LLMProviders.HETZNER => true, + LLMProviders.IONOS => true, _ => false, }; @@ -438,6 +452,7 @@ public static class LLMProvidersExtensions LLMProviders.PERPLEXITY => true, LLMProviders.OPEN_ROUTER => true, LLMProviders.HETZNER => true, + LLMProviders.IONOS => true, LLMProviders.GROQ => true, LLMProviders.FIREWORKS => true, @@ -462,6 +477,7 @@ public static class LLMProvidersExtensions LLMProviders.PERPLEXITY => true, LLMProviders.OPEN_ROUTER => true, LLMProviders.HETZNER => true, + LLMProviders.IONOS => true, LLMProviders.GROQ => true, LLMProviders.FIREWORKS => true, diff --git a/app/MindWork AI Studio/Provider/LLMProvidersIconExtensions.cs b/app/MindWork AI Studio/Provider/LLMProvidersIconExtensions.cs index d47ff27a..cd83b62b 100644 --- a/app/MindWork AI Studio/Provider/LLMProvidersIconExtensions.cs +++ b/app/MindWork AI Studio/Provider/LLMProvidersIconExtensions.cs @@ -29,6 +29,7 @@ public static class LLMProvidersIconExtensions LLMProviders.PERPLEXITY => $"{ICON_ROOT}/perplexity.svg", LLMProviders.OPEN_ROUTER => $"{ICON_ROOT}/openrouter.svg", LLMProviders.HETZNER => $"{ICON_ROOT}/hetzner.svg", + LLMProviders.IONOS => $"{ICON_ROOT}/ionos.svg", LLMProviders.GROQ => $"{ICON_ROOT}/groq.svg", LLMProviders.FIREWORKS => $"{ICON_ROOT}/fireworks.svg", LLMProviders.HUGGINGFACE => $"{ICON_ROOT}/hugging-face.svg", diff --git a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs index 9f70fe16..27107cfb 100644 --- a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs +++ b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs @@ -91,10 +91,12 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, new U { Models = [ + // Codestral is a fill-in-the-middle model, which we cannot use for chats. That is + // specific to Mistral's catalog, which is why it is not part of the shared model + // kind detection: ..modelResponse.Models.Where(n => !n.Id.StartsWith("code", StringComparison.OrdinalIgnoreCase) && - !n.Id.Contains("embed", StringComparison.OrdinalIgnoreCase) && - !n.Id.Contains("moderation", StringComparison.OrdinalIgnoreCase)) + n.IsChatModel()) ] }; } @@ -108,7 +110,7 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, new U return modelResponse with { - Models = [..modelResponse.Models.Where(n => n.Id.Contains("embed", StringComparison.InvariantCulture))] + Models = [..modelResponse.Models.Where(n => n.IsEmbeddingModel())] }; } diff --git a/app/MindWork AI Studio/Provider/Model.cs b/app/MindWork AI Studio/Provider/Model.cs index f0b64539..97ca3bbf 100644 --- a/app/MindWork AI Studio/Provider/Model.cs +++ b/app/MindWork AI Studio/Provider/Model.cs @@ -42,15 +42,33 @@ public readonly record struct Model(string Id, string? DisplayName) #endregion - #region Implementation of IEquatable + #region Implementation of IEquatable - public bool Equals(Model? other) - { - if(other is null) - return false; - - return this.Id == other.Value.Id; - } + /// + /// Two models are the same model when they carry the same ID. + /// + /// + /// The display name is decoration. A provider may report a model under a display name of its own, + /// while we know the very same model as a hardcoded fallback under a different one. Comparing the + /// ID alone keeps those two the same model, so that removing duplicates works. + /// + /// Note that this overload is the one the runtime uses, for example for Distinct(). The overload + /// taking a nullable model below is a separate one and never gets called on its behalf, which is + /// why the hash code has to follow this one. + /// + /// The model to compare with. + /// True, when both models carry the same ID. + public bool Equals(Model other) => string.Equals(this.Id, other.Id, StringComparison.Ordinal); + + /// + /// Two models are the same model when they carry the same ID. + /// + /// The model to compare with, which may be null. + /// True, when the other model exists and carries the same ID. + public bool Equals(Model? other) => other is not null && this.Equals(other.Value); + + /// + public override int GetHashCode() => this.Id?.GetHashCode(StringComparison.Ordinal) ?? 0; #endregion } \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/ModelKind.cs b/app/MindWork AI Studio/Provider/ModelKind.cs new file mode 100644 index 00000000..30e10945 --- /dev/null +++ b/app/MindWork AI Studio/Provider/ModelKind.cs @@ -0,0 +1,69 @@ +namespace AIStudio.Provider; + +/// +/// The kind of an AI model, i.e. what the model is made for. +/// +/// +/// This describes what kind of model we are dealing with. It answers a different question than the +/// Capability enum: capabilities describe what a chat model is able to do, for example whether it +/// accepts images or performs reasoning. Note that Capability.EMBEDDING marks a chat model which is +/// able to create embeddings as well, whereas ModelKind.EMBEDDING marks a model whose only purpose +/// is creating embeddings. +/// +public enum ModelKind +{ + /// + /// The model is used for chat completions. + /// + /// + /// This is the fallback: we report a model as a chat model whenever we do not recognize any + /// other kind. Providers keep adding models we have never heard of, and a model we fail to + /// recognize must stay visible to the user instead of silently disappearing from their list. + /// + CHAT, + + /// + /// The model continues a text instead of answering in a conversation. + /// + /// + /// These are the models from the era before chat completions, such as OpenAI's text-davinci-003. + /// Some providers still offer them, but they only work through the completions endpoint. Asking + /// them for a chat completion fails, so they must not show up as chat models. + /// + TEXT_COMPLETION, + + /// + /// The model maps text or images into a vector space. + /// + EMBEDDING, + + /// + /// The model scores documents against a query to reorder search results. + /// + RERANKING, + + /// + /// The model generates or edits images. + /// + IMAGE_GENERATION, + + /// + /// The model transcribes audio into text. + /// + TRANSCRIPTION, + + /// + /// The model synthesizes speech from text. + /// + SPEECH_SYNTHESIS, + + /// + /// The model extracts text from images or scanned documents. + /// + OCR, + + /// + /// The model classifies content for policy violations. + /// + MODERATION, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/ModelKindExtensions.cs b/app/MindWork AI Studio/Provider/ModelKindExtensions.cs new file mode 100644 index 00000000..38f14945 --- /dev/null +++ b/app/MindWork AI Studio/Provider/ModelKindExtensions.cs @@ -0,0 +1,116 @@ +namespace AIStudio.Provider; + +/// +/// Determines what kind of model we are dealing with, based on its name. +/// +/// +/// Many providers serve every kind of model through one models endpoint, without telling us what +/// kind each model is. Before this class existed, every provider carried its own list of name +/// fragments to sort those models apart. Those lists disagreed with each other: a model like +/// nomic-embed-text was recognized as an embedding model by some providers, while others offered it +/// as a chat model. The knowledge about model families is the same for all providers, so it lives +/// here now. +/// +/// This class recognizes what a model is NOT made for. Everything we do not recognize is reported as +/// a chat model. That direction matters: when a provider adds a model family we have never seen, the +/// user still gets to use it. Getting it wrong the other way around would hide a model the user is +/// paying for. +/// +/// What this class must not become is a place for provider-specific knowledge. That a model called +/// "codestral" is a fill-in-the-middle model at Mistral, or that Alibaba's chat models all start +/// with a "q", is true for that one provider only. Such rules stay in the provider. +/// +public static class ModelKindExtensions +{ + // + // Reranking is checked before embedding: rerankers are commonly named after the embedding model + // they belong to, e.g. Qwen3-VL-Reranker-8B next to Qwen3-VL-Embedding-8B. + // + private static readonly string[] RERANKING_MARKERS = ["rerank"]; + + private static readonly string[] EMBEDDING_MARKERS = ["embed", "bge", "mpnet", "paraphrase", "sentence-transformers", "gte-", "e5-", "gritlm"]; + + // + // The models from before chat completions existed. Providers keep offering some of them, and + // Helmholtz Blablador still reports 'text-davinci-003', but asking any of them for a chat + // completion fails. We deliberately do not look for 'ada' here: three letters appear in far too + // many unrelated model names, and losing a chat model weighs heavier than keeping a dead one. + // + private static readonly string[] TEXT_COMPLETION_MARKERS = ["davinci", "babbage", "curie", "gpt-3.5-turbo-instruct"]; + + private static readonly string[] IMAGE_GENERATION_MARKERS = ["flux", "stable-diffusion", "sdxl", "dall-e", "midjourney", "gpt-image"]; + + // + // Voxtral is marketed as an audio model which understands speech, so one could expect it to work + // in a chat as well. It does not: asking Mistral for a chat completion with 'voxtral-mini-latest' + // is answered with 'Invalid model'. Voxtral therefore belongs here, next to the models which do + // nothing but transcribe. + // + private static readonly string[] TRANSCRIPTION_MARKERS = ["whisper", "-transcribe", "wav2vec", "parakeet", "voxtral"]; + + private static readonly string[] SPEECH_SYNTHESIS_MARKERS = ["-tts", "tts-", "-speech", "speech-"]; + + private static readonly string[] OCR_MARKERS = ["ocr"]; + + private static readonly string[] MODERATION_MARKERS = ["moderation", "guard"]; + + /// + /// Determines what kind of model this is, based on its name. + /// + /// The model to inspect. + /// The recognized kind, or ModelKind.CHAT when we recognize no other kind. + public static ModelKind DetermineKind(this Model model) + { + if (string.IsNullOrWhiteSpace(model.Id) || model.IsSystemModel) + return ModelKind.CHAT; + + if (HasAnyMarker(model.Id, RERANKING_MARKERS)) + return ModelKind.RERANKING; + + if (HasAnyMarker(model.Id, EMBEDDING_MARKERS)) + return ModelKind.EMBEDDING; + + if (HasAnyMarker(model.Id, TEXT_COMPLETION_MARKERS)) + return ModelKind.TEXT_COMPLETION; + + if (HasAnyMarker(model.Id, IMAGE_GENERATION_MARKERS)) + return ModelKind.IMAGE_GENERATION; + + if (HasAnyMarker(model.Id, TRANSCRIPTION_MARKERS)) + return ModelKind.TRANSCRIPTION; + + if (HasAnyMarker(model.Id, SPEECH_SYNTHESIS_MARKERS)) + return ModelKind.SPEECH_SYNTHESIS; + + if (HasAnyMarker(model.Id, OCR_MARKERS)) + return ModelKind.OCR; + + if (HasAnyMarker(model.Id, MODERATION_MARKERS)) + return ModelKind.MODERATION; + + return ModelKind.CHAT; + } + + /// + /// Checks whether this model can be used for chatting. + /// + /// The model to check. + /// True, when the model is a chat model or when we recognize no other kind. + public static bool IsChatModel(this Model model) => model.DetermineKind() is ModelKind.CHAT; + + /// + /// Checks whether this model creates embeddings. + /// + /// The model to check. + /// True, when the model is an embedding model. + public static bool IsEmbeddingModel(this Model model) => model.DetermineKind() is ModelKind.EMBEDDING; + + private static bool HasAnyMarker(string modelId, string[] markers) + { + foreach (var marker in markers) + if (modelId.Contains(marker, StringComparison.OrdinalIgnoreCase)) + return true; + + return false; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/OpenRouter/ProviderOpenRouter.cs b/app/MindWork AI Studio/Provider/OpenRouter/ProviderOpenRouter.cs index 1d5654d8..e6737bd3 100644 --- a/app/MindWork AI Studio/Provider/OpenRouter/ProviderOpenRouter.cs +++ b/app/MindWork AI Studio/Provider/OpenRouter/ProviderOpenRouter.cs @@ -115,16 +115,8 @@ public sealed class ProviderOpenRouter() : BaseProvider(LLMProviders.OPEN_ROUTER storeType, "models", modelResponse => modelResponse.Data - .Where(n => - !n.Id.Contains("whisper", StringComparison.OrdinalIgnoreCase) && - !n.Id.Contains("dall-e", StringComparison.OrdinalIgnoreCase) && - !n.Id.Contains("tts", StringComparison.OrdinalIgnoreCase) && - !n.Id.Contains("embedding", StringComparison.OrdinalIgnoreCase) && - !n.Id.Contains("moderation", StringComparison.OrdinalIgnoreCase) && - !n.Id.Contains("stable-diffusion", StringComparison.OrdinalIgnoreCase) && - !n.Id.Contains("flux", StringComparison.OrdinalIgnoreCase) && - !n.Id.Contains("midjourney", StringComparison.OrdinalIgnoreCase)) - .Select(n => new Model(n.Id, n.Name)), + .Select(n => new Model(n.Id, n.Name)) + .Where(model => model.IsChatModel()), token, apiKeyProvisional, requestConfigurator: (request, secretKey) => diff --git a/app/MindWork AI Studio/Settings/ProviderExtensions.Reasoning.cs b/app/MindWork AI Studio/Settings/ProviderExtensions.Reasoning.cs index 437aae9b..aeaa39a7 100644 --- a/app/MindWork AI Studio/Settings/ProviderExtensions.Reasoning.cs +++ b/app/MindWork AI Studio/Settings/ProviderExtensions.Reasoning.cs @@ -91,6 +91,7 @@ public static partial class ProviderExtensions LLMProviders.OPEN_ROUTER or LLMProviders.HETZNER or + LLMProviders.IONOS or LLMProviders.X or LLMProviders.DEEP_SEEK or LLMProviders.GROQ or diff --git a/app/MindWork AI Studio/Settings/ProviderExtensions.cs b/app/MindWork AI Studio/Settings/ProviderExtensions.cs index 6dc38cff..a2de33ce 100644 --- a/app/MindWork AI Studio/Settings/ProviderExtensions.cs +++ b/app/MindWork AI Studio/Settings/ProviderExtensions.cs @@ -55,6 +55,7 @@ public static partial class ProviderExtensions LLMProviders.PERPLEXITY => GetModelCapabilitiesPerplexity(model), LLMProviders.OPEN_ROUTER => GetModelCapabilitiesOpenRouter(model), LLMProviders.HETZNER => GetModelCapabilitiesOpenSource(model), + LLMProviders.IONOS => GetModelCapabilitiesOpenSource(model), LLMProviders.GROQ => GetModelCapabilitiesOpenSource(model), LLMProviders.FIREWORKS => GetModelCapabilitiesOpenSource(model), diff --git a/app/MindWork AI Studio/Settings/SettingsManager.cs b/app/MindWork AI Studio/Settings/SettingsManager.cs index fe78548b..13587f50 100644 --- a/app/MindWork AI Studio/Settings/SettingsManager.cs +++ b/app/MindWork AI Studio/Settings/SettingsManager.cs @@ -817,6 +817,7 @@ public sealed class SettingsManager LLMProviders.HELMHOLTZ => ConfidenceLevel.LOW, LLMProviders.GWDG => ConfidenceLevel.LOW, LLMProviders.HETZNER => ConfidenceLevel.LOW, + LLMProviders.IONOS => ConfidenceLevel.LOW, LLMProviders.DEEP_SEEK => ConfidenceLevel.LOW, LLMProviders.ALIBABA_CLOUD => ConfidenceLevel.LOW, @@ -831,6 +832,7 @@ public sealed class SettingsManager LLMProviders.HELMHOLTZ => ConfidenceLevel.MEDIUM, LLMProviders.GWDG => ConfidenceLevel.MEDIUM, LLMProviders.HETZNER => ConfidenceLevel.MEDIUM, + LLMProviders.IONOS => ConfidenceLevel.MEDIUM, _ => ConfidenceLevel.LOW, }; diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md b/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md index 3cfaa98f..9bf4adba 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md @@ -4,6 +4,7 @@ - Added provider logos throughout AI Studio, making models easier to recognize at a glance. Configuration plugins can now give managed LLM, transcription, and embedding providers their own project icon with the optional `IconPath` field. - Added the option for IT departments to enable assistant plugins they rolled out. Approving an assistant only stated that it is safe, so everybody still had to switch it on themselves. An approval can now also enable the assistant, either as a default which you may switch off again, or in a way your IT department keeps in place. The plugin page and the security card of the assistant tell you which of the two applies. - Added knowledge about the latest AI models. AI Studio now recognizes Qwen 3.8 Flash, GLM-5.3 Flash, Meta's Muse Glimmer, NVIDIA's Nemotron 3.5, Tencent's Hunyuan Hy3, Grok 4, Claude Opus 5 and Sonnet 5, and Gemini 3.6 and 3.7. It knows what each of them is capable of, so images, videos, tool usage, and reasoning are available right away instead of staying hidden. +- Added the IONOS AI Model Hub as a provider for chats and embeddings. It runs open-source models in Germany, is subject to the GDPR, and IONOS states that your data is not used for training. - Improved the safety of plugin symbols: AI Studio now shows the symbol of a plugin in isolation, so nothing inside a symbol can reach the rest of the app. - Improved how much memory AI Studio needs. Working with large documents used to grow the app to several gigabytes, and on macOS that memory was never handed back. AI Studio now stays at a fraction of that and returns memory to your system. This matters most on devices with little memory, such as a Raspberry Pi. - Improved the preview for large documents. It now shows you the beginning of your document instead of loading all of it, so the dialog opens right away. Your complete document still goes to the AI. diff --git a/app/MindWork AI Studio/wwwroot/images/provider-icons/README.md b/app/MindWork AI Studio/wwwroot/images/provider-icons/README.md index 88246d1d..dfe3a5db 100644 --- a/app/MindWork AI Studio/wwwroot/images/provider-icons/README.md +++ b/app/MindWork AI Studio/wwwroot/images/provider-icons/README.md @@ -4,7 +4,7 @@ All provider icons are shipped with AI Studio and loaded locally. No icon trigge ## Sources -- `alibaba-cloud.svg`, `anthropic*.svg`, `deepseek.svg`, `hetzner.svg`, `mistral.svg`, `perplexity.svg`, and `x*.svg` use paths and brand colors from [Simple Icons 16.21.0](https://github.com/simple-icons/simple-icons/tree/16.21.0), licensed under [CC0-1.0](https://github.com/simple-icons/simple-icons/blob/16.21.0/LICENSE.md). +- `alibaba-cloud.svg`, `anthropic*.svg`, `deepseek.svg`, `hetzner.svg`, `ionos.svg`, `mistral.svg`, `perplexity.svg`, and `x*.svg` use paths and brand colors from [Simple Icons 16.21.0](https://github.com/simple-icons/simple-icons/tree/16.21.0), licensed under [CC0-1.0](https://github.com/simple-icons/simple-icons/blob/16.21.0/LICENSE.md). - `hugging-face.svg` was taken from their official website (https://huggingface.co/brand) - `openai*.svg` uses the OpenAI mark path from [Simple Icons 15.15.0](https://github.com/simple-icons/simple-icons/blob/15.15.0/icons/openai.svg) and black/white variants following the [OpenAI Design Guidelines](https://openai.com/brand/). - `fireworks.svg` is adapted from the [Fireworks AI site icon](https://fireworks.ai/icon0.svg). diff --git a/app/MindWork AI Studio/wwwroot/images/provider-icons/ionos.svg b/app/MindWork AI Studio/wwwroot/images/provider-icons/ionos.svg new file mode 100644 index 00000000..a7d74425 --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/images/provider-icons/ionos.svg @@ -0,0 +1 @@ + diff --git a/runtime/packaging/linux/org.mindworkai.AIStudio.metainfo.xml b/runtime/packaging/linux/org.mindworkai.AIStudio.metainfo.xml index ff0dc201..58e7718a 100644 --- a/runtime/packaging/linux/org.mindworkai.AIStudio.metainfo.xml +++ b/runtime/packaging/linux/org.mindworkai.AIStudio.metainfo.xml @@ -31,8 +31,8 @@
  • Independence: You are not tied to any single provider. Choose the providers that best suit your needs, including OpenAI, Perplexity, Mistral, Anthropic, Google Gemini, xAI, - DeepSeek, Alibaba Cloud, OpenRouter, Hugging Face, Groq, Fireworks, Helmholtz, GWDG, - and self-hosted models. + DeepSeek, Alibaba Cloud, OpenRouter, Hetzner, IONOS, Hugging Face, Groq, Fireworks, + Helmholtz, GWDG, and self-hosted models.
  • Assistants: Use ready-made assistants for common business and other tasks without writing prompts yourself.