From bae92b58b4e5d913fc35b05dade5254bb5302964 Mon Sep 17 00:00:00 2001 From: ricatix Date: Mon, 15 Jun 2026 14:28:56 +0700 Subject: [PATCH 1/3] test(provider): cover provider icon fallback resolver Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/shared/helpers/providerIconInfo.js | 38 +++++++++++++ tests/unit/provider-icon-info.test.js | 78 ++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 src/shared/helpers/providerIconInfo.js create mode 100644 tests/unit/provider-icon-info.test.js diff --git a/src/shared/helpers/providerIconInfo.js b/src/shared/helpers/providerIconInfo.js new file mode 100644 index 0000000000..c59fdd8553 --- /dev/null +++ b/src/shared/helpers/providerIconInfo.js @@ -0,0 +1,38 @@ +import { getProviderByAlias, isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers" + +function getInitials(name, fallbackId) { + if (name && name.trim() !== "") { + return name.slice(0, 2).toUpperCase() + } + return fallbackId.slice(0, 2).toUpperCase() +} + +export function resolveProviderIconInfo(providerId, displayName) { + const entry = getProviderByAlias(providerId) + + if (entry) { + return { + fallbackText: entry.textIcon || entry.alias.slice(0, 2).toUpperCase(), + fallbackColor: entry.color, + } + } + + if (isOpenAICompatibleProvider(providerId)) { + return { + fallbackText: getInitials(displayName, providerId), + fallbackColor: "#10A37F", + } + } + + if (isAnthropicCompatibleProvider(providerId)) { + return { + fallbackText: getInitials(displayName, providerId), + fallbackColor: "#D97757", + } + } + + return { + fallbackText: getInitials(displayName, providerId), + fallbackColor: "#6b7280", + } +} diff --git a/tests/unit/provider-icon-info.test.js b/tests/unit/provider-icon-info.test.js new file mode 100644 index 0000000000..86f5dbf525 --- /dev/null +++ b/tests/unit/provider-icon-info.test.js @@ -0,0 +1,78 @@ +import { describe, it, expect } from "vitest" + +import { resolveProviderIconInfo } from "../../src/shared/helpers/providerIconInfo.js" + +describe("resolveProviderIconInfo", () => { + it("returns icon info for built-in provider kr (Kiro)", () => { + const result = resolveProviderIconInfo("kr") + + expect(result).toEqual({ + fallbackText: "KR", + fallbackColor: "#FF6B35", + }) + }) + + it("returns icon info for built-in provider glm", () => { + const result = resolveProviderIconInfo("glm") + + expect(result).toEqual({ + fallbackText: "GL", + fallbackColor: "#2563EB", + }) + }) + + it("returns OpenAI-compatible chat provider icon with displayName uppercase slice", () => { + const result = resolveProviderIconInfo("openai-compatible-chat-abc123", "Sumopod") + + expect(result).toEqual({ + fallbackText: "SU", + fallbackColor: "#10A37F", + }) + }) + + it("returns OpenAI-compatible responses provider icon with displayName uppercase slice", () => { + const result = resolveProviderIconInfo("openai-compatible-responses-xyz", "MyServer") + + expect(result).toEqual({ + fallbackText: "MY", + fallbackColor: "#10A37F", + }) + }) + + it("returns Anthropic-compatible provider icon with displayName uppercase slice", () => { + const result = resolveProviderIconInfo("anthropic-compatible-xyz", "ClaudeProxy") + + expect(result).toEqual({ + fallbackText: "CL", + fallbackColor: "#D97757", + }) + }) + + it("falls back to provider ID slice when displayName is empty", () => { + const result = resolveProviderIconInfo("openai-compatible-chat-abc", "") + + expect(result).toEqual({ + fallbackText: "OP", + fallbackColor: "#10A37F", + }) + }) + + it("does not throw when displayName is null, returns non-empty fallbackText", () => { + expect(() => resolveProviderIconInfo("anthropic-compatible-xyz", null)).not.toThrow() + + const result = resolveProviderIconInfo("anthropic-compatible-xyz", null) + + expect(result.fallbackText).toBeDefined() + expect(result.fallbackText.length).toBeGreaterThan(0) + expect(result.fallbackColor).toBeDefined() + }) + + it("returns generic icon for unknown provider", () => { + const result = resolveProviderIconInfo("unknown-xyz") + + expect(result).toEqual({ + fallbackText: "UN", + fallbackColor: "#6b7280", + }) + }) +}) From 70cd42badfa4f0bc1291f5a157e065e458495ff9 Mon Sep 17 00:00:00 2001 From: ricatix Date: Mon, 15 Jun 2026 14:29:04 +0700 Subject: [PATCH 2/3] fix(providers): resolve compatible provider icon fallbacks Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../dashboard/providers/[id]/page.js | 24 ++++++----- .../(dashboard)/dashboard/providers/page.js | 41 ++++++++++--------- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.js b/src/app/(dashboard)/dashboard/providers/[id]/page.js index fd9defc5ed..b910b55013 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.js +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.js @@ -10,6 +10,7 @@ import { getModelsByProviderId } from "@/shared/constants/models"; import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; import { translate } from "@/i18n/runtime"; import { fetchSuggestedModels } from "@/shared/utils/providerModelsFetcher"; +import { resolveProviderIconInfo } from "@/shared/helpers/providerIconInfo"; import ModelRow from "./ModelRow"; import PassthroughModelsSection from "./PassthroughModelsSection"; import CompatibleModelsSection from "./CompatibleModelsSection"; @@ -116,15 +117,18 @@ export default function ProviderDetailPage() { }; const providerInfo = providerNode - ? { - id: providerNode.id, - name: providerNode.name || (providerNode.type === "anthropic-compatible" ? "Anthropic Compatible" : "OpenAI Compatible"), - color: providerNode.type === "anthropic-compatible" ? "#D97757" : "#10A37F", - textIcon: providerNode.type === "anthropic-compatible" ? "AC" : "OC", - apiType: providerNode.apiType, - baseUrl: providerNode.baseUrl, - type: providerNode.type, - } + ? (() => { + const iconInfo = resolveProviderIconInfo(providerNode.id, providerNode.name) + return { + id: providerNode.id, + name: providerNode.name || (providerNode.type === "anthropic-compatible" ? "Anthropic Compatible" : "OpenAI Compatible"), + textIcon: iconInfo.fallbackText, + color: iconInfo.fallbackColor, + apiType: providerNode.apiType, + baseUrl: providerNode.baseUrl, + type: providerNode.type, + } + })() : (OAUTH_PROVIDERS[providerId] || APIKEY_PROVIDERS[providerId] || FREE_PROVIDERS[providerId] || FREE_TIER_PROVIDERS[providerId] || WEB_COOKIE_PROVIDERS[providerId]); const authModes = providerInfo?.authModes || []; const isOAuth = !!OAUTH_PROVIDERS[providerId] || !!FREE_PROVIDERS[providerId] || authModes.includes("oauth"); @@ -1107,7 +1111,7 @@ export default function ProviderDetailPage() { > {headerImgError ? ( - {providerInfo.textIcon || providerInfo.id.slice(0, 2).toUpperCase()} + {providerInfo.textIcon} ) : ( node.type === "openai-compatible") - .map((node) => ({ - id: node.id, - name: node.name || "OpenAI Compatible", - color: "#10A37F", - textIcon: "OC", - apiType: node.apiType, - })) + .map((node) => { + const info = resolveProviderIconInfo(node.id, node.name); + return { + id: node.id, + name: node.name || "OpenAI Compatible", + color: info.fallbackColor, + textIcon: info.fallbackText, + apiType: node.apiType, + }; + }) .filter((p) => matchSearch(p.name)); const anthropicCompatibleProviders = providerNodes .filter((node) => node.type === "anthropic-compatible") - .map((node) => ({ - id: node.id, - name: node.name || "Anthropic Compatible", - color: "#D97757", - textIcon: "AC", - })) + .map((node) => { + const info = resolveProviderIconInfo(node.id, node.name); + return { + id: node.id, + name: node.name || "Anthropic Compatible", + color: info.fallbackColor, + textIcon: info.fallbackText, + }; + }) .filter((p) => matchSearch(p.name)); const oauthEntries = Object.entries(OAUTH_PROVIDERS).filter( @@ -628,9 +635,7 @@ function ProviderCard({ providerId, provider, stats, authType, onToggle }) { alt={provider.name} size={30} className="object-contain rounded-lg max-w-[32px] max-h-[32px]" - fallbackText={ - provider.textIcon || provider.id.slice(0, 2).toUpperCase() - } + fallbackText={provider.textIcon} fallbackColor={provider.color} /> @@ -756,9 +761,7 @@ function ApiKeyProviderCard({ alt={provider.name} size={30} className="object-contain rounded-lg max-w-[30px] max-h-[30px]" - fallbackText={ - provider.textIcon || provider.id.slice(0, 2).toUpperCase() - } + fallbackText={provider.textIcon} fallbackColor={provider.color} /> From ae32dcfebeb6cd9187e87dd8c78c3e4be3c6b082 Mon Sep 17 00:00:00 2001 From: ricatix Date: Mon, 15 Jun 2026 14:29:14 +0700 Subject: [PATCH 3/3] fix(dashboard): use provider icon resolver in model UI Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../dashboard/media-providers/combo/[id]/page.js | 5 +++-- .../dashboard/usage/components/ProviderTopology.js | 6 ++++-- src/shared/components/ModelSelectModal.js | 5 +++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/app/(dashboard)/dashboard/media-providers/combo/[id]/page.js b/src/app/(dashboard)/dashboard/media-providers/combo/[id]/page.js index 45c1546025..1de397e1fa 100644 --- a/src/app/(dashboard)/dashboard/media-providers/combo/[id]/page.js +++ b/src/app/(dashboard)/dashboard/media-providers/combo/[id]/page.js @@ -6,6 +6,7 @@ import Link from "next/link"; import { Card, Button, Input, Toggle, ModelSelectModal } from "@/shared/components"; import ProviderIcon from "@/shared/components/ProviderIcon"; import { AI_PROVIDERS, MEDIA_PROVIDER_KINDS } from "@/shared/constants/providers"; +import { resolveProviderIconInfo } from "@/shared/helpers/providerIconInfo"; // Parse "providerId/model" or just "providerId" → { providerId, model } function parseModelEntry(entry) { @@ -304,8 +305,8 @@ export default function ComboDetailPage() { alt={p?.name || providerId} size={24} className="object-contain rounded shrink-0" - fallbackText={p?.textIcon || providerId.slice(0, 2).toUpperCase()} - fallbackColor={p?.color} + fallbackText={resolveProviderIconInfo(providerId).fallbackText} + fallbackColor={resolveProviderIconInfo(providerId).fallbackColor} />
{p?.name || providerId}
diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderTopology.js b/src/app/(dashboard)/dashboard/usage/components/ProviderTopology.js index 3827a0f854..84337ab921 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderTopology.js +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderTopology.js @@ -10,6 +10,7 @@ import { } from "@xyflow/react"; import "@xyflow/react/dist/style.css"; import { AI_PROVIDERS } from "@/shared/constants/providers"; +import { resolveProviderIconInfo } from "@/shared/helpers/providerIconInfo"; // Force-stop FE animation if a provider stays active longer than this const FE_ACTIVE_TIMEOUT_MS = 60000; @@ -144,15 +145,16 @@ function buildLayout(providers, activeSet, lastSet, errorSet) { providers.forEach((p, i) => { const config = getProviderConfig(p.provider); + const iconInfo = resolveProviderIconInfo(p.provider, p.nodeName || p.name); const active = activeSet.has(p.provider?.toLowerCase()); const last = !active && lastSet.has(p.provider?.toLowerCase()); const error = !active && errorSet.has(p.provider?.toLowerCase()); const nodeId = `provider-${p.provider}`; const data = { label: (config.name !== p.provider ? config.name : null) || p.nodeName || p.name || p.provider, - color: config.color || "#6b7280", + color: iconInfo.fallbackColor, imageUrl: getProviderImageUrl(p.provider), - textIcon: config.textIcon || (p.provider || "?").slice(0, 2).toUpperCase(), + textIcon: iconInfo.fallbackText, active, }; diff --git a/src/shared/components/ModelSelectModal.js b/src/shared/components/ModelSelectModal.js index 0e965f8d98..883da14b7a 100644 --- a/src/shared/components/ModelSelectModal.js +++ b/src/shared/components/ModelSelectModal.js @@ -6,6 +6,7 @@ import Modal from "./Modal"; import ProviderIcon from "./ProviderIcon"; import { getModelsByProviderId } from "@/shared/constants/models"; import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, FREE_PROVIDERS, FREE_TIER_PROVIDERS, AI_PROVIDERS, isOpenAICompatibleProvider, isAnthropicCompatibleProvider, getProviderAlias } from "@/shared/constants/providers"; +import { resolveProviderIconInfo } from "@/shared/helpers/providerIconInfo"; // Provider order: OAuth first, then Free Tier, then API Key (matches dashboard/providers) const PROVIDER_ORDER = [ @@ -447,8 +448,8 @@ export default function ModelSelectModal({ src={`/providers/${providerId}.png`} alt={group.name} size={14} - fallbackText={(group.name || providerId).slice(0, 2).toUpperCase()} - fallbackColor={group.color} + fallbackText={resolveProviderIconInfo(providerId, group.name).fallbackText} + fallbackColor={resolveProviderIconInfo(providerId, group.name).fallbackColor} /> {group.name}