"use client"; /** * ProviderIcon — Renders a provider logo using @lobehub/icons with PNG fallback. * * Strategy (#529): * 1. Try @lobehub/icons ProviderIcon (130+ providers, React components) * 2. Fall back to /providers/{id}.png (existing static assets) * 3. Fall back to /providers/{id}.svg (SVG assets) * 4. Fall back to a generic AI icon * * Usage: * * */ import { memo, useState, Component, type ReactNode } from "react"; import Image from "next/image"; import { ProviderIcon as LobehubProviderIcon } from "@lobehub/icons"; // Mapping from OmniRoute provider IDs → Lobehub icon IDs // Lobehub uses lowercase IDs matching ModelProvider enum values const LOBEHUB_PROVIDER_MAP: Record = { openai: "openai", anthropic: "anthropic", claude: "anthropic", gemini: "google", google: "google", deepseek: "deepseek", groq: "groq", mistral: "mistral", cohere: "cohere", perplexity: "perplexity", xai: "xai", grok: "xai", together: "togetherai", fireworks: "fireworks", "fireworks-ai": "fireworks", cerebras: "cerebras", huggingface: "huggingface", "hugging-face": "huggingface", openrouter: "openrouter", "open-router": "openrouter", ollama: "ollama", minimax: "minimax", qwen: "qwen", alibaba: "qwen", moonshot: "moonshot", kimi: "moonshot", baidu: "baidu", ernie: "baidu", spark: "iflytek", "zhipu-ai": "zhipu", zhipu: "zhipu", lmsys: "lmsys", "stability-ai": "stability", stability: "stability", replicate: "replicate", ai21: "ai21", nvidia: "nvidia", cloudflare: "cloudflare", "cloudflare-ai": "cloudflare", "aws-bedrock": "bedrock", bedrock: "bedrock", azure: "azure", "azure-openai": "azure", copilot: "githubcopilot", "github-copilot": "githubcopilot", mistralai: "mistral", codex: "openai", blackbox: "blackboxai", blackboxai: "blackboxai", pollinations: "pollinations", }; interface ProviderIconProps { providerId: string; size?: number; type?: "mono" | "color"; className?: string; style?: React.CSSProperties; } /** Error boundary to catch Lobehub component render errors gracefully. */ class LobehubErrorBoundary extends Component< { children: ReactNode; onError: () => void }, { hasError: boolean } > { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError() { return { hasError: true }; } componentDidCatch() { this.props.onError(); } render() { if (this.state.hasError) return null; return this.props.children; } } function GenericProviderIcon({ size }: { size: number }) { return ( ); } const KNOWN_PNGS = new Set([ "aimlapi", "alibaba", "alicode-intl", "alicode", "anthropic-m", "anthropic", "antigravity", "bailian-coding-plan", "blackbox", "brave-search", "brave", "cerebras", "claude", "cline", "codex", "cohere", "continue", "copilot", "cursor", "deepgram", "deepseek", "droid", "exa-search", "fireworks", "gemini-cli", "gemini", "github", "glm", "groq", "iflow", "ironclaw", "kilo-gateway", "kilocode", "kimi-coding-apikey", "kimi-coding", "kimi", "kiro", "longcat", "minimax-cn", "minimax", "mistral", "nanobot", "nebius", "nvidia", "oai-cc", "oai-r", "ollama-cloud", "openai", "openclaw", "openrouter", "perplexity-search", "perplexity", "pollinations", "qwen", "roo", "serper-search", "serper", "siliconflow", "tavily-search", "tavily", "together", "xai", "zeroclaw", ]); const KNOWN_SVGS = new Set([ "apikey", "assemblyai", "brave", "cartesia", "cloudflare-ai", "comfyui", "elevenlabs", "exa-search", "exa", "huggingface", "hyperbolic", "inworld", "nanobanana", "oauth", "opencode-go", "opencode-zen", "opencode", "playht", "puter", "scaleway", "sdwebui", "synthetic", "vertex", "windsurf", "zai", ]); const ProviderIcon = memo(function ProviderIcon({ providerId, size = 24, type = "color", className, style, }: ProviderIconProps) { const normalizedId = providerId.toLowerCase(); const lobehubId = LOBEHUB_PROVIDER_MAP[normalizedId] ?? null; const hasPng = KNOWN_PNGS.has(normalizedId); const hasSvg = KNOWN_SVGS.has(normalizedId); const [useLobehub, setUseLobehub] = useState(lobehubId !== null); const [usePng, setUsePng] = useState(hasPng); const [useSvg, setUseSvg] = useState(!hasPng && hasSvg); if (useLobehub && lobehubId) { return ( setUseLobehub(false)}> ); } if (usePng) { return ( {providerId} { setUsePng(false); setUseSvg(hasSvg); }} unoptimized /> ); } if (useSvg) { return ( {providerId} setUseSvg(false)} unoptimized /> ); } return ( ); }); export default ProviderIcon; export type { ProviderIconProps };