diff --git a/CHANGELOG.md b/CHANGELOG.md index d185446ddc..defc45cfce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,11 @@ ### Fixed +- **dashboard:** custom providers (`openai-compatible-*` / `anthropic-compatible-*`) + now show their user-given node name instead of the raw UUID id across the + active-requests panel, proxy logger, and home-page provider topology. The + display-label resolver was extracted into a shared util reused by all surfaces + (previously only the request-log viewer resolved it). (#2968) - **docker:** the standalone launcher (Docker `CMD`) now honors `OMNIROUTE_MEMORY_MB` (default 512, clamped [64, 16384]) and overrides the baked `--max-old-space-size=256`, fixing random OOM crashes under load / with diff --git a/src/app/(dashboard)/dashboard/HomePageClient.tsx b/src/app/(dashboard)/dashboard/HomePageClient.tsx index 739cd9a1ea..0c62159272 100644 --- a/src/app/(dashboard)/dashboard/HomePageClient.tsx +++ b/src/app/(dashboard)/dashboard/HomePageClient.tsx @@ -11,6 +11,7 @@ import ProviderIcon from "@/shared/components/ProviderIcon"; import { AI_PROVIDERS, NOAUTH_PROVIDERS, OAUTH_PROVIDERS } from "@/shared/constants/providers"; import { useNotificationStore } from "@/store/notificationStore"; import { copyToClipboard } from "@/shared/utils/clipboard"; +import { getProviderDisplayLabel } from "@/shared/utils/providerDisplayLabel"; import { useIsElectron, useOpenExternal } from "@/shared/hooks/useElectron"; const ProviderTopology = dynamic(() => import("../home/ProviderTopology"), { ssr: false }); @@ -117,6 +118,9 @@ export default function HomePageClient({ machineId }: HomePageClientProps) { const [selectedProvider, setSelectedProvider] = useState(null); const [providerMetrics, setProviderMetrics] = useState>({}); const [activeRequests, setActiveRequests] = useState([]); + const [providerNodes, setProviderNodes] = useState< + Array<{ id?: string; prefix?: string; name?: string }> + >([]); const [versionInfo, setVersionInfo] = useState(null); const [updating, setUpdating] = useState(false); @@ -268,6 +272,14 @@ export default function HomePageClient({ machineId }: HomePageClientProps) { fetchData(); }, [fetchData]); + // Fetch provider nodes for display labels (compat providers) + useEffect(() => { + fetch("/api/provider-nodes") + .then((r) => (r.ok ? r.json() : { nodes: [] })) + .then((d) => setProviderNodes(d.nodes || [])) + .catch(() => {}); + }, []); + useEffect(() => { let cancelled = false; let timeoutId: ReturnType | null = null; @@ -464,10 +476,16 @@ export default function HomePageClient({ machineId }: HomePageClientProps) { const canonicalProviderId = normalizeProviderId(rawProviderId); if (!canonicalProviderId || byProvider.has(canonicalProviderId)) return; + const resolvedName = + getProviderDisplayLabel(rawProviderId, providerNodes) || + name || + providerConfig[canonicalProviderId]?.name || + rawProviderId; + byProvider.set(canonicalProviderId, { id: canonicalProviderId, provider: canonicalProviderId, - name: name || providerConfig[canonicalProviderId]?.name || rawProviderId, + name: resolvedName, }); }; @@ -478,7 +496,7 @@ export default function HomePageClient({ machineId }: HomePageClientProps) { activeRequests.forEach((request) => addProvider(request.provider)); return Array.from(byProvider.values()); - }, [providerStats, providerMetrics, activeRequests]); + }, [providerStats, providerMetrics, activeRequests, providerNodes]); const topologyActiveRequests = useMemo( () => diff --git a/src/shared/components/ActiveRequestsPanel.tsx b/src/shared/components/ActiveRequestsPanel.tsx index 2b58605b59..2466f26ac3 100644 --- a/src/shared/components/ActiveRequestsPanel.tsx +++ b/src/shared/components/ActiveRequestsPanel.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from "react"; import { useTranslations } from "next-intl"; import { maskAccount } from "@/shared/utils/formatting"; +import { getProviderDisplayLabel } from "@/shared/utils/providerDisplayLabel"; import useEmailPrivacyStore from "@/store/emailPrivacyStore"; type ActiveRequestRow = { @@ -46,6 +47,16 @@ export default function ActiveRequestsPanel() { const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); const [selectedRow, setSelectedRow] = useState(null); + const [providerNodes, setProviderNodes] = useState< + Array<{ id?: string; prefix?: string; name?: string }> + >([]); + + useEffect(() => { + fetch("/api/provider-nodes") + .then((r) => (r.ok ? r.json() : { nodes: [] })) + .then((d) => setProviderNodes(d.nodes || [])) + .catch(() => {}); + }, []); useEffect(() => { let cancelled = false; @@ -149,7 +160,9 @@ export default function ActiveRequestsPanel() { className="border-t border-border/60" > {row.model} - {row.provider} + + {getProviderDisplayLabel(row.provider, providerNodes) || row.provider} + {accountLabel} @@ -178,7 +191,9 @@ export default function ActiveRequestsPanel() {

