This commit is contained in:
Markus Hartung
2026-08-28 04:53:40 -03:00
132 changed files with 1633 additions and 1560 deletions

View File

@@ -160,9 +160,14 @@ function WarningBadge({
lastFailureSuffix,
}
);
const ariaLabel = providerText(t, "warningNotice.ariaLabel", "View connection health details, {count} warning(s)", {
count,
});
const ariaLabel = providerText(
t,
"warningNotice.ariaLabel",
"View connection health details, {count} warning(s)",
{
count,
}
);
const activate = (e: MouseEvent | KeyboardEvent) => {
e.preventDefault();
@@ -431,10 +436,9 @@ const ProviderCard = forwardRef<ProviderCardHandle, ProviderCardProps>(function
};
const staticIconPath = (() => {
if (isCompatible) {
return provider.apiType === "responses" ? "/providers/oai-r.png" : "/providers/oai-cc.png";
if (isCompatible || isAnthropicCompatible || isCcCompatible) {
return "/providers/cli-generic.svg";
}
if (isAnthropicCompatible || isCcCompatible) return "/providers/anthropic-m.png";
return null;
})();

View File

@@ -1,17 +1,18 @@
"use client";
/**
* ProviderIcon — Renders a provider logo prioritizing local SVGs for speed.
* ProviderIcon — Renders a provider icon from a provenance-aware resolution chain.
*
* Strategy (#529):
* 0. If `src` is set (operator-supplied remote icon URL, #2166), render it — this always
* wins over the resolution below. On load error, falls back to
* `fallbackText`/`fallbackColor` (a colored text badge) if provided, otherwise falls
* through to steps 1-6.
* 1. Theme-aware static SVGs (`THEMED_SVGS`, e.g. arena-light/dark for lmarena)
* 2. Try /providers/{id}.svg (local SVG assets — fastest, cached separately from JS bundle)
* 3. Try @lobehub/icons direct React components (no @lobehub/ui peer runtime)
* 4. Try /providers/{id}.png (legacy static assets)
* 1. Render the internal generic icon for provider IDs whose previous local asset has no
* verified source/license provenance. Those IDs must never fall through to an icon CDN.
* 2. Theme-aware static SVGs (`THEMED_SVGS`)
* 3. Try /providers/{id}.svg (local SVG assets — fastest, cached separately from JS bundle)
* 4. Try @lobehub/icons direct React components (no @lobehub/ui peer runtime)
* 5. Fall back to thesvg.org CDN (external SVG)
* 6. Fall back to a generic AI icon
*
@@ -22,7 +23,6 @@
*/
import { createElement, memo, useState } from "react";
import Image from "next/image";
import { useTheme } from "@/shared/hooks/useTheme";
@@ -48,7 +48,14 @@ interface ProviderIconProps {
function GenericProviderIcon({ size }: { size: number }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" style={{ flex: "none" }}>
<svg
data-provider-icon="generic"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
style={{ flex: "none" }}
>
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="1.5" opacity="0.4" />
<path d="M8 12h8M12 8v8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
</svg>
@@ -59,8 +66,6 @@ const KNOWN_SVGS = new Set([
"360ai",
"alibaba",
"anthropic",
"api-airforce",
"apikey",
"arcee",
"arcee-ai",
"assemblyai",
@@ -72,19 +77,11 @@ const KNOWN_SVGS = new Set([
"baidu",
"bailian",
"baseten",
"bazaarlink",
"bluesminds",
"brave",
"brave-search",
"byteplus",
"bytez",
"cartesia",
"cerebras",
"charm-hyper",
"cheaperinference",
"chipotle",
"chutes",
"clarifai",
"claude",
"claude-web",
"cline",
@@ -92,7 +89,6 @@ const KNOWN_SVGS = new Set([
"codex",
"cohere",
"comfyui",
"command-code",
"continue",
"copilot",
"coze",
@@ -103,50 +99,37 @@ const KNOWN_SVGS = new Set([
"deepseek",
"dgrid",
"dify",
"digitalocean",
"dit",
"docker-model-runner",
"doubao",
"droid",
"duckduckgo-web",
"elevenlabs",
"exa",
"factory",
"fal",
"fireworks",
"freeaiapikey",
"freebuff",
"freemodel-dev",
"friendli",
"galadriel",
"gemini",
"gitlab",
"gitlab-duo",
"gitlawb",
"gitlawb-gmi",
"google",
"grok",
"groq",
"haiper",
"hcnsec",
"heroku",
"huggingchat",
"huggingface",
"hyperbolic",
"ibm",
"ideogram",
"iflytek",
"inclusionai",
"inference",
"inworld",
"kenari",
"kilo-gateway",
"kilocode",
"kimi",
"kiro",
"krutrim",
"lambda",
"leonardo",
"liquid",
"llm7",
"longcat",
@@ -154,8 +137,6 @@ const KNOWN_SVGS = new Set([
"metaai",
"minimax",
"mistral",
"modal",
"modelscope",
"monsterapi",
"moonshot",
"morph",
@@ -166,14 +147,10 @@ const KNOWN_SVGS = new Set([
"novita",
"nube",
"nvidia",
"oauth",
"oci",
"ollama",
"openadapter",
"openai",
"openclaw",
"openference",
"opencode",
"openrouter",
"orcarouter",
"ovhcloud",
@@ -181,44 +158,32 @@ const KNOWN_SVGS = new Set([
"phind",
"picoclaw",
"pioneer",
"playht",
"poe",
"pollinations",
"poolside",
"publicai",
"qianfan",
"qiniu",
"qwen",
"qwencloud",
"recraft",
"replicate",
"requesty",
"roocode",
"runway",
"sambanova",
"sap",
"scaleway",
"searchapi",
"searxng-search",
"sensenova",
"serper-search",
"snowflake",
"soniox",
"sparkdesk",
"stepfun",
"sumopod",
"suno",
"synthetic",
"t3-web",
"tavily",
"tencent",
"theoldllm",
"tokenrouter",
"topazlabs",
"trae",
"udio",
"uncloseai",
"unorouter",
"upstage",
"v0",
"veoaifree-web",
@@ -227,12 +192,10 @@ const KNOWN_SVGS = new Set([
"volcengine",
"voyage",
"wafer",
"wandb",
"x5lab",
"xai",
"xinference",
"yi",
"youcom-search",
"yuanbao-web",
"zed-hosted",
"zenmux",
@@ -246,14 +209,64 @@ const LOCAL_SVG_ALIASES: Record<string, string> = {
"qwen-cloud-token-plan": "qwencloud",
};
const KNOWN_PNGS = new Set([
// These provider IDs remain fully available, but their former bundled logos are not shipped
// until source, license, and trademark provenance can be demonstrated. Keep this guard before
// every local/npm/CDN logo tier: silently fetching a similarly named third-party image would
// replace one unverified provenance path with another.
const GENERIC_PROVIDER_IDS = new Set([
"api-airforce",
"apikey",
"bazaarlink",
"brave-search",
"brave",
"byteplus",
"cartesia",
"cheaperinference",
"chipotle",
"clarifai",
"command-code",
"digitalocean",
"docker-model-runner",
"droid",
"duckduckgo-web",
"freebuff",
"gitlab-duo",
"gitlab",
"haiper",
"ideogram",
"inworld",
"kilo-gateway",
"kilocode",
"leonardo",
"modal",
"modelscope",
"nlpcloud",
"oauth",
"oci",
"opencode",
"openference",
"playht",
"qianfan",
"qiniu",
"qwencloud",
"sap",
"scaleway",
"searxng-search",
"serper-search",
"soniox",
"synthetic",
"theoldllm",
"unorouter",
"wandb",
"youcom-search",
"adapta-web",
"agentrouter",
"aimlapi",
"anthropic-m",
"blackbox",
"blackbox-web",
"blackbox",
"cliproxyapi",
"dahl",
"empower",
"gigachat",
"inner-ai",
@@ -273,28 +286,20 @@ const KNOWN_PNGS = new Set([
"piapi",
"predibase",
"reka",
"dahl",
"zeroclaw",
"lmarena",
"lma",
]);
const THEMED_SVGS: Record<string, { light: string; dark: string }> = {
// Arena (formerly LMArena) — wire id stays `lmarena`; alias `lma` also accepted.
lmarena: {
light: "/providers/arena-light.svg",
dark: "/providers/arena-dark.svg",
},
lma: {
light: "/providers/arena-light.svg",
dark: "/providers/arena-dark.svg",
},
// Kimi (Moonshot AI) official-partnership logomarks (2026-07): the official
// rounded-square badge in Kimi's brand blue (#1783FF — see KIMI_BRAND_COLOR in
// featuredProviders.ts) for the 3 visible Kimi-family cards. This replaces two
// weaker fallbacks: kimi-coding/kimi-web previously fell through to the
// third-party LobeHub "Kimi" icon (Tier 3, KNOWN_SVGS has no "kimi-coding"/
// third-party LobeHub "Kimi" icon (Tier 4, KNOWN_SVGS has no "kimi-coding"/
// "kimi-web" entry), and moonshot's `/providers/moonshot.svg` uses
// `fill="currentColor"`, which never resolves against the page theme because
// Tier 2 renders it inside a plain `<img>` (no CSS-inheritance path for an
// Tier 3 renders it inside a plain `<img>` (no CSS-inheritance path for an
// externally-referenced SVG document) — it stayed black in dark mode. Asset
// filenames name the BACKGROUND each mark is designed for (Kimi's own naming),
// matching the light/dark pairing used elsewhere in this map.
@@ -332,23 +337,21 @@ const ProviderIcon = memo(function ProviderIcon({
const { isDark } = useTheme();
const normalizedId = PROVIDER_ICON_ALIASES[providerId.toLowerCase()] || providerId.toLowerCase();
const localSvgId = LOCAL_SVG_ALIASES[normalizedId] || normalizedId;
const lobeIcon = getLobeProviderIcon(normalizedId, type);
const usesGenericIcon =
GENERIC_PROVIDER_IDS.has(normalizedId) || GENERIC_PROVIDER_IDS.has(localSvgId);
const themedSvg = THEMED_SVGS[normalizedId];
const hasSvg = KNOWN_SVGS.has(localSvgId);
const hasPng = KNOWN_PNGS.has(normalizedId);
const [failedAssets, setFailedAssets] = useState<Record<string, true>>({});
const [remoteSrcFailed, setRemoteSrcFailed] = useState(false);
const themedKey = `${normalizedId}:themed`;
const svgKey = `${normalizedId}:svg`;
const pngKey = `${normalizedId}:png`;
const theSvgKey = `${normalizedId}:thesvg`;
const trimmedSrc = typeof src === "string" ? src.trim() : "";
const themedFailed = failedAssets[themedKey];
const svgFailed = failedAssets[svgKey];
const theSvgFailed = failedAssets[theSvgKey];
const pngFailed = failedAssets[pngKey];
// #2166: a custom remote icon URL always wins over the resolution chain below.
// It is a plain <img> (not next/image) so operators can point at any host
@@ -394,7 +397,23 @@ const ProviderIcon = memo(function ProviderIcon({
);
}
// Tier 1: Theme-aware local SVGs (e.g. Arena light/dark)
// Provenance guard: an operator-supplied `src` may still win above, but the built-in
// resolution chain for these IDs is deliberately local and generic. Do not retry LobeHub,
// legacy assets, or thesvg.org for them.
if (usesGenericIcon) {
return (
<span
className={className}
style={{ display: "inline-flex", alignItems: "center", ...style }}
>
<GenericProviderIcon size={size} />
</span>
);
}
const lobeIcon = getLobeProviderIcon(normalizedId, type);
// Tier 2: Theme-aware local SVGs
if (themedSvg && !themedFailed) {
const themedSrc = isDark ? themedSvg.dark : themedSvg.light;
return (
@@ -402,7 +421,7 @@ const ProviderIcon = memo(function ProviderIcon({
className={className}
style={{ display: "inline-flex", alignItems: "center", ...style }}
>
{/* eslint-disable-next-line @next/next/no-img-element -- themed local SVG asset; see the Tier 2 comment for why these use a plain <img> */}
{/* eslint-disable-next-line @next/next/no-img-element -- themed local SVG asset; see the Tier 3 comment for why these use a plain <img> */}
<img
src={themedSrc}
alt={providerId}
@@ -422,14 +441,14 @@ const ProviderIcon = memo(function ProviderIcon({
);
}
// Tier 2: Local SVG — fastest, cached separately from the JS bundle.
// Tier 3: Local SVG — fastest, cached separately from the JS bundle.
// Rendered as a plain <img> (not next/image): provider SVGs carry their own
// intrinsic aspect ratio (e.g. opencode.svg is 234×42), and next/image's
// intrinsic aspect ratio (some wordmarks are much wider than tall), and next/image's
// dev-mode check warns whenever the layout size differs from the square
// width/height attributes — a false positive for non-square logos rendered
// at fixed icon sizes. We keep `width/height` attributes for layout reserve
// but let the intrinsic ratio win on both axes (`width/height: "auto"`) so
// wide logos like opencode render at their true aspect ratio instead of
// wide logos render at their true aspect ratio instead of
// being letterboxed into a 1:1 box.
if (hasSvg && !svgFailed) {
return (
@@ -457,7 +476,7 @@ const ProviderIcon = memo(function ProviderIcon({
);
}
// Tier 3: LobeHub npm icons — only when no local SVG (or SVG failed to load)
// Tier 4: LobeHub npm icons — only when no local SVG (or SVG failed to load)
if (lobeIcon) {
return (
<span
@@ -473,26 +492,6 @@ const ProviderIcon = memo(function ProviderIcon({
);
}
// Tier 4: Known local PNG — avoid a failing external request when a bundled asset exists
if (hasPng && !pngFailed) {
return (
<span
className={className}
style={{ display: "inline-flex", alignItems: "center", ...style }}
>
<Image
src={`/providers/${normalizedId}.png`}
alt={providerId}
width={size}
height={size}
style={{ objectFit: "contain" }}
onError={() => setFailedAssets((current) => ({ ...current, [pngKey]: true }))}
unoptimized
/>
</span>
);
}
// Tier 5: thesvg.org CDN — external SVG fallback for unknown providers
if (!theSvgFailed) {
return (

View File

@@ -110,7 +110,7 @@ export const CLI_TOOLS: Record<string, CliCatalogEntry> = {
droid: {
id: "droid",
name: "Factory Droid",
image: "/providers/droid.svg",
image: "/providers/cli-generic.svg",
color: "#00D4FF",
description: "Factory AI Droid — BYOK assistant with configurable endpoint",
docsUrl: "/docs?section=cli-tools&tool=droid",
@@ -184,7 +184,7 @@ export const CLI_TOOLS: Record<string, CliCatalogEntry> = {
kilo: {
id: "kilo",
name: "Kilo Code",
image: "/providers/kilocode.svg",
image: "/providers/cli-generic.svg",
color: "#FF6B6B",
description: "Kilo Code — VS Code AI assistant with custom base URL support",
docsUrl: "/docs?section=cli-tools&tool=kilocode",
@@ -279,8 +279,8 @@ export const CLI_TOOLS: Record<string, CliCatalogEntry> = {
opencode: {
id: "opencode",
name: "OpenCode",
imageLight: "/providers/opencode-light.svg",
imageDark: "/providers/opencode-dark.svg",
imageLight: "/providers/cli-generic.svg",
imageDark: "/providers/cli-generic.svg",
icon: "terminal",
color: "#FF6B35",
description: "OpenCode — AI coding agent CLI by Anomaly (terminal, multi-provider)",
@@ -771,7 +771,7 @@ OPENAI_API_KEY: "{{apiKey}}"`,
omp: {
id: "omp",
name: "Oh My Pi",
image: "/providers/omp.png",
image: "/providers/cli-generic.svg",
color: "#111111",
docsUrl: "https://github.com/can1357/oh-my-pi",
description: "Oh My Pi terminal coding agent via OmniRoute",
@@ -796,7 +796,7 @@ OPENAI_API_KEY: "{{apiKey}}"`,
letta: {
id: "letta",
name: "Letta CLI",
image: "/providers/letta.png",
image: "/providers/cli-generic.svg",
color: "#FF6B35",
description: "Letta CLI — AI agent with persistent memory and tool use",
configType: "custom",