mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix(dashboard): resolve custom provider display name across surfaces (#2968)
Custom providers (openai-compatible-<uuid> / anthropic-compatible-<uuid>) showed the raw UUID id instead of the user-given node name in the active-requests panel, proxy logger, and home-page provider topology. Only RequestLoggerV2 resolved it (via a local helper + /api/provider-nodes fetch). Extract the resolver into a shared util (src/shared/utils/providerDisplayLabel.ts, unit-tested), reuse it in RequestLoggerV2, and apply it (with a provider-nodes fetch) in ActiveRequestsPanel, ProxyLogger, and the HomePageClient topology so all surfaces show the user-defined provider name. Closes #2968
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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(
|
||||
() =>
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
41
src/shared/utils/providerDisplayLabel.ts
Normal file
41
src/shared/utils/providerDisplayLabel.ts
Normal 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
|
||||
}
|
||||
81
tests/unit/provider-display-label.test.ts
Normal file
81
tests/unit/provider-display-label.test.ts
Normal 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");
|
||||
});
|
||||
Reference in New Issue
Block a user