- {selectedRow.provider} / {selectedRow.model} + {getProviderDisplayLabel(selectedRow.provider, providerNodes) || + selectedRow.provider}{" "} + / {selectedRow.model}

{t("runningRequestDetailMeta", { diff --git a/src/shared/components/ProxyLogger.tsx b/src/shared/components/ProxyLogger.tsx index ba8fbcb23b..58375598ba 100644 --- a/src/shared/components/ProxyLogger.tsx +++ b/src/shared/components/ProxyLogger.tsx @@ -15,6 +15,7 @@ import { formatDuration as formatLatency, truncateUrl, } from "@/shared/utils/formatting"; +import { getProviderDisplayLabel } from "@/shared/utils/providerDisplayLabel"; const PROXY_COLUMN_KEYS = [ "status", @@ -43,6 +44,9 @@ export default function ProxyLogger() { const [selectedLevel, setSelectedLevel] = useState(""); const [sortBy, setSortBy] = useState("newest"); const [selectedLog, setSelectedLog] = useState(null); + const [providerNodes, setProviderNodes] = useState< + Array<{ id?: string; prefix?: string; name?: string }> + >([]); const intervalRef = useRef(null); const hasLoadedRef = useRef(false); const logsSignatureRef = useRef(""); @@ -132,6 +136,14 @@ export default function ProxyLogger() { fetchLogs(showLoading); }, [fetchLogs]); + // Fetch provider nodes for display labels + useEffect(() => { + fetch("/api/provider-nodes") + .then((r) => (r.ok ? r.json() : { nodes: [] })) + .then((d) => setProviderNodes(d.nodes || [])) + .catch(() => {}); + }, []); + useEffect(() => { if (intervalRef.current) clearInterval(intervalRef.current); if (recording) { @@ -454,10 +466,13 @@ export default function ProxyLogger() { label: log.proxy?.type || "-", }; const levelColor = LEVEL_COLORS[log.level] || LEVEL_COLORS.direct; + const resolvedProviderLabel = + getProviderDisplayLabel(log.provider, providerNodes) || + (log.provider || "-").toUpperCase(); const providerColor = PROVIDER_COLORS[log.provider] || { bg: "#374151", text: "#fff", - label: (log.provider || "-").toUpperCase(), + label: resolvedProviderLabel, }; const isError = log.status === "error" || log.status === "timeout"; diff --git a/src/shared/components/RequestLoggerV2.tsx b/src/shared/components/RequestLoggerV2.tsx index 6fcf6bbb3f..f2cc14a1b4 100644 --- a/src/shared/components/RequestLoggerV2.tsx +++ b/src/shared/components/RequestLoggerV2.tsx @@ -18,6 +18,7 @@ import { stableAccountSuffix, formatApiKeyLabel, } from "@/shared/utils/formatting"; +import { getProviderDisplayLabel } from "@/shared/utils/providerDisplayLabel"; import useEmailPrivacyStore from "@/store/emailPrivacyStore"; // Number of call-log rows fetched per page. The viewer grows its window by this @@ -25,39 +26,6 @@ import useEmailPrivacyStore from "@/store/emailPrivacyStore"; // page (previously hardcoded to a single 300-row window). See #2565. const PAGE_SIZE = 300; -/** - * Get a friendly display label for compatible providers. - * Converts long IDs like "openai-compatible-chat-02669115-2545-4896-b003-cb4dac09d441" - * to readable labels. If providerNodes are available, uses user-defined name; - * otherwise falls back to "OAI-Compat". - */ -function getProviderDisplayLabel(provider: string, providerNodes?: any[]): string { - if (!provider) return "-"; - if (provider.startsWith("openai-compatible-") || provider.startsWith("anthropic-compatible-")) { - // Try to find user-defined name from provider nodes - if (providerNodes?.length) { - const matchedNode = providerNodes.find( - (node) => node.id === provider || node.prefix === provider - ); - if (matchedNode?.name) return matchedNode.name; - } - // Fallback to generic labels - if (provider.startsWith("openai-compatible-")) { - const suffix = provider.replace("openai-compatible-", ""); - const parts = suffix.split("-"); - if (parts.length > 1 && parts[1]?.length >= 8) return `OAI-COMPAT`; - return `OAI: ${suffix.slice(0, 16).toUpperCase()}`; - } - if (provider.startsWith("anthropic-compatible-")) { - const suffix = provider.replace("anthropic-compatible-", ""); - const parts = suffix.split("-"); - if (parts.length > 1 && parts[1]?.length >= 8) return `ANT-COMPAT`; - return `ANT: ${suffix.slice(0, 16).toUpperCase()}`; - } - } - return null; // Not a compatible provider, use default PROVIDER_COLORS -} - function getLogTotalTokens(log) { return (log?.tokens?.in || 0) + (log?.tokens?.out || 0); } diff --git a/src/shared/utils/providerDisplayLabel.ts b/src/shared/utils/providerDisplayLabel.ts new file mode 100644 index 0000000000..1fa02d2a9d --- /dev/null +++ b/src/shared/utils/providerDisplayLabel.ts @@ -0,0 +1,41 @@ +/** + * Get a friendly display label for compatible providers. + * Converts long IDs like "openai-compatible-chat-02669115-2545-4896-b003-cb4dac09d441" + * to readable labels. If providerNodes are available, uses user-defined name; + * otherwise falls back to "OAI-COMPAT" / "ANT-COMPAT". + * + * @param provider - The raw provider ID string from a request log or active request. + * @param providerNodes - Optional array of provider node objects (from /api/provider-nodes). + * @returns A human-readable label for compatible providers, or `null` if the provider + * is not an openai-compatible-* or anthropic-compatible-* provider (caller + * should use its own default in that case). + */ +export function getProviderDisplayLabel( + provider: string, + providerNodes?: Array<{ id?: string; prefix?: string; name?: string }> +): string | null { + if (!provider) return "-"; + if (provider.startsWith("openai-compatible-") || provider.startsWith("anthropic-compatible-")) { + // Try to find user-defined name from provider nodes + if (providerNodes?.length) { + const matchedNode = providerNodes.find( + (node) => node.id === provider || node.prefix === provider + ); + if (matchedNode?.name) return matchedNode.name; + } + // Fallback to generic labels + if (provider.startsWith("openai-compatible-")) { + const suffix = provider.replace("openai-compatible-", ""); + const parts = suffix.split("-"); + if (parts.length > 1 && parts[1]?.length >= 8) return `OAI-COMPAT`; + return `OAI: ${suffix.slice(0, 16).toUpperCase()}`; + } + if (provider.startsWith("anthropic-compatible-")) { + const suffix = provider.replace("anthropic-compatible-", ""); + const parts = suffix.split("-"); + if (parts.length > 1 && parts[1]?.length >= 8) return `ANT-COMPAT`; + return `ANT: ${suffix.slice(0, 16).toUpperCase()}`; + } + } + return null; // Not a compatible provider, use default PROVIDER_COLORS +} diff --git a/tests/unit/provider-display-label.test.ts b/tests/unit/provider-display-label.test.ts new file mode 100644 index 0000000000..fd2437eb0a --- /dev/null +++ b/tests/unit/provider-display-label.test.ts @@ -0,0 +1,81 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { getProviderDisplayLabel } from "../../src/shared/utils/providerDisplayLabel.ts"; + +test("returns matched node name for openai-compatible UUID id", () => { + const providerNodes = [ + { id: "openai-compatible-chat-02669115-2545-4896-b003-cb4dac09d441", prefix: undefined, name: "My Custom OAI" }, + ]; + const result = getProviderDisplayLabel( + "openai-compatible-chat-02669115-2545-4896-b003-cb4dac09d441", + providerNodes + ); + assert.equal(result, "My Custom OAI"); +}); + +test("returns matched node name when matched by prefix", () => { + const providerNodes = [ + { id: "some-other-id", prefix: "openai-compatible-myservice", name: "My Service" }, + ]; + const result = getProviderDisplayLabel("openai-compatible-myservice", providerNodes); + assert.equal(result, "My Service"); +}); + +test("returns OAI-COMPAT fallback for openai-compatible UUID when no matching node", () => { + const result = getProviderDisplayLabel( + "openai-compatible-chat-02669115-2545-4896-b003-cb4dac09d441", + [] + ); + assert.equal(result, "OAI-COMPAT"); +}); + +test("returns OAI-COMPAT fallback for openai-compatible UUID when providerNodes is undefined", () => { + const result = getProviderDisplayLabel( + "openai-compatible-chat-02669115-2545-4896-b003-cb4dac09d441" + ); + assert.equal(result, "OAI-COMPAT"); +}); + +test("returns OAI: prefix label for openai-compatible short name (no UUID)", () => { + const result = getProviderDisplayLabel("openai-compatible-myserver", []); + assert.equal(result, "OAI: MYSERVER"); +}); + +test("returns ANT-COMPAT fallback for anthropic-compatible UUID when no matching node", () => { + const result = getProviderDisplayLabel( + "anthropic-compatible-chat-02669115-2545-4896-b003-cb4dac09d441", + [] + ); + assert.equal(result, "ANT-COMPAT"); +}); + +test("returns ANT: prefix label for anthropic-compatible short name (no UUID)", () => { + const result = getProviderDisplayLabel("anthropic-compatible-myserver", []); + assert.equal(result, "ANT: MYSERVER"); +}); + +test("returns null for a plain provider like openai", () => { + const result = getProviderDisplayLabel("openai", []); + assert.equal(result, null); +}); + +test("returns null for a plain provider like anthropic", () => { + const result = getProviderDisplayLabel("anthropic", []); + assert.equal(result, null); +}); + +test("returns null for a plain provider with no providerNodes arg", () => { + const result = getProviderDisplayLabel("gemini"); + assert.equal(result, null); +}); + +test("matched node name wins over fallback for anthropic-compatible UUID", () => { + const providerNodes = [ + { id: "anthropic-compatible-chat-02669115-2545-4896-b003-cb4dac09d441", name: "My Custom ANT" }, + ]; + const result = getProviderDisplayLabel( + "anthropic-compatible-chat-02669115-2545-4896-b003-cb4dac09d441", + providerNodes + ); + assert.equal(result, "My Custom ANT"); +});