Merge PR 2994 into release/v3.8.8

fix(dashboard): resolve custom provider display name across surfaces (#2968)
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-05-31 09:36:56 -03:00
committed by GitHub
7 changed files with 181 additions and 38 deletions

View File

@@ -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

View File

@@ -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<Record<string, ProviderMetricSummary>>({});
const [activeRequests, setActiveRequests] = useState<ActiveRequestSummary[]>([]);
const [providerNodes, setProviderNodes] = useState<
Array<{ id?: string; prefix?: string; name?: string }>
>([]);
const [versionInfo, setVersionInfo] = useState<VersionInfo | null>(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<typeof setTimeout> | 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(
() =>

View File

@@ -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<ActiveRequestRow[]>([]);
const [loading, setLoading] = useState(true);
const [selectedRow, setSelectedRow] = useState<ActiveRequestRow | null>(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"
>
<td className="px-4 py-3 font-medium text-text-main">{row.model}</td>
<td className="px-4 py-3 text-text-muted">{row.provider}</td>
<td className="px-4 py-3 text-text-muted">
{getProviderDisplayLabel(row.provider, providerNodes) || row.provider}
</td>
<td className="px-4 py-3 text-text-muted" title={accountLabel}>
{accountLabel}
</td>
@@ -178,7 +191,9 @@ export default function ActiveRequestsPanel() {
<div className="flex items-start justify-between gap-4 border-b border-border px-5 py-4">
<div>
<h4 className="text-lg font-semibold text-text-main">
{selectedRow.provider} / {selectedRow.model}
{getProviderDisplayLabel(selectedRow.provider, providerNodes) ||
selectedRow.provider}{" "}
/ {selectedRow.model}
</h4>
<p className="mt-1 text-sm text-text-muted">
{t("runningRequestDetailMeta", {

View File

@@ -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";

View File

@@ -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);
}

View File

@@ -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
}

View File

@@ -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");
});