- {isMcp ? (
-
- ) : isAgent ? (
-
- ) : (
-
- )}
+ {eventIcon}
{getEventDisplayName(row.call_type, row.model)}
@@ -274,7 +278,12 @@ export function LogDetailsDrawer({
).length;
const agentCount = sessionLogs.filter((row) => AGENT_CALL_TYPES.includes(row.call_type)).length;
const mcpCount = sessionLogs.filter((row) => MCP_CALL_TYPES.includes(row.call_type)).length;
- const logsForList = isSessionMode ? sessionLogs : currentLog ? [currentLog] : [];
+ let logsForList: LogEntry[] = [];
+ if (isSessionMode) {
+ logsForList = sessionLogs;
+ } else if (currentLog) {
+ logsForList = [currentLog];
+ }
const leftPanelId = isSessionMode ? sessionId || "" : currentLog?.request_id || "";
const leftPanelDisplayId = leftPanelId.length > 14 ? `${leftPanelId.slice(0, 11)}...` : leftPanelId;
diff --git a/ui/litellm-dashboard/src/components/view_logs/TypeBadges.test.tsx b/ui/litellm-dashboard/src/components/view_logs/TypeBadges.test.tsx
index e3467310265..d63df05cb3e 100644
--- a/ui/litellm-dashboard/src/components/view_logs/TypeBadges.test.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/TypeBadges.test.tsx
@@ -1,6 +1,14 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
-import { LlmBadge, McpBadge, AgentBadge } from "./TypeBadges";
+import {
+ LlmBadge,
+ McpBadge,
+ AgentBadge,
+ RelayBadge,
+ RelaySourceBadge,
+ RelayTypeBadge,
+ getRelaySource,
+} from "./TypeBadges";
describe("TypeBadges", () => {
describe("LlmBadge", () => {
@@ -43,4 +51,43 @@ describe("TypeBadges", () => {
expect(screen.getByText("12")).toBeInTheDocument();
});
});
+
+ describe("RelayBadge", () => {
+ it("should render with default 'litellm-relay' text when no count is provided", () => {
+ render(
);
+ expect(screen.getByText("litellm-relay")).toBeInTheDocument();
+ });
+ });
+
+ describe("RelaySourceBadge", () => {
+ it("should render Notion source with logo", () => {
+ render(
);
+ expect(screen.getByRole("img", { name: "Notion logo" })).toBeInTheDocument();
+ expect(screen.getByText("Notion")).toBeInTheDocument();
+ });
+
+ it("should render Codex source with logo", () => {
+ render(
);
+ expect(screen.getByRole("img", { name: "Codex logo" })).toBeInTheDocument();
+ expect(screen.getByText("Codex")).toBeInTheDocument();
+ });
+
+ it("should derive source from relay metadata app", () => {
+ expect(getRelaySource({ metadata: { app: "notion" }, model: "local-ai" })).toBe("notion");
+ });
+
+ it("should derive source from relay model when metadata is missing", () => {
+ expect(getRelaySource({ model: "codex-ai" })).toBe("codex");
+ });
+ });
+
+ describe("RelayTypeBadge", () => {
+ it("should render relay type next to the captured app", () => {
+ render(
);
+
+ expect(screen.getByText("litellm-relay")).toBeInTheDocument();
+ expect(screen.getByRole("img", { name: "Notion logo" })).toBeInTheDocument();
+ expect(screen.getByText("Notion")).toBeInTheDocument();
+ });
+ });
});
diff --git a/ui/litellm-dashboard/src/components/view_logs/TypeBadges.tsx b/ui/litellm-dashboard/src/components/view_logs/TypeBadges.tsx
index 77ff4cfef62..a8c3ef751aa 100644
--- a/ui/litellm-dashboard/src/components/view_logs/TypeBadges.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/TypeBadges.tsx
@@ -1,7 +1,9 @@
/**
- * Compact type-indicator badges for LLM, Agent, and MCP log entries.
+ * Compact type-indicator badges for LLM, Agent, MCP, and Relay log entries.
* Used in the request logs table and session type column.
*/
+import { Cable, Monitor } from "lucide-react";
+import { resolveLogoSrc } from "@/lib/assetPaths";
export const SparkleIcon = ({ size = 12 }: { size?: number }) => (
);
+export const RelayIcon = ({ size = 12 }: { size?: number }) =>
;
+
export const LlmBadge = ({ count }: { count?: number }) => (
@@ -77,3 +81,97 @@ export const AgentBadge = ({ count }: { count?: number }) => (
{count != null ? count : "Agent"}
);
+
+export const RelayBadge = ({ count }: { count?: number }) => (
+
+
+ {count != null ? count : "litellm-relay"}
+
+);
+
+type RelaySourceLike = {
+ metadata?: Record
;
+ model?: string;
+ request_tags?: Record | string[] | string;
+};
+
+const RELAY_SOURCE_LABELS: Record = {
+ notion: "Notion",
+ codex: "Codex",
+};
+
+const RELAY_SOURCE_LOGOS: Record = {
+ notion: "/ui/assets/logos/notion.svg",
+ codex: "/ui/assets/logos/openai_small.svg",
+};
+
+const normalizeRelaySource = (value: unknown): string | undefined => {
+ if (typeof value !== "string") return undefined;
+ const normalized = value.trim().toLowerCase();
+ if (!normalized) return undefined;
+ if (normalized === "litellm-relay") return undefined;
+ if (normalized.includes("notion")) return "notion";
+ if (normalized.includes("codex")) return "codex";
+ return normalized.replace(/-ai$/, "");
+};
+
+const getTagSource = (requestTags: RelaySourceLike["request_tags"]): string | undefined => {
+ if (Array.isArray(requestTags)) {
+ return requestTags.map(normalizeRelaySource).find(Boolean);
+ }
+ if (typeof requestTags === "string") {
+ try {
+ const parsed = JSON.parse(requestTags);
+ return getTagSource(parsed);
+ } catch {
+ return normalizeRelaySource(requestTags);
+ }
+ }
+ return undefined;
+};
+
+export const getRelaySource = (entry: RelaySourceLike): string => {
+ return (
+ normalizeRelaySource(entry.metadata?.app) ||
+ normalizeRelaySource(entry.metadata?.relay_app) ||
+ normalizeRelaySource(entry.metadata?.shadow_source) ||
+ normalizeRelaySource(entry.metadata?.host) ||
+ getTagSource(entry.request_tags) ||
+ normalizeRelaySource(entry.model) ||
+ "unknown"
+ );
+};
+
+export const getRelaySourceLabel = (source: string) => {
+ return RELAY_SOURCE_LABELS[source] || source.charAt(0).toUpperCase() + source.slice(1);
+};
+
+export const RelaySourceLogo = ({ source, size = 16 }: { source: string; size?: number }) => {
+ const logo = RELAY_SOURCE_LOGOS[source];
+ if (logo) {
+ return (
+
+
+
+ );
+ }
+ return (
+
+
+
+ );
+};
+
+export const RelaySourceBadge = ({ source }: { source: string }) => (
+
+
+ {getRelaySourceLabel(source)}
+
+);
+
+export const RelayTypeBadge = ({ source }: { source: string }) => (
+
+
+
+
+);
diff --git a/ui/litellm-dashboard/src/components/view_logs/columns.test.tsx b/ui/litellm-dashboard/src/components/view_logs/columns.test.tsx
index afdd17813f3..2743eae8309 100644
--- a/ui/litellm-dashboard/src/components/view_logs/columns.test.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/columns.test.tsx
@@ -54,3 +54,66 @@ describe("Cost column", () => {
expect(await screen.findByText("$0.00012345678")).toBeInTheDocument();
});
});
+
+describe("view logs columns", () => {
+ const relayLog = logEntry({
+ request_id: "req-relay",
+ api_key: "hashed-relay-key",
+ team_id: "",
+ model: "notion-ai",
+ api_base: "https://www.notion.so",
+ call_type: "litellm-relay",
+ total_tokens: 0,
+ prompt_tokens: 0,
+ completion_tokens: 0,
+ metadata: {
+ app: "notion",
+ status: "success",
+ status_code: 200,
+ user_api_key: null,
+ user_api_key_alias: "relay-key",
+ user_api_key_team_alias: null,
+ },
+ request_tags: { source: "notion" },
+ proxy_server_request: {},
+ status: "success",
+ });
+
+ it("should render relay type with captured app logo and name", () => {
+ render( row.request_id} />);
+
+ expect(screen.getByText("litellm-relay")).toBeInTheDocument();
+ expect(screen.getAllByText("Notion").length).toBeGreaterThan(0);
+ expect(screen.getAllByRole("img", { name: "Notion logo" }).length).toBeGreaterThan(0);
+ expect(screen.queryByRole("columnheader", { name: "Source" })).not.toBeInTheDocument();
+ expect(screen.queryByText("LLM")).not.toBeInTheDocument();
+ });
+
+ it("should render collector rows with relay metadata as relay logs", () => {
+ const legacyCollectorLog = {
+ ...relayLog,
+ request_id: "collector-01f159da",
+ call_type: "completion",
+ metadata: {
+ ...relayLog.metadata,
+ app: "codex",
+ source: "litellm-relay",
+ },
+ model: "codex-ai",
+ request_tags: ["litellm-relay", "codex"],
+ };
+
+ render( row.request_id} />);
+
+ expect(screen.getByText("litellm-relay")).toBeInTheDocument();
+ expect(screen.getAllByText("Codex").length).toBeGreaterThan(0);
+ expect(screen.getAllByRole("img", { name: "Codex logo" }).length).toBeGreaterThan(0);
+ expect(screen.queryByText("LLM")).not.toBeInTheDocument();
+ });
+
+ it("should fall back to row api_key when relay metadata does not include a key hash", () => {
+ render( row.request_id} />);
+
+ expect(screen.getByText("hashed-relay-key")).toBeInTheDocument();
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/view_logs/columns.tsx b/ui/litellm-dashboard/src/components/view_logs/columns.tsx
index 1d0f3f33d08..65205ce09ca 100644
--- a/ui/litellm-dashboard/src/components/view_logs/columns.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/columns.tsx
@@ -5,8 +5,18 @@ import { Tooltip } from "antd";
import React from "react";
import { getProviderLogoAndName } from "../provider_info_helpers";
import { TableHeaderSortDropdown } from "../common_components/TableHeaderSortDropdown/TableHeaderSortDropdown";
-import { AGENT_CALL_TYPES, MCP_CALL_TYPES } from "./constants";
-import { AgentBadge, AgentIcon, LlmBadge, McpBadge, SparkleIcon, WrenchIcon } from "./TypeBadges";
+import { AGENT_CALL_TYPES, MCP_CALL_TYPES, RELAY_CALL_TYPES } from "./constants";
+import {
+ AgentBadge,
+ AgentIcon,
+ LlmBadge,
+ McpBadge,
+ RelayIcon,
+ RelayTypeBadge,
+ SparkleIcon,
+ WrenchIcon,
+ getRelaySource,
+} from "./TypeBadges";
/** API sort field mapping for /spend/logs/ui endpoint */
export const LOGS_SORT_FIELD_MAP = {
@@ -56,7 +66,7 @@ export type LogEntry = {
metadata?: Record;
cache_hit: string;
cache_key?: string;
- request_tags?: Record;
+ request_tags?: Record | string[] | string;
requester_ip_address?: string;
messages: string | any[] | Record;
response: string | any[] | Record;
@@ -104,6 +114,32 @@ const SortableHeader = ({
);
+const requestTagsIncludeRelay = (requestTags: LogEntry["request_tags"]): boolean => {
+ if (!requestTags) return false;
+ if (Array.isArray(requestTags)) {
+ return requestTags.some((tag) => String(tag).toLowerCase() === "litellm-relay");
+ }
+ if (typeof requestTags === "string") {
+ try {
+ return requestTagsIncludeRelay(JSON.parse(requestTags));
+ } catch {
+ return requestTags.toLowerCase().includes("litellm-relay");
+ }
+ }
+ return Object.entries(requestTags).some(
+ ([key, value]) => key.toLowerCase().includes("litellm-relay") || String(value).toLowerCase().includes("litellm-relay"),
+ );
+};
+
+const isRelayLog = (row: LogEntry): boolean => {
+ return (
+ RELAY_CALL_TYPES.includes(row.call_type) ||
+ row.metadata?.source === "litellm-relay" ||
+ requestTagsIncludeRelay(row.request_tags) ||
+ (row.request_id?.startsWith("collector-") && getRelaySource(row) !== "unknown")
+ );
+};
+
export const createColumns = (sortProps?: LogsSortProps): ColumnDef