mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-21 06:12:17 +03:00
merge #11750
This commit is contained in:
@@ -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;
|
||||
})();
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user