From 7ec39e928c58e91a65448caea362cf1575b4361a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20B=C3=BCrgi?= Date: Fri, 10 Jul 2026 22:19:16 +0200 Subject: [PATCH 1/2] AI models show their provider's brand logo in the model picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every LanguageModel node carried a generic "Sparkle" icon (and every ModelProvider a "Key"), so the picker rendered the same mark for every model. Add per-provider brand icons: 13 tile SVGs (brand colour + white glyph, matching the existing NodeType icon design) under MeshWeaver.Graph/Icons, plus a ModelProviderIcons resolver that maps a model to its maker by the model id first, then the provider name — so a Claude served through Azure or OpenRouter still reads as Anthropic. Wired into BuiltInLanguageModelProvider; unknown models fall back to the neutral sparkle. Adds a unit test and a What's New entry. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../BuiltInLanguageModelProvider.cs | 8 +- src/MeshWeaver.AI/ModelProviderIcons.cs | 92 +++++++++++++++++++ .../2026-07-10-model-provider-icons.md | 12 +++ src/MeshWeaver.Graph/Icons/anthropic.svg | 1 + src/MeshWeaver.Graph/Icons/azure.svg | 1 + src/MeshWeaver.Graph/Icons/deepseek.svg | 1 + src/MeshWeaver.Graph/Icons/githubcopilot.svg | 1 + src/MeshWeaver.Graph/Icons/google.svg | 1 + src/MeshWeaver.Graph/Icons/meta.svg | 1 + src/MeshWeaver.Graph/Icons/mistral.svg | 1 + src/MeshWeaver.Graph/Icons/ollama.svg | 1 + src/MeshWeaver.Graph/Icons/openai.svg | 1 + src/MeshWeaver.Graph/Icons/openrouter.svg | 1 + src/MeshWeaver.Graph/Icons/perplexity.svg | 1 + src/MeshWeaver.Graph/Icons/qwen.svg | 1 + src/MeshWeaver.Graph/Icons/xai.svg | 1 + .../ModelProviderIconsTest.cs | 62 +++++++++++++ 17 files changed, 185 insertions(+), 2 deletions(-) create mode 100644 src/MeshWeaver.AI/ModelProviderIcons.cs create mode 100644 src/MeshWeaver.Documentation/Data/WhatsNew/2026-07-10-model-provider-icons.md create mode 100644 src/MeshWeaver.Graph/Icons/anthropic.svg create mode 100644 src/MeshWeaver.Graph/Icons/azure.svg create mode 100644 src/MeshWeaver.Graph/Icons/deepseek.svg create mode 100644 src/MeshWeaver.Graph/Icons/githubcopilot.svg create mode 100644 src/MeshWeaver.Graph/Icons/google.svg create mode 100644 src/MeshWeaver.Graph/Icons/meta.svg create mode 100644 src/MeshWeaver.Graph/Icons/mistral.svg create mode 100644 src/MeshWeaver.Graph/Icons/ollama.svg create mode 100644 src/MeshWeaver.Graph/Icons/openai.svg create mode 100644 src/MeshWeaver.Graph/Icons/openrouter.svg create mode 100644 src/MeshWeaver.Graph/Icons/perplexity.svg create mode 100644 src/MeshWeaver.Graph/Icons/qwen.svg create mode 100644 src/MeshWeaver.Graph/Icons/xai.svg create mode 100644 test/MeshWeaver.AI.Test/ModelProviderIconsTest.cs diff --git a/src/MeshWeaver.AI/BuiltInLanguageModelProvider.cs b/src/MeshWeaver.AI/BuiltInLanguageModelProvider.cs index a675582277..ad1fddd307 100644 --- a/src/MeshWeaver.AI/BuiltInLanguageModelProvider.cs +++ b/src/MeshWeaver.AI/BuiltInLanguageModelProvider.cs @@ -146,7 +146,9 @@ public IEnumerable GetStaticNodes() NodeType = ModelProviderNodeType.NodeType, Name = source.ProviderName, Category = "Providers", - Icon = "Key", + // Brand logo when the provider name resolves to a known maker; the + // generic key otherwise. See ModelProviderIcons. + Icon = ModelProviderIcons.ForProvider(source.ProviderName) ?? "Key", // create-if-absent: importer seeds it once, then admin owns it. SyncBehavior = SyncBehavior.ExcludeThisAndChildren, Content = providerConfig @@ -194,7 +196,9 @@ public IEnumerable GetStaticNodes() NodeType = LanguageModelNodeType.NodeType, Name = modelId, Category = "Models", - Icon = "Sparkle", + // Brand logo inferred from the model id (then provider) so a model reads + // as its maker; the generic sparkle when unknown. See ModelProviderIcons. + Icon = ModelProviderIcons.ForModel(source.ProviderName, modelId) ?? "Sparkle", Content = def }); } diff --git a/src/MeshWeaver.AI/ModelProviderIcons.cs b/src/MeshWeaver.AI/ModelProviderIcons.cs new file mode 100644 index 0000000000..c96e61f7d9 --- /dev/null +++ b/src/MeshWeaver.AI/ModelProviderIcons.cs @@ -0,0 +1,92 @@ +using System.Collections.Immutable; + +namespace MeshWeaver.AI; + +/// +/// Resolves a brand icon for an AI model or model provider so the picker shows a +/// recognizable logo instead of the generic sparkle. Icons are the self-contained +/// coloured-tile SVGs shipped under src/MeshWeaver.Graph/Icons (served at +/// /static/NodeTypeIcons/*.svg). +/// +/// The brand is inferred by substring match against the MODEL ID first, then +/// the provider name — so a gateway provider (AzureFoundry, OpenRouter) that serves +/// many brands still resolves each model to its true maker (a claude-* served +/// through Azure gets the Anthropic mark). An unknown model/provider returns +/// null so the caller can fall back to the neutral sparkle. +/// +public static class ModelProviderIcons +{ + private const string Base = "/static/NodeTypeIcons/"; + + /// + /// Brand tokens, checked in order via against a + /// lower-cased id/name. Order matters only where several tokens could match the same + /// string: the first match wins, so brand-specific tokens (mixtral, codestral, + /// llama) precede broader ones (mistral, meta). Each icon file exists + /// under MeshWeaver.Graph/Icons. + /// + private static readonly ImmutableArray<(string Token, string Icon)> Brands = + [ + ("claude", "anthropic"), + ("anthropic", "anthropic"), + ("gemini", "google"), + ("gemma", "google"), + ("deepseek", "deepseek"), + ("mixtral", "mistral"), + ("codestral", "mistral"), + ("ministral", "mistral"), + ("magistral", "mistral"), + ("devstral", "mistral"), + ("mistral", "mistral"), + ("llama", "meta"), + ("grok", "xai"), + ("qwen", "qwen"), + ("qwq", "qwen"), + ("copilot", "githubcopilot"), + ("openrouter", "openrouter"), + ("perplexity", "perplexity"), + ("sonar", "perplexity"), + ("ollama", "ollama"), + ("gpt", "openai"), + ("chatgpt", "openai"), + ("davinci", "openai"), + ("dall-e", "openai"), + ("openai", "openai"), + ("meta", "meta"), + ("xai", "xai"), + ("azure", "azure"), + ("microsoft", "azure"), + ]; + + /// + /// Resolves the icon path for a model, preferring the brand implied by + /// and falling back to . + /// Returns null when no brand matches. + /// + public static string? ForModel(string? provider, string? modelId) + { + var icon = Match(modelId) ?? Match(provider); + return icon is null ? null : Base + icon + ".svg"; + } + + /// + /// Resolves the icon path for a provider by its name. Returns null when no + /// brand matches. + /// + public static string? ForProvider(string? provider) + { + var icon = Match(provider); + return icon is null ? null : Base + icon + ".svg"; + } + + private static string? Match(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return null; + var v = value.ToLowerInvariant(); + foreach (var (token, icon) in Brands) + if (v.Contains(token)) + return icon; + return null; + } +} diff --git a/src/MeshWeaver.Documentation/Data/WhatsNew/2026-07-10-model-provider-icons.md b/src/MeshWeaver.Documentation/Data/WhatsNew/2026-07-10-model-provider-icons.md new file mode 100644 index 0000000000..b11cc0da2b --- /dev/null +++ b/src/MeshWeaver.Documentation/Data/WhatsNew/2026-07-10-model-provider-icons.md @@ -0,0 +1,12 @@ +--- +Name: AI models now show their provider's logo +Category: What's New +Description: The model picker displays each model's brand mark instead of a generic sparkle. +Icon: Sparkle +--- + +# AI models now show their provider's logo + +Picking a model is easier to scan: every model now carries its maker's logo — Anthropic, OpenAI, Google, Mistral, Meta, DeepSeek, xAI, Qwen, Perplexity, GitHub Copilot and more — in place of the identical sparkle every model used to show. + +The logo is inferred from the model itself, so a Claude model served through Azure or an OpenRouter gateway still reads as Anthropic. Models from a provider we don't yet recognise keep the neutral sparkle. diff --git a/src/MeshWeaver.Graph/Icons/anthropic.svg b/src/MeshWeaver.Graph/Icons/anthropic.svg new file mode 100644 index 0000000000..b49d5a3a6e --- /dev/null +++ b/src/MeshWeaver.Graph/Icons/anthropic.svg @@ -0,0 +1 @@ + diff --git a/src/MeshWeaver.Graph/Icons/azure.svg b/src/MeshWeaver.Graph/Icons/azure.svg new file mode 100644 index 0000000000..ae4f49cc08 --- /dev/null +++ b/src/MeshWeaver.Graph/Icons/azure.svg @@ -0,0 +1 @@ + diff --git a/src/MeshWeaver.Graph/Icons/deepseek.svg b/src/MeshWeaver.Graph/Icons/deepseek.svg new file mode 100644 index 0000000000..e7a294b341 --- /dev/null +++ b/src/MeshWeaver.Graph/Icons/deepseek.svg @@ -0,0 +1 @@ + diff --git a/src/MeshWeaver.Graph/Icons/githubcopilot.svg b/src/MeshWeaver.Graph/Icons/githubcopilot.svg new file mode 100644 index 0000000000..587845c774 --- /dev/null +++ b/src/MeshWeaver.Graph/Icons/githubcopilot.svg @@ -0,0 +1 @@ + diff --git a/src/MeshWeaver.Graph/Icons/google.svg b/src/MeshWeaver.Graph/Icons/google.svg new file mode 100644 index 0000000000..6ce13202f7 --- /dev/null +++ b/src/MeshWeaver.Graph/Icons/google.svg @@ -0,0 +1 @@ + diff --git a/src/MeshWeaver.Graph/Icons/meta.svg b/src/MeshWeaver.Graph/Icons/meta.svg new file mode 100644 index 0000000000..b30999ed81 --- /dev/null +++ b/src/MeshWeaver.Graph/Icons/meta.svg @@ -0,0 +1 @@ + diff --git a/src/MeshWeaver.Graph/Icons/mistral.svg b/src/MeshWeaver.Graph/Icons/mistral.svg new file mode 100644 index 0000000000..354111e5f6 --- /dev/null +++ b/src/MeshWeaver.Graph/Icons/mistral.svg @@ -0,0 +1 @@ + diff --git a/src/MeshWeaver.Graph/Icons/ollama.svg b/src/MeshWeaver.Graph/Icons/ollama.svg new file mode 100644 index 0000000000..4c201e0949 --- /dev/null +++ b/src/MeshWeaver.Graph/Icons/ollama.svg @@ -0,0 +1 @@ + diff --git a/src/MeshWeaver.Graph/Icons/openai.svg b/src/MeshWeaver.Graph/Icons/openai.svg new file mode 100644 index 0000000000..e608185152 --- /dev/null +++ b/src/MeshWeaver.Graph/Icons/openai.svg @@ -0,0 +1 @@ + diff --git a/src/MeshWeaver.Graph/Icons/openrouter.svg b/src/MeshWeaver.Graph/Icons/openrouter.svg new file mode 100644 index 0000000000..8138bcadca --- /dev/null +++ b/src/MeshWeaver.Graph/Icons/openrouter.svg @@ -0,0 +1 @@ + diff --git a/src/MeshWeaver.Graph/Icons/perplexity.svg b/src/MeshWeaver.Graph/Icons/perplexity.svg new file mode 100644 index 0000000000..bc450e6fb0 --- /dev/null +++ b/src/MeshWeaver.Graph/Icons/perplexity.svg @@ -0,0 +1 @@ + diff --git a/src/MeshWeaver.Graph/Icons/qwen.svg b/src/MeshWeaver.Graph/Icons/qwen.svg new file mode 100644 index 0000000000..59a632e499 --- /dev/null +++ b/src/MeshWeaver.Graph/Icons/qwen.svg @@ -0,0 +1 @@ + diff --git a/src/MeshWeaver.Graph/Icons/xai.svg b/src/MeshWeaver.Graph/Icons/xai.svg new file mode 100644 index 0000000000..823716b12f --- /dev/null +++ b/src/MeshWeaver.Graph/Icons/xai.svg @@ -0,0 +1 @@ + diff --git a/test/MeshWeaver.AI.Test/ModelProviderIconsTest.cs b/test/MeshWeaver.AI.Test/ModelProviderIconsTest.cs new file mode 100644 index 0000000000..830be65938 --- /dev/null +++ b/test/MeshWeaver.AI.Test/ModelProviderIconsTest.cs @@ -0,0 +1,62 @@ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +// Assertions (.Should()) come from the in-house MeshWeaver.Reactive.Assertions, +// wired as a global using in test/Directory.Build.props — no per-file import. +using Xunit; + +namespace MeshWeaver.AI.Test; + +/// +/// Pins : every AI model resolves to its maker's brand logo +/// (so the picker no longer shows an identical sparkle for every model), the model id wins over +/// a gateway provider name (a claude-* served through AzureFoundry still reads as Anthropic), +/// and an unknown model/provider returns null so the caller falls back to the neutral icon. +/// +public class ModelProviderIconsTest +{ + [Theory] + [InlineData("claude-sonnet-4-20250514", "Anthropic", "anthropic")] + [InlineData("gpt-4o-mini", "OpenAI", "openai")] + [InlineData("chatgpt-4o-latest", "OpenAI", "openai")] + [InlineData("gemini-2.5-pro", "Google", "google")] + [InlineData("gemma-3-27b", "Google", "google")] + [InlineData("mistral-large-latest", "Mistral", "mistral")] + [InlineData("mixtral-8x7b", "Mistral", "mistral")] + [InlineData("codestral-latest", "Mistral", "mistral")] + [InlineData("llama-3.3-70b", "Meta", "meta")] + [InlineData("deepseek-chat", "DeepSeek", "deepseek")] + [InlineData("grok-4", "xAI", "xai")] + [InlineData("qwen-2.5-72b", "Qwen", "qwen")] + [InlineData("sonar-pro", "Perplexity", "perplexity")] + public void ModelId_resolves_to_its_brand(string modelId, string provider, string expectedIcon) + => ModelProviderIcons.ForModel(provider, modelId) + .Should().Be($"/static/NodeTypeIcons/{expectedIcon}.svg"); + + [Fact] + public void ModelId_wins_over_a_gateway_provider_name() + // AzureFoundry / OpenRouter serve many makers — the model id must decide the logo. + => ModelProviderIcons.ForModel("AzureFoundry", "claude-sonnet-4-20250514") + .Should().Be("/static/NodeTypeIcons/anthropic.svg"); + + [Fact] + public void OpenRouter_prefixed_ids_resolve_to_the_maker() + => ModelProviderIcons.ForModel("OpenRouter", "meta-llama/llama-3.3-70b-instruct") + .Should().Be("/static/NodeTypeIcons/meta.svg"); + + [Theory] + [InlineData("Anthropic", "anthropic")] + [InlineData("OpenAI", "openai")] + [InlineData("AzureOpenAI", "openai")] // OpenAI models hosted on Azure → the OpenAI mark + [InlineData("AzureFoundry", "azure")] // generic Azure gateway → the Azure mark + [InlineData("GitHubCopilot", "githubcopilot")] + public void Provider_name_resolves_when_no_model_id(string provider, string expectedIcon) + => ModelProviderIcons.ForProvider(provider) + .Should().Be($"/static/NodeTypeIcons/{expectedIcon}.svg"); + + [Theory] + [InlineData(null, null)] + [InlineData("", null)] + [InlineData("SomeCustomProvider", "totally-unknown-model")] + public void Unknown_returns_null_for_neutral_fallback(string? provider, string? modelId) + => ModelProviderIcons.ForModel(provider, modelId).Should().BeNull(); +} From 7044244d8b8c172016c692a04163d4079bde156d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20B=C3=BCrgi?= Date: Fri, 10 Jul 2026 22:55:29 +0200 Subject: [PATCH 2/2] Fall back to self-contained SVG URLs, not legacy Fluent icon names Addresses Copilot review: MeshNodeImageHelper filters out legacy Fluent icon names (Key/Sparkle), so fall back to the /static/NodeTypeIcons/*.svg URL directly, keeping node.Icon always renderable. No rendered change (the legacy names already resolved to those same SVGs via the NodeType default). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/MeshWeaver.AI/BuiltInLanguageModelProvider.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/MeshWeaver.AI/BuiltInLanguageModelProvider.cs b/src/MeshWeaver.AI/BuiltInLanguageModelProvider.cs index ad1fddd307..dafac540ee 100644 --- a/src/MeshWeaver.AI/BuiltInLanguageModelProvider.cs +++ b/src/MeshWeaver.AI/BuiltInLanguageModelProvider.cs @@ -147,8 +147,10 @@ public IEnumerable GetStaticNodes() Name = source.ProviderName, Category = "Providers", // Brand logo when the provider name resolves to a known maker; the - // generic key otherwise. See ModelProviderIcons. - Icon = ModelProviderIcons.ForProvider(source.ProviderName) ?? "Key", + // generic key SVG otherwise. Fall back to the self-contained SVG URL + // (not the legacy "Key" Fluent name, which MeshNodeImageHelper filters + // out) so node.Icon is always directly renderable. See ModelProviderIcons. + Icon = ModelProviderIcons.ForProvider(source.ProviderName) ?? "/static/NodeTypeIcons/key.svg", // create-if-absent: importer seeds it once, then admin owns it. SyncBehavior = SyncBehavior.ExcludeThisAndChildren, Content = providerConfig @@ -197,8 +199,10 @@ public IEnumerable GetStaticNodes() Name = modelId, Category = "Models", // Brand logo inferred from the model id (then provider) so a model reads - // as its maker; the generic sparkle when unknown. See ModelProviderIcons. - Icon = ModelProviderIcons.ForModel(source.ProviderName, modelId) ?? "Sparkle", + // as its maker; the generic sparkle SVG when unknown. Fall back to the + // self-contained SVG URL (not the legacy "Sparkle" Fluent name, which + // MeshNodeImageHelper filters out). See ModelProviderIcons. + Icon = ModelProviderIcons.ForModel(source.ProviderName, modelId) ?? "/static/NodeTypeIcons/sparkle.svg", Content = def }); }