Release v3.8.0 (#2073)

Integrated into release/v3.8.0
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-05-10 00:55:06 -03:00
committed by GitHub
parent 08e18867fd
commit 3d75fb3fae
726 changed files with 68560 additions and 10908 deletions

View File

@@ -37,126 +37,69 @@ function GenericProviderIcon({ size }: { size: number }) {
}
const KNOWN_PNGS = new Set([
"agentrouter",
"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",
"glmt",
"groq",
"ironclaw",
"kilo-gateway",
"kilocode",
"kimi-coding-apikey",
"kimi-coding",
"kimi",
"kiro",
"longcat",
"minimax-cn",
"minimax",
"mistral",
"kie",
"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",
"aws-polly",
"blackbox-web",
"cliproxyapi",
"databricks",
"empower",
"gigachat",
"gitlab-duo",
"gitlab",
"heroku",
"lemonade",
"linkup-search",
"llamafile",
"llamagate",
"maritalk",
"modal",
"nanogpt",
"nscale",
"oci",
"ovhcloud",
"piapi",
"poe",
"predibase",
"qoder",
"recraft",
"reka",
"runwayml",
"triton",
"venice",
"voyage-ai",
"wandb",
"youcom-search",
]);
const KNOWN_SVGS = new Set([
"apikey",
"assemblyai",
"brave",
"brave-search",
"cartesia",
"cloudflare-ai",
"comfyui",
"elevenlabs",
"exa-search",
"exa",
"huggingface",
"hyperbolic",
"clarifai",
"docker-model-runner",
"droid",
"gemini-cli",
"gitlab",
"gitlab-duo",
"inworld",
"nanobanana",
"kiro",
"kilo-gateway",
"kilocode",
"modal",
"nlpcloud",
"oauth",
"opencode-go",
"opencode-zen",
"oci",
"opencode",
"playht",
"puter",
"qianfan",
"sap",
"scaleway",
"sdwebui",
"serper-search",
"searxng-search",
"synthetic",
"vertex",
"windsurf",
"zai",
"wandb",
"youcom-search",
]);
const ProviderIcon = memo(function ProviderIcon({

View File

@@ -11,6 +11,7 @@ interface ToggleProps {
size?: "sm" | "md" | "lg";
className?: string;
title?: string;
ariaLabel?: string;
}
export default function Toggle({
@@ -21,6 +22,8 @@ export default function Toggle({
disabled = false,
size = "md",
className,
title,
ariaLabel,
}: ToggleProps) {
const sizes = {
sm: {
@@ -58,7 +61,8 @@ export default function Toggle({
type="button"
role="switch"
aria-checked={checked}
aria-label={!label ? description || "Toggle" : undefined}
aria-label={ariaLabel || label || description || title || "Toggle"}
title={title}
disabled={disabled}
onClick={handleClick}
className={cn(

View File

@@ -18,6 +18,7 @@ import {
ProviderCostDonut,
ModelOverTimeChart,
ProviderTable,
ServiceTierBreakdown,
ApiKeyFilterDropdown,
CustomRangePicker,
} from "./analytics";
@@ -61,16 +62,15 @@ export default function UsageAnalytics() {
setError(null);
// Update available keys from unfiltered data (only when no filter is active).
// Use apiKeyName as the stable identifier — it is always populated
// for every OmniRoute API key regardless of the downstream provider.
if (selectedApiKeys.length === 0 && data.byApiKey?.length > 0) {
const seen = new Set<string>();
const keys: { id: string; name: string }[] = [];
for (const k of data.byApiKey) {
const id = k.apiKeyId || k.apiKeyName || "unknown";
const name = k.apiKeyName || k.apiKeyId || "unknown";
if (seen.has(name)) continue;
seen.add(name);
keys.push({ id: name, name });
if (seen.has(id)) continue;
seen.add(id);
keys.push({ id, name });
}
setAvailableApiKeys(keys);
}
@@ -311,10 +311,10 @@ export default function UsageAnalytics() {
color: "text-violet-500",
},
{
icon: "swap_horiz",
label: "Fallback Rate",
value: `${Number(s.fallbackRatePct || 0).toFixed(1)}%`,
color: "text-amber-500",
icon: "bolt",
label: "Fast Requests",
value: fmt(s.fastRequests || 0),
color: "text-sky-500",
},
],
},
@@ -331,6 +331,12 @@ export default function UsageAnalytics() {
value: `${providerDiversity.toFixed(1)}%`,
color: "text-sky-500",
},
{
icon: "swap_horiz",
label: "Fallback Rate",
value: `${Number(s.fallbackRatePct || 0).toFixed(1)}%`,
color: "text-amber-500",
},
],
},
]}
@@ -351,6 +357,9 @@ export default function UsageAnalytics() {
<ProviderCostDonut byProvider={analytics?.byProvider} />
</div>
{/* Fast / Standard service tier split */}
<ServiceTierBreakdown byServiceTier={analytics?.byServiceTier} summary={s} />
{/* Model Usage Over Time (stacked area) */}
<ModelOverTimeChart
dailyByModel={analytics?.dailyByModel}

View File

@@ -1081,6 +1081,71 @@ export function ModelTable({ byModel, summary }) {
);
}
export function ServiceTierBreakdown({ byServiceTier, summary }) {
const data = useMemo(() => byServiceTier || [], [byServiceTier]);
const totalRequests = Number(summary?.totalRequests || 0);
const totalCost = Number(summary?.totalCost || 0);
if (!data.length) {
return null;
}
return (
<Card className="overflow-hidden">
<div className="p-4 border-b border-border flex items-center justify-between gap-3">
<h3 className="text-sm font-semibold text-text-muted uppercase tracking-wider">
Service Tier
</h3>
<span className="text-[11px] text-text-muted">Fast / Standard cost split</span>
</div>
<div className="divide-y divide-border">
{data.map((tier) => {
const isFast = tier.serviceTier === "priority";
const requestPct =
totalRequests > 0
? ((Number(tier.requests || 0) / totalRequests) * 100).toFixed(1)
: "0";
const costPct =
totalCost > 0 ? ((Number(tier.cost || 0) / totalCost) * 100).toFixed(1) : "0";
return (
<div key={tier.serviceTier} className="p-4 flex flex-col gap-3">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2">
<span
className={`material-symbols-outlined text-[18px] ${
isFast ? "text-sky-500" : "text-text-muted"
}`}
>
{isFast ? "bolt" : "speed"}
</span>
<div>
<div className="text-sm font-semibold text-text-main">{tier.label}</div>
<div className="text-xs text-text-muted">
{fmtFull(tier.requests)} requests · {fmt(tier.totalTokens)} tokens
</div>
</div>
</div>
<div className="text-right">
<div className="font-mono text-sm font-semibold text-amber-500">
{fmtCost(tier.cost)}
</div>
<div className="text-xs text-text-muted">{costPct}% of cost</div>
</div>
</div>
<div className="h-1.5 rounded-full bg-black/5 dark:bg-white/10 overflow-hidden">
<div
className={`h-full rounded-full ${isFast ? "bg-sky-500" : "bg-text-muted/50"}`}
style={{ width: `${requestPct}%` }}
/>
</div>
</div>
);
})}
</div>
</Card>
);
}
// ── UsageDetail ────────────────────────────────────────────────────────────
export function UsageDetail({ summary }) {

View File

@@ -25,6 +25,7 @@ export {
ProviderCostDonut,
ModelOverTimeChart,
ProviderTable,
ServiceTierBreakdown,
} from "./charts";
export { default as ApiKeyFilterDropdown } from "./ApiKeyFilterDropdown";

View File

@@ -75,6 +75,8 @@ import KimiColorIcon from "@lobehub/icons/es/Kimi/components/Color";
import KimiMonoIcon from "@lobehub/icons/es/Kimi/components/Mono";
import LambdaMonoIcon from "@lobehub/icons/es/Lambda/components/Mono";
import LmStudioMonoIcon from "@lobehub/icons/es/LmStudio/components/Mono";
import LongCatColorIcon from "@lobehub/icons/es/LongCat/components/Color";
import LongCatMonoIcon from "@lobehub/icons/es/LongCat/components/Mono";
import MetaColorIcon from "@lobehub/icons/es/Meta/components/Color";
import MetaMonoIcon from "@lobehub/icons/es/Meta/components/Mono";
import MetaAIColorIcon from "@lobehub/icons/es/MetaAI/components/Color";
@@ -104,12 +106,14 @@ import PerplexityColorIcon from "@lobehub/icons/es/Perplexity/components/Color";
import PerplexityMonoIcon from "@lobehub/icons/es/Perplexity/components/Mono";
import PoeColorIcon from "@lobehub/icons/es/Poe/components/Color";
import PoeMonoIcon from "@lobehub/icons/es/Poe/components/Mono";
import PollinationsMonoIcon from "@lobehub/icons/es/Pollinations/components/Mono";
import QoderColorIcon from "@lobehub/icons/es/Qoder/components/Color";
import QoderMonoIcon from "@lobehub/icons/es/Qoder/components/Mono";
import QwenColorIcon from "@lobehub/icons/es/Qwen/components/Color";
import QwenMonoIcon from "@lobehub/icons/es/Qwen/components/Mono";
import RecraftMonoIcon from "@lobehub/icons/es/Recraft/components/Mono";
import ReplicateMonoIcon from "@lobehub/icons/es/Replicate/components/Mono";
import RooCodeMonoIcon from "@lobehub/icons/es/RooCode/components/Mono";
import RunwayMonoIcon from "@lobehub/icons/es/Runway/components/Mono";
import SambaNovaColorIcon from "@lobehub/icons/es/SambaNova/components/Color";
import SambaNovaMonoIcon from "@lobehub/icons/es/SambaNova/components/Mono";
@@ -139,6 +143,7 @@ import VolcengineColorIcon from "@lobehub/icons/es/Volcengine/components/Color";
import VolcengineMonoIcon from "@lobehub/icons/es/Volcengine/components/Mono";
import VoyageColorIcon from "@lobehub/icons/es/Voyage/components/Color";
import VoyageMonoIcon from "@lobehub/icons/es/Voyage/components/Mono";
import WindsurfMonoIcon from "@lobehub/icons/es/Windsurf/components/Mono";
import WorkersAIColorIcon from "@lobehub/icons/es/WorkersAI/components/Color";
import WorkersAIMonoIcon from "@lobehub/icons/es/WorkersAI/components/Mono";
import XAIMonoIcon from "@lobehub/icons/es/XAI/components/Mono";
@@ -208,6 +213,7 @@ const LOBE_ICON_COMPONENTS = {
Kimi: { mono: KimiMonoIcon, color: KimiColorIcon },
Lambda: { mono: LambdaMonoIcon },
LmStudio: { mono: LmStudioMonoIcon },
LongCat: { mono: LongCatMonoIcon, color: LongCatColorIcon },
Meta: { mono: MetaMonoIcon, color: MetaColorIcon },
MetaAI: { mono: MetaAIMonoIcon, color: MetaAIColorIcon },
Minimax: { mono: MinimaxMonoIcon, color: MinimaxColorIcon },
@@ -226,10 +232,12 @@ const LOBE_ICON_COMPONENTS = {
OpenRouter: { mono: OpenRouterMonoIcon },
Perplexity: { mono: PerplexityMonoIcon, color: PerplexityColorIcon },
Poe: { mono: PoeMonoIcon, color: PoeColorIcon },
Pollinations: { mono: PollinationsMonoIcon },
Qoder: { mono: QoderMonoIcon, color: QoderColorIcon },
Qwen: { mono: QwenMonoIcon, color: QwenColorIcon },
Recraft: { mono: RecraftMonoIcon },
Replicate: { mono: ReplicateMonoIcon },
RooCode: { mono: RooCodeMonoIcon },
Runway: { mono: RunwayMonoIcon },
SambaNova: { mono: SambaNovaMonoIcon, color: SambaNovaColorIcon },
SearchApi: { mono: SearchApiMonoIcon },
@@ -247,6 +255,7 @@ const LOBE_ICON_COMPONENTS = {
Vllm: { mono: VllmMonoIcon, color: VllmColorIcon },
Volcengine: { mono: VolcengineMonoIcon, color: VolcengineColorIcon },
Voyage: { mono: VoyageMonoIcon, color: VoyageColorIcon },
Windsurf: { mono: WindsurfMonoIcon },
WorkersAI: { mono: WorkersAIMonoIcon, color: WorkersAIColorIcon },
XAI: { mono: XAIMonoIcon },
XiaomiMiMo: { mono: XiaomiMiMoMonoIcon },
@@ -275,6 +284,7 @@ const LOBE_PROVIDER_ALIASES = {
bfl: "Bfl",
"black-forest-labs": "Bfl",
cerebras: "Cerebras",
"chatgpt-web": "OpenAI",
claude: "ClaudeCode",
cline: "Cline",
cloudflare: "Cloudflare",
@@ -325,6 +335,7 @@ const LOBE_PROVIDER_ALIASES = {
"lambda-ai": "Lambda",
"lm-studio": "LmStudio",
lmstudio: "LmStudio",
longcat: "LongCat",
"meta-llama": "Meta",
minimax: "Minimax",
"minimax-cn": "Minimax",
@@ -352,10 +363,12 @@ const LOBE_PROVIDER_ALIASES = {
"perplexity-search": "Perplexity",
"perplexity-web": "Perplexity",
poe: "Poe",
pollinations: "Pollinations",
qoder: "Qoder",
qwen: "Qwen",
recraft: "Recraft",
replicate: "Replicate",
roo: "RooCode",
runwayml: "Runway",
sambanova: "SambaNova",
sdwebui: "Automatic",
@@ -369,6 +382,7 @@ const LOBE_PROVIDER_ALIASES = {
"tavily-search": "Tavily",
together: "Together",
topaz: "TopazLabs",
triton: "Nvidia",
upstage: "Upstage",
v0: "V0",
"v0-vercel": "V0",
@@ -382,6 +396,7 @@ const LOBE_PROVIDER_ALIASES = {
voyage: "Voyage",
"voyage-ai": "Voyage",
watsonx: "IBM",
windsurf: "Windsurf",
"workers-ai": "WorkersAI",
workersai: "WorkersAI",
xai: "XAI",