diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json
index 369efb7e338..7820750cee3 100644
--- a/ui/litellm-dashboard/eslint-suppressions.json
+++ b/ui/litellm-dashboard/eslint-suppressions.json
@@ -678,14 +678,6 @@
"count": 1
}
},
- "src/components/WebRTCTester.jsx": {
- "no-restricted-syntax": {
- "count": 2
- },
- "react/no-unescaped-entities": {
- "count": 2
- }
- },
"src/components/activity_metrics.tsx": {
"no-restricted-imports": {
"count": 1
diff --git a/ui/litellm-dashboard/src/components/WebRTCTester.jsx b/ui/litellm-dashboard/src/components/WebRTCTester.jsx
deleted file mode 100644
index 75e331f6586..00000000000
--- a/ui/litellm-dashboard/src/components/WebRTCTester.jsx
+++ /dev/null
@@ -1,648 +0,0 @@
-import { useState, useRef, useEffect, useCallback } from "react";
-
-const STYLES = `
-.wrt-wrap {
- font-family: 'JetBrains Mono', 'Fira Code', monospace;
- background: #0d0d14;
- border: 1px solid #1e1e2e;
- border-radius: 10px;
- overflow: hidden;
- margin: 24px 0;
-}
-
-.wrt-toggle {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 14px 20px;
- cursor: pointer;
- user-select: none;
- background: #0d0d14;
- transition: background 0.15s;
-}
-.wrt-toggle:hover { background: #111120; }
-
-.wrt-toggle-left { display: flex; align-items: center; gap: 10px; }
-
-.wrt-live-dot {
- width: 8px; height: 8px; border-radius: 50%;
- background: #00ff88;
- box-shadow: 0 0 8px #00ff88;
- animation: wrt-blink 2s infinite;
-}
-@keyframes wrt-blink { 0%,100%{opacity:1} 50%{opacity:0.4} }
-
-.wrt-toggle-title { font-size: 12px; font-weight: 600; color: #e2e8f0; letter-spacing: 0.06em; }
-.wrt-toggle-sub { font-size: 10px; color: #4a5568; margin-top: 1px; }
-.wrt-chevron { font-size: 11px; color: #4a5568; transition: transform 0.2s; }
-.wrt-chevron.open { transform: rotate(180deg); }
-
-.wrt-body {
- border-top: 1px solid #1e1e2e;
- display: grid;
- grid-template-columns: 280px 1fr;
- height: 460px;
-}
-
-.wrt-sidebar {
- border-right: 1px solid #1e1e2e;
- padding: 14px;
- display: flex;
- flex-direction: column;
- gap: 12px;
- overflow-y: auto;
-}
-
-.wrt-label {
- font-size: 9px;
- letter-spacing: 0.15em;
- color: #4a5568;
- text-transform: uppercase;
- margin-bottom: 5px;
-}
-
-.wrt-field { display: flex; flex-direction: column; gap: 4px; margin-bottom: 6px; }
-.wrt-field label { font-size: 10px; color: #4a5568; }
-.wrt-field input {
- background: #0a0a0f;
- border: 1px solid #1e1e2e;
- border-radius: 5px;
- color: #e2e8f0;
- font-family: inherit;
- font-size: 11px;
- padding: 7px 9px;
- outline: none;
- width: 100%;
- transition: border-color 0.2s;
-}
-.wrt-field input:focus { border-color: #7c3aed; }
-
-.wrt-divider { height: 1px; background: #1e1e2e; }
-
-.wrt-btn {
- display: flex; align-items: center; justify-content: center;
- border: none; border-radius: 5px; cursor: pointer;
- font-family: inherit; font-size: 11px; font-weight: 600;
- padding: 8px; width: 100%;
- transition: all 0.15s; letter-spacing: 0.04em;
-}
-.wrt-btn + .wrt-btn { margin-top: 5px; }
-.wrt-btn-primary { background: #00ff88; color: #000; }
-.wrt-btn-primary:hover:not(:disabled) { filter: brightness(1.1); }
-.wrt-btn-primary:disabled { opacity: 0.35; cursor: not-allowed; }
-.wrt-btn-danger { background: transparent; color: #ff4466; border: 1px solid #ff4466; }
-.wrt-btn-danger:hover:not(:disabled) { background: rgba(255,68,102,0.08); }
-.wrt-btn-danger:disabled { opacity: 0.3; cursor: not-allowed; }
-.wrt-btn-ghost { background: #111118; color: #e2e8f0; border: 1px solid #1e1e2e; }
-.wrt-btn-ghost:hover { border-color: #7c3aed; }
-
-.wrt-flow { display: flex; align-items: center; padding: 4px 0; gap: 0; }
-.wrt-flow-box {
- padding: 4px 7px; border-radius: 4px; font-size: 9px;
- border: 1px solid #1e1e2e; color: #4a5568;
- transition: all 0.3s; white-space: nowrap;
-}
-.wrt-flow-box.active { border-color: #00ff88; color: #00ff88; box-shadow: 0 0 8px rgba(0,255,136,0.15); }
-.wrt-flow-arrow { font-size: 10px; color: #4a5568; padding: 0 4px; transition: color 0.3s; }
-.wrt-flow-arrow.active { color: #00ff88; }
-
-.wrt-meta { display: flex; flex-direction: column; gap: 4px; }
-.wrt-meta-row { display: flex; justify-content: space-between; font-size: 10px; }
-.wrt-meta-row span:first-child { color: #4a5568; }
-.wrt-meta-row span:last-child { color: #e2e8f0; }
-
-.wrt-status-pill {
- display: flex; align-items: center; gap: 6px;
- font-size: 10px; color: #4a5568;
- background: #111118; border: 1px solid #1e1e2e;
- border-radius: 100px; padding: 3px 10px;
-}
-.wrt-status-dot {
- width: 6px; height: 6px; border-radius: 50%;
- background: #4a5568; transition: all 0.3s;
-}
-.wrt-status-dot.connected { background: #00ff88; box-shadow: 0 0 6px #00ff88; }
-.wrt-status-dot.connecting { background: #ffaa00; animation: wrt-blink 1s infinite; }
-.wrt-status-dot.error { background: #ff4466; }
-
-.wrt-main { display: flex; flex-direction: column; overflow: hidden; }
-
-.wrt-header {
- display: flex; align-items: center; justify-content: space-between;
- padding: 8px 14px; border-bottom: 1px solid #1e1e2e; background: #111118;
-}
-.wrt-header-title { font-size: 10px; color: #4a5568; letter-spacing: 0.08em; }
-
-.wrt-tabs { display: flex; padding: 0 14px; border-bottom: 1px solid #1e1e2e; }
-.wrt-tab {
- font-size: 9px; letter-spacing: 0.08em; padding: 10px 12px; cursor: pointer;
- color: #4a5568; border-bottom: 2px solid transparent; transition: all 0.15s;
- user-select: none;
-}
-.wrt-tab.active { color: #00ff88; border-bottom-color: #00ff88; }
-.wrt-tab:hover:not(.active) { color: #e2e8f0; }
-
-.wrt-tab-content { flex: 1; overflow: hidden; display: none; flex-direction: column; }
-.wrt-tab-content.active { display: flex; }
-
-.wrt-log {
- flex: 1; overflow-y: auto; padding: 8px 12px;
- display: flex; flex-direction: column; gap: 2px;
-}
-.wrt-log::-webkit-scrollbar { width: 3px; }
-.wrt-log::-webkit-scrollbar-thumb { background: #1e1e2e; border-radius: 2px; }
-
-.wrt-entry {
- display: grid; grid-template-columns: 58px 56px 1fr; gap: 8px;
- padding: 3px 7px; border-radius: 3px;
- border-left: 2px solid transparent;
- font-size: 10px; line-height: 1.5;
- animation: wrt-fadein 0.15s ease;
-}
-@keyframes wrt-fadein { from { opacity:0; transform:translateY(2px); } to { opacity:1; transform:none; } }
-
-.wrt-entry.info { border-left-color: #7c3aed; }
-.wrt-entry.info .we-tag { color: #7c3aed; }
-.wrt-entry.success { border-left-color: #00ff88; }
-.wrt-entry.success .we-tag { color: #00ff88; }
-.wrt-entry.error { border-left-color: #ff4466; }
-.wrt-entry.error .we-tag { color: #ff4466; }
-.wrt-entry.warn { border-left-color: #ffaa00; }
-.wrt-entry.warn .we-tag { color: #ffaa00; }
-.wrt-entry.step { border-left-color: #60a5fa; }
-.wrt-entry.step .we-tag { color: #60a5fa; }
-
-.we-time { color: #4a5568; font-size: 9px; padding-top: 1px; }
-.we-tag { font-size: 9px; font-weight: 700; padding-top: 1px; }
-.we-msg { color: #e2e8f0; word-break: break-all; white-space: pre-wrap; }
-
-.wrt-empty {
- display: flex; flex-direction: column; align-items: center; justify-content: center;
- flex: 1; gap: 6px; color: #4a5568; font-size: 11px;
-}
-
-.wrt-sdp-pane { flex: 1; display: grid; grid-template-columns: 1fr 1fr; overflow: hidden; }
-.wrt-sdp-box { display: flex; flex-direction: column; border-right: 1px solid #1e1e2e; overflow: hidden; }
-.wrt-sdp-box:last-child { border-right: none; }
-.wrt-sdp-hdr {
- padding: 7px 12px; border-bottom: 1px solid #1e1e2e;
- font-size: 9px; color: #4a5568; letter-spacing: 0.08em;
- display: flex; align-items: center; gap: 6px;
-}
-.wrt-sdp-dot { width: 5px; height: 5px; border-radius: 50%; background: #1e1e2e; }
-.wrt-sdp-dot.active { background: #00ff88; }
-.wrt-sdp-pane textarea {
- flex: 1; background: transparent; border: none; color: #e2e8f0;
- font-family: inherit; font-size: 10px; padding: 10px 12px;
- resize: none; outline: none; line-height: 1.5;
-}
-
-.wrt-audio-pane {
- flex: 1; display: flex; flex-direction: column;
- align-items: center; justify-content: center; gap: 14px;
-}
-.wrt-viz { display: flex; align-items: center; gap: 2px; height: 44px; }
-.wrt-bar { width: 3px; border-radius: 2px; min-height: 2px; background: #00ff88; transition: height 0.05s; }
-.wrt-mic-btn {
- width: 52px; height: 52px; border-radius: 50%;
- background: #111118; border: 1.5px solid #1e1e2e;
- font-size: 18px; cursor: pointer;
- display: flex; align-items: center; justify-content: center; transition: all 0.2s;
-}
-.wrt-mic-btn.active { border-color: #00ff88; box-shadow: 0 0 16px rgba(0,255,136,0.2); }
-.wrt-audio-status { font-size: 10px; color: #4a5568; text-align: center; }
-`;
-
-function useLog() {
- const [entries, setEntries] = useState([]);
- const add = useCallback((level, tag, msg) => {
- const time = new Date().toTimeString().slice(0, 8);
- setEntries((prev) => [...prev, { level, tag, msg, time, id: Date.now() + Math.random() }]);
- }, []);
- const clear = useCallback(() => setEntries([]), []);
- return { entries, add, clear };
-}
-
-export default function WebRTCTester() {
- const [open, setOpen] = useState(false);
- const [activeTab, setActiveTab] = useState("logs");
- const [proxyUrl, setProxyUrl] = useState("http://localhost:4000");
- const [apiKey, setApiKey] = useState("sk-1234");
- const [model, setModel] = useState("gpt-4o-realtime-preview");
- const [status, setStatus] = useState("idle");
- const [flowStep, setFlowStep] = useState(0);
- const [tokenPreview, setTokenPreview] = useState("—");
- const [iceState, setIceState] = useState("—");
- const [connState, setConnState] = useState("—");
- const [dcState, setDcState] = useState("—");
- const [sdpOffer, setSdpOffer] = useState("");
- const [sdpAnswer, setSdpAnswer] = useState("");
- const [offerActive, setOfferActive] = useState(false);
- const [answerActive, setAnswerActive] = useState(false);
- const [audioStatus, setAudioStatus] = useState("Start a session first");
- const [micActive, setMicActive] = useState(false);
- const [bars, setBars] = useState(Array(28).fill(2));
- const [connected, setConnected] = useState(false);
-
- const { entries, add: log, clear: clearLogs } = useLog();
- const logRef = useRef(null);
-
- const pcRef = useRef(null);
- const dcRef = useRef(null);
- const streamRef = useRef(null);
- const audioCtxRef = useRef(null);
- const analyserRef = useRef(null);
- const animRef = useRef(null);
- const tokenRef = useRef(null);
- const micRef = useRef(false);
- const remoteAudioRef = useRef(null);
-
- useEffect(() => {
- if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight;
- }, [entries]);
-
- function drawBars() {
- animRef.current = requestAnimationFrame(drawBars);
- if (!analyserRef.current) return;
- const data = new Uint8Array(analyserRef.current.frequencyBinCount);
- analyserRef.current.getByteFrequencyData(data);
- setBars(Array.from({ length: 28 }, (_, i) => Math.max(2, ((data[i] || 0) / 255) * 42)));
- }
-
- function setupAnalyser(stream) {
- audioCtxRef.current = new AudioContext();
- const src = audioCtxRef.current.createMediaStreamSource(stream);
- analyserRef.current = audioCtxRef.current.createAnalyser();
- analyserRef.current.fftSize = 64;
- src.connect(analyserRef.current);
- drawBars();
- }
-
- async function startSession() {
- const url = proxyUrl.trim().replace(/\/$/, "");
- const key = apiKey.trim();
- const mdl = model.trim();
-
- setConnected(true);
- setStatus("connecting");
- setFlowStep(1);
-
- // Step 1: ephemeral token
- log("step", "STEP 1", `POST ${url}/v1/realtime/client_secrets`);
- let tokenResp;
- try {
- const r = await fetch(`${url}/v1/realtime/client_secrets`, {
- method: "POST",
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
- body: JSON.stringify({ model: mdl }),
- });
- log("info", "HTTP", `${r.status} ${r.statusText}`);
- const raw = await r.text();
- if (!r.ok) {
- log("error", "ERR", raw);
- stopSession();
- return;
- }
- tokenResp = JSON.parse(raw);
- log("success", "TOKEN", "Received encrypted ephemeral token");
- } catch (e) {
- log("error", "ERR", `client_secrets failed: ${e.message}`);
- stopSession();
- return;
- }
-
- const token = tokenResp?.client_secret?.value ?? tokenResp?.value;
- if (!token) {
- log("error", "ERR", `Cannot extract token: ${JSON.stringify(tokenResp)}`);
- stopSession();
- return;
- }
- tokenRef.current = token;
- setTokenPreview(token.slice(0, 10) + "…");
- log("info", "TOKEN", `Preview: ${token.slice(0, 10)}…`);
-
- // Step 2: PeerConnection
- log("step", "STEP 2", "Creating RTCPeerConnection");
- const pc = new RTCPeerConnection();
- pcRef.current = pc;
-
- pc.oniceconnectionstatechange = () => {
- setIceState(pc.iceConnectionState);
- log("info", "ICE", pc.iceConnectionState);
- if (pc.iceConnectionState === "connected" || pc.iceConnectionState === "completed") {
- setStatus("connected");
- setFlowStep(3);
- }
- if (pc.iceConnectionState === "failed" || pc.iceConnectionState === "disconnected") {
- setStatus("error");
- }
- };
-
- pc.onconnectionstatechange = () => {
- setConnState(pc.connectionState);
- log("info", "CONN", pc.connectionState);
- };
-
- pc.ontrack = (e) => {
- log("success", "AUDIO", "Remote audio track received from OpenAI");
- if (remoteAudioRef.current) remoteAudioRef.current.srcObject = e.streams[0];
- setupAnalyser(e.streams[0]);
- setAudioStatus("Receiving audio from OpenAI ✓");
- };
-
- const dc = pc.createDataChannel("oai-events");
- dcRef.current = dc;
- dc.onopen = () => {
- setDcState("open");
- log("success", "DC", "Data channel open — ready!");
- setStatus("connected");
- };
- dc.onclose = () => {
- setDcState("closed");
- log("warn", "DC", "Closed");
- };
- dc.onmessage = (e) => {
- try {
- log("info", "EVENT", JSON.parse(e.data).type ?? "unknown");
- } catch {
- log("info", "EVENT", e.data.slice(0, 100));
- }
- };
-
- // Mic
- try {
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
- streamRef.current = stream;
- stream.getTracks().forEach((t) => pc.addTrack(t, stream));
- log("success", "MIC", "Microphone access granted");
- setAudioStatus("Mic active — waiting for remote audio");
- micRef.current = true;
- setMicActive(true);
- } catch (e) {
- log("warn", "MIC", `Mic denied: ${e.message}`);
- const ctx = new AudioContext();
- const dest = ctx.createMediaStreamDestination();
- dest.stream.getTracks().forEach((t) => pc.addTrack(t, dest.stream));
- }
-
- // Step 3: SDP offer
- log("step", "STEP 3", "Creating SDP offer");
- const offer = await pc.createOffer();
- await pc.setLocalDescription(offer);
- setSdpOffer(offer.sdp);
- setOfferActive(true);
- log("info", "SDP", `Offer created (${offer.sdp.split("\n").length} lines)`);
-
- // Step 4: SDP exchange
- setFlowStep(2);
- log("step", "STEP 4", `POST ${url}/v1/realtime/calls`);
- try {
- const r = await fetch(`${url}/v1/realtime/calls`, {
- method: "POST",
- headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/sdp" },
- body: offer.sdp,
- });
- log("info", "HTTP", `${r.status} ${r.statusText}`);
- if (!r.ok) {
- log("error", "ERR", await r.text());
- stopSession();
- return;
- }
- const ans = await r.text();
- log("success", "SDP", `Answer received (${ans.split("\n").length} lines)`);
-
- // Step 5: remote description
- log("step", "STEP 5", "Setting remote description");
- await pc.setRemoteDescription({ type: "answer", sdp: ans });
- setSdpAnswer(ans);
- setAnswerActive(true);
- log("success", "CONN", "✓ Session established — Browser ↔ LiteLLM ↔ OpenAI");
- } catch (e) {
- log("error", "ERR", `calls failed: ${e.message}`);
- stopSession();
- }
- }
-
- function stopSession() {
- if (pcRef.current) {
- pcRef.current.close();
- pcRef.current = null;
- }
- if (streamRef.current) {
- streamRef.current.getTracks().forEach((t) => t.stop());
- streamRef.current = null;
- }
- if (animRef.current) {
- cancelAnimationFrame(animRef.current);
- animRef.current = null;
- }
- tokenRef.current = null;
- micRef.current = false;
- setConnected(false);
- setStatus("idle");
- setFlowStep(0);
- setTokenPreview("—");
- setIceState("—");
- setConnState("—");
- setDcState("—");
- setMicActive(false);
- setOfferActive(false);
- setAnswerActive(false);
- setBars(Array(28).fill(2));
- setAudioStatus("Start a session first");
- log("warn", "SESSION", "Session stopped");
- }
-
- function toggleMic() {
- if (!streamRef.current) {
- log("warn", "MIC", "No active session");
- return;
- }
- const next = !micRef.current;
- micRef.current = next;
- streamRef.current.getAudioTracks().forEach((t) => {
- t.enabled = next;
- });
- setMicActive(next);
- log("info", "MIC", next ? "Unmuted" : "Muted");
- }
-
- const f = (n) => flowStep >= n;
-
- return (
- <>
-
-
- {/* Toggle header */}
-
setOpen((o) => !o)}>
-
-
-
-
INTERACTIVE TESTER
-
Browser → LiteLLM → OpenAI · WebRTC
-
-
-
▼
-
-
- {open && (
-
- {/* Sidebar */}
-
-
-
Proxy Config
-
-
- setProxyUrl(e.target.value)}
- placeholder="http://localhost:4000"
- />
-
-
-
- setApiKey(e.target.value)}
- placeholder="sk-1234"
- />
-
-
-
- setModel(e.target.value)} />
-
-
-
-
-
-
-
Flow
-
-
Browser
-
→
-
LiteLLM
-
→
-
OpenAI
-
-
-
-
-
-
-
Controls
-
-
-
-
-
-
-
-
-
Session Info
-
- {[
- ["token", tokenPreview],
- ["ice", iceState],
- ["conn", connState],
- ["data ch.", dcState],
- ].map(([k, v]) => (
-
- {k}
- {v}
-
- ))}
-
-
-
-
- {/* Right panel */}
-
-
-
WEBRTC REALTIME TESTER
-
-
-
-
- {["logs", "sdp", "audio"].map((t) => (
-
setActiveTab(t)}>
- {t.toUpperCase()}
-
- ))}
-
-
- {/* Logs */}
-
-
- {entries.length === 0 ? (
-
-
📡
-
Hit "Start Session" to begin
-
- ) : (
- entries.map((e) => (
-
- {e.time}
- [{e.tag}]
- {e.msg}
-
- ))
- )}
-
-
-
- {/* SDP */}
-
-
- {/* Audio */}
-
-
-
- {bars.map((h, i) => (
-
- ))}
-
-
-
{audioStatus}
-
-
-
-
-
- )}
-
- >
- );
-}
diff --git a/ui/litellm-dashboard/src/components/agents.test.tsx b/ui/litellm-dashboard/src/components/agents.test.tsx
index 61d9c81b92f..21dc2cbf9fe 100644
--- a/ui/litellm-dashboard/src/components/agents.test.tsx
+++ b/ui/litellm-dashboard/src/components/agents.test.tsx
@@ -14,12 +14,6 @@ vi.mock("./agents/add_agent_form", () => ({
default: () => ,
}));
-vi.mock("./agents/agent_card_grid", () => ({
- default: ({ isAdmin }: { isAdmin: boolean }) => ,
-}));
-
-// Note: agents.tsx no longer uses AgentCardGrid — it renders a Table directly.
-
vi.mock("./agents/agent_info", () => ({
default: () => ,
}));
diff --git a/ui/litellm-dashboard/src/components/agents/agent_card.test.tsx b/ui/litellm-dashboard/src/components/agents/agent_card.test.tsx
deleted file mode 100644
index dd356aedd72..00000000000
--- a/ui/litellm-dashboard/src/components/agents/agent_card.test.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-import React from "react";
-import { describe, it, expect, vi } from "vitest";
-import { screen } from "@testing-library/react";
-import userEvent from "@testing-library/user-event";
-import { renderWithProviders } from "../../../tests/test-utils";
-import AgentCard from "./agent_card";
-import type { Agent } from "./types";
-
-const baseAgent: Agent = {
- agent_id: "agent-123",
- agent_name: "Test Agent",
- litellm_params: { model: "gpt-4" },
- agent_card_params: {
- description: "A test agent for unit testing",
- url: "https://agent.example.com",
- },
-};
-
-const defaultProps = {
- agent: baseAgent,
- onAgentClick: vi.fn(),
- accessToken: "token-123",
- isAdmin: false,
- onAgentUpdated: vi.fn(),
-};
-
-describe("AgentCard", () => {
- it("should render the agent name and description", () => {
- renderWithProviders();
-
- expect(screen.getByText("Test Agent")).toBeInTheDocument();
- expect(screen.getByText("A test agent for unit testing")).toBeInTheDocument();
- });
-
- it("should show 'No description' when agent has no description", () => {
- const agent = { ...baseAgent, agent_card_params: {} };
- renderWithProviders();
-
- expect(screen.getByText("No description")).toBeInTheDocument();
- });
-
- it("should show the agent URL when provided", () => {
- renderWithProviders();
-
- expect(screen.getByText("https://agent.example.com")).toBeInTheDocument();
- });
-
- it("should show 'Needs Setup' badge when agent has no key", () => {
- renderWithProviders();
-
- expect(screen.getByText("Needs Setup")).toBeInTheDocument();
- expect(screen.getByText("No key assigned")).toBeInTheDocument();
- });
-
- it("should show 'Active' badge and key info when agent has a key", () => {
- const keyInfo = { has_key: true, key_alias: "my-key" };
- renderWithProviders();
-
- expect(screen.getByText("Active")).toBeInTheDocument();
- expect(screen.getByText("my-key")).toBeInTheDocument();
- });
-
- it("should call onAgentClick when card is clicked", async () => {
- const user = userEvent.setup();
- const onAgentClick = vi.fn();
- renderWithProviders();
-
- await user.click(screen.getByText("Test Agent"));
-
- expect(onAgentClick).toHaveBeenCalledWith("agent-123");
- });
-
- it("should show delete button only for admins", () => {
- const onDeleteClick = vi.fn();
- const { unmount } = renderWithProviders(
- ,
- );
- expect(screen.queryByRole("button", { name: /delete/i })).not.toBeInTheDocument();
-
- unmount();
-
- renderWithProviders();
- expect(screen.getByRole("button", { name: /delete/i })).toBeInTheDocument();
- });
-
- it("should call onDeleteClick with agent id and name when delete is clicked", async () => {
- const user = userEvent.setup();
- const onDeleteClick = vi.fn();
- renderWithProviders();
-
- await user.click(screen.getByRole("button", { name: /delete/i }));
-
- expect(onDeleteClick).toHaveBeenCalledWith("agent-123", "Test Agent");
- });
-});
diff --git a/ui/litellm-dashboard/src/components/agents/agent_card.tsx b/ui/litellm-dashboard/src/components/agents/agent_card.tsx
deleted file mode 100644
index f865304534a..00000000000
--- a/ui/litellm-dashboard/src/components/agents/agent_card.tsx
+++ /dev/null
@@ -1,88 +0,0 @@
-import React from "react";
-import { Card, Badge, Tooltip, Button } from "antd";
-import { CopyOutlined, KeyOutlined, WarningOutlined, DeleteOutlined } from "@ant-design/icons";
-import { Agent, AgentKeyInfo } from "./types";
-
-interface AgentCardProps {
- agent: Agent;
- keyInfo?: AgentKeyInfo;
- onAgentClick: (agentId: string) => void;
- onDeleteClick?: (agentId: string, agentName: string) => void;
- accessToken: string | null;
- isAdmin: boolean;
- onAgentUpdated: () => void;
-}
-
-const AgentCard: React.FC = ({ agent, keyInfo, onAgentClick, onDeleteClick, isAdmin }) => {
- const description = agent.agent_card_params?.description || "No description";
- const url = agent.agent_card_params?.url;
- const hasKey = keyInfo?.has_key ?? false;
- const statusBadge = hasKey ? : ;
-
- const copyToClipboard = (e: React.MouseEvent, text: string) => {
- e.stopPropagation();
- navigator.clipboard.writeText(text);
- };
-
- return (
- onAgentClick(agent.agent_id)}
- >
-
-
-
- {agent.agent_name}
-
- copyToClipboard(e, agent.agent_id)}
- className="cursor-pointer text-gray-400 hover:text-blue-500 text-xs shrink-0"
- />
-
-
-
{statusBadge}
-
- {isAdmin && onDeleteClick && (
-
- }
- onClick={(e) => {
- e.stopPropagation();
- onDeleteClick(agent.agent_id, agent.agent_name);
- }}
- className="shrink-0 -mr-1"
- />
-
- )}
-
- {description}
- {url && (
-
- {url}
-
- )}
-
- {hasKey ? (
-
-
- {keyInfo?.key_alias || keyInfo?.token_prefix || "Key assigned"}
-
- ) : (
-
-
- No key assigned
-
- )}
-
-
- );
-};
-
-export default AgentCard;
diff --git a/ui/litellm-dashboard/src/components/agents/agent_card_grid.test.tsx b/ui/litellm-dashboard/src/components/agents/agent_card_grid.test.tsx
deleted file mode 100644
index d7d183a7d35..00000000000
--- a/ui/litellm-dashboard/src/components/agents/agent_card_grid.test.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import { renderWithProviders, screen } from "../../../tests/test-utils";
-import { vi } from "vitest";
-import AgentCardGrid from "./agent_card_grid";
-import type { Agent, AgentKeyInfo } from "./types";
-
-vi.mock("./agent_card", () => ({
- default: ({ agent, onAgentClick }: any) => (
- onAgentClick(agent.agent_id)}>
- {agent.agent_name}
-
- ),
-}));
-
-const mockAgents: Agent[] = [
- {
- agent_id: "agent-1",
- agent_name: "Test Agent 1",
- litellm_params: { model: "gpt-4" },
- agent_card_params: { description: "First agent" },
- },
- {
- agent_id: "agent-2",
- agent_name: "Test Agent 2",
- litellm_params: { model: "claude-3" },
- agent_card_params: { description: "Second agent" },
- },
-];
-
-const mockKeyInfoMap: Record = {
- "agent-1": { has_key: true, key_alias: "key-1" },
- "agent-2": { has_key: false },
-};
-
-const defaultProps = {
- agentsList: mockAgents,
- keyInfoMap: mockKeyInfoMap,
- isLoading: false,
- onDeleteClick: vi.fn(),
- accessToken: "test-token",
- onAgentUpdated: vi.fn(),
- isAdmin: true,
- onAgentClick: vi.fn(),
-};
-
-describe("AgentCardGrid", () => {
- it("should render", () => {
- renderWithProviders();
- expect(screen.getByText("Test Agent 1")).toBeInTheDocument();
- });
-
- it("should render all agent cards", () => {
- renderWithProviders();
- expect(screen.getByText("Test Agent 1")).toBeInTheDocument();
- expect(screen.getByText("Test Agent 2")).toBeInTheDocument();
- });
-
- it("should show loading skeletons when isLoading is true", () => {
- renderWithProviders();
- expect(screen.queryByText("Test Agent 1")).not.toBeInTheDocument();
- });
-
- it("should show admin empty state message when no agents and isAdmin", () => {
- renderWithProviders();
- expect(screen.getByText("No agents found. Create one to get started.")).toBeInTheDocument();
- });
-
- it("should show non-admin empty state message when no agents and not admin", () => {
- renderWithProviders();
- expect(screen.getByText("No agents found. Contact an admin to create agents.")).toBeInTheDocument();
- });
-
- it("should call onAgentClick when a card is clicked", async () => {
- const onAgentClick = vi.fn();
- renderWithProviders();
- const { default: userEvent } = await import("@testing-library/user-event");
- const user = userEvent.setup();
- await user.click(screen.getByTestId("agent-card-agent-1"));
- expect(onAgentClick).toHaveBeenCalledWith("agent-1");
- });
-});
diff --git a/ui/litellm-dashboard/src/components/agents/agent_card_grid.tsx b/ui/litellm-dashboard/src/components/agents/agent_card_grid.tsx
deleted file mode 100644
index 5e984cf220d..00000000000
--- a/ui/litellm-dashboard/src/components/agents/agent_card_grid.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import React from "react";
-import { Skeleton } from "antd";
-import AgentCard from "./agent_card";
-import { Agent, AgentKeyInfo } from "./types";
-
-interface AgentCardGridProps {
- agentsList: Agent[];
- keyInfoMap: Record;
- isLoading: boolean;
- onDeleteClick: (agentId: string, agentName: string) => void;
- accessToken: string | null;
- onAgentUpdated: () => void;
- isAdmin: boolean;
- onAgentClick: (agentId: string) => void;
-}
-
-const AgentCardGrid: React.FC = ({
- agentsList,
- keyInfoMap,
- isLoading,
- onDeleteClick,
- accessToken,
- onAgentUpdated,
- isAdmin,
- onAgentClick,
-}) => {
- if (isLoading) {
- return (
-
- {[1, 2, 3].map((i) => (
-
- ))}
-
- );
- }
-
- if (!agentsList || agentsList.length === 0) {
- return (
-
-
- {isAdmin
- ? "No agents found. Create one to get started."
- : "No agents found. Contact an admin to create agents."}
-
-
- );
- }
-
- return (
-
- {agentsList.map((agent) => (
-
- ))}
-
- );
-};
-
-export default AgentCardGrid;
diff --git a/ui/litellm-dashboard/src/components/view_logs/ErrorViewer.test.tsx b/ui/litellm-dashboard/src/components/view_logs/ErrorViewer.test.tsx
deleted file mode 100644
index 2e50cd3991a..00000000000
--- a/ui/litellm-dashboard/src/components/view_logs/ErrorViewer.test.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-import { render, screen } from "@testing-library/react";
-import { describe, expect, it, vi } from "vitest";
-import userEvent from "@testing-library/user-event";
-import { ErrorViewer } from "./ErrorViewer";
-
-const basicError = {
- error_class: "NotFoundError",
- error_message: "Model gpt-5 not found",
-};
-
-const errorWithTraceback = {
- error_class: "AuthenticationError",
- error_message: "Invalid API key",
- traceback: `Traceback (most recent call last):
- File "/app/main.py", line 42, in handle_request
- result = await client.chat(model="gpt-4")
- File "/app/llms/openai.py", line 100, in chat
- response = self._make_request(payload)
- File "/app/llms/base.py", line 55, in _make_request
- raise AuthenticationError("Invalid API key")`,
-};
-
-describe("ErrorViewer", () => {
- it("should render error type and message", () => {
- render();
- expect(screen.getByText("NotFoundError")).toBeInTheDocument();
- expect(screen.getByText("Model gpt-5 not found")).toBeInTheDocument();
- });
-
- it("should show 'Unknown Error' when error_class is missing", () => {
- render();
- expect(screen.getByText("Unknown Error")).toBeInTheDocument();
- });
-
- it("should show 'Unknown error occurred' when error_message is missing", () => {
- render();
- expect(screen.getByText("Unknown error occurred")).toBeInTheDocument();
- });
-
- it("should render traceback frames when traceback is present", () => {
- render();
- expect(screen.getByText("Traceback")).toBeInTheDocument();
- expect(screen.getByText("main.py")).toBeInTheDocument();
- expect(screen.getByText("openai.py")).toBeInTheDocument();
- expect(screen.getByText("base.py")).toBeInTheDocument();
- });
-
- it("should expand a frame when clicked", async () => {
- const user = userEvent.setup();
- render();
-
- await user.click(screen.getByText("main.py"));
-
- expect(screen.getByText('result = await client.chat(model="gpt-4")')).toBeInTheDocument();
- });
-
- it("should expand all frames when 'Expand All' is clicked", async () => {
- const user = userEvent.setup();
- render();
-
- await user.click(screen.getByText("Expand All"));
-
- expect(screen.getByText("Collapse All")).toBeInTheDocument();
- });
-
- it("should copy traceback to clipboard when copy button is clicked", async () => {
- const user = userEvent.setup();
- const mockWriteText = vi.fn().mockResolvedValue(undefined);
- Object.defineProperty(navigator, "clipboard", {
- value: { writeText: mockWriteText },
- writable: true,
- configurable: true,
- });
-
- render();
-
- await user.click(screen.getByTitle("Copy traceback"));
- expect(mockWriteText).toHaveBeenCalledWith(errorWithTraceback.traceback);
- });
-
- it("should not render traceback section when traceback is absent", () => {
- render();
- expect(screen.queryByText("Traceback")).not.toBeInTheDocument();
- });
-});
diff --git a/ui/litellm-dashboard/src/components/view_logs/ErrorViewer.tsx b/ui/litellm-dashboard/src/components/view_logs/ErrorViewer.tsx
deleted file mode 100644
index e0a34da6690..00000000000
--- a/ui/litellm-dashboard/src/components/view_logs/ErrorViewer.tsx
+++ /dev/null
@@ -1,180 +0,0 @@
-import React from "react";
-
-interface ErrorViewerProps {
- errorInfo: {
- error_class?: string;
- error_message?: string;
- traceback?: string;
- llm_provider?: string;
- error_code?: string | number;
- };
-}
-
-export const ErrorViewer: React.FC = ({ errorInfo }) => {
- const [expandedFrames, setExpandedFrames] = React.useState<{ [key: number]: boolean }>({});
- const [allExpanded, setAllExpanded] = React.useState(false);
-
- // Toggle individual frame
- const toggleFrame = (index: number) => {
- setExpandedFrames((prev) => ({
- ...prev,
- [index]: !prev[index],
- }));
- };
-
- // Toggle all frames
- const toggleAllFrames = () => {
- const newState = !allExpanded;
- setAllExpanded(newState);
-
- if (tracebackFrames.length > 0) {
- const newExpandedState: { [key: number]: boolean } = {};
- tracebackFrames.forEach((_, idx) => {
- newExpandedState[idx] = newState;
- });
- setExpandedFrames(newExpandedState);
- }
- };
-
- // Parse traceback into frames
- const parseTraceback = (traceback: string) => {
- if (!traceback) return [];
-
- // Extract file paths, line numbers and code from traceback
- const fileLineRegex = /File "([^"]+)", line (\d+)/g;
- const matches = Array.from(traceback.matchAll(fileLineRegex));
-
- // Create simplified frames
- return matches.map((match) => {
- const filePath = match[1];
- const lineNumber = match[2];
- const fileName = filePath.split("/").pop() || filePath;
-
- // Extract the context around this frame
- const matchIndex = match.index || 0;
- const nextMatchIndex = traceback.indexOf('File "', matchIndex + 1);
- const frameContent =
- nextMatchIndex > -1
- ? traceback.substring(matchIndex, nextMatchIndex).trim()
- : traceback.substring(matchIndex).trim();
-
- // Try to extract the code line
- const lines = frameContent.split("\n");
- let code = "";
- if (lines.length > 1) {
- code = lines[lines.length - 1].trim();
- }
-
- return {
- filePath,
- fileName,
- lineNumber,
- code,
- inFunction: frameContent.includes(" in ") ? frameContent.split(" in ")[1].split("\n")[0] : "",
- };
- });
- };
-
- const tracebackFrames = errorInfo.traceback ? parseTraceback(errorInfo.traceback) : [];
-
- return (
-
-
-
-
- Error Details
-
-
-
-
-
-
- Type:
- {errorInfo.error_class || "Unknown Error"}
-
-
- Message:
-
- {errorInfo.error_message || "Unknown error occurred"}
-
-
-
-
- {errorInfo.traceback && (
-
-
-
Traceback
-
-
-
-
-
-
-
- {tracebackFrames.map((frame, index) => (
-
-
toggleFrame(index)}
- >
-
- {frame.lineNumber}
- {frame.fileName}
- in
- {frame.inFunction || frame.fileName}
-
-
-
- {(expandedFrames[index] || false) && frame.code && (
-
- {frame.code}
-
- )}
-
- ))}
-
-
- )}
-
-
- );
-};
diff --git a/ui/litellm-dashboard/src/components/view_logs/RequestResponsePanel.test.tsx b/ui/litellm-dashboard/src/components/view_logs/RequestResponsePanel.test.tsx
deleted file mode 100644
index 777676b18d8..00000000000
--- a/ui/litellm-dashboard/src/components/view_logs/RequestResponsePanel.test.tsx
+++ /dev/null
@@ -1,267 +0,0 @@
-import { render, screen, act } from "@testing-library/react";
-import { describe, expect, it, vi, beforeEach } from "vitest";
-import userEvent from "@testing-library/user-event";
-import { RequestResponsePanel } from "./RequestResponsePanel";
-import type { LogEntry } from "./columns";
-import NotificationsManager from "../molecules/notifications_manager";
-
-const mockNotificationsManager = vi.mocked(NotificationsManager);
-
-const baseLogEntry: LogEntry = {
- request_id: "chatcmpl-test-id",
- api_key: "api-key",
- team_id: "team-id",
- model: "gpt-4",
- model_id: "gpt-4",
- call_type: "chat",
- spend: 0,
- total_tokens: 0,
- prompt_tokens: 0,
- completion_tokens: 0,
- startTime: "2025-11-14T00:00:00Z",
- endTime: "2025-11-14T00:00:00Z",
- cache_hit: "miss",
- request_duration_ms: 1000,
- messages: [{ role: "user", content: "hello" }],
- response: { status: "ok" },
- metadata: {
- status: "success",
- additional_usage_values: {
- cache_read_input_tokens: 0,
- cache_creation_input_tokens: 0,
- },
- },
- request_tags: {},
- custom_llm_provider: "openai",
- api_base: "https://api.example.com",
-};
-
-describe("RequestResponsePanel", () => {
- beforeEach(() => {
- vi.clearAllMocks();
- Object.defineProperty(navigator, "clipboard", {
- value: {
- writeText: vi.fn().mockResolvedValue(undefined),
- },
- writable: true,
- configurable: true,
- });
- Object.defineProperty(window, "isSecureContext", {
- value: true,
- writable: true,
- configurable: true,
- });
- });
-
- it("should render the component with request and response panels", () => {
- const mockGetRawRequest = vi.fn().mockReturnValue({ test: "request" });
- const mockFormattedResponse = vi.fn().mockReturnValue({ test: "response" });
-
- render(
- ,
- );
-
- expect(screen.getByText("Request")).toBeInTheDocument();
- expect(screen.getByText("Response")).toBeInTheDocument();
- });
-
- it("should copy request to clipboard when copy button is clicked", async () => {
- const user = userEvent.setup();
- const mockGetRawRequest = vi.fn().mockReturnValue({ test: "request data" });
- const mockFormattedResponse = vi.fn().mockReturnValue({ test: "response" });
- const mockWriteText = vi.fn().mockResolvedValue(undefined);
-
- if (navigator.clipboard) {
- vi.spyOn(navigator.clipboard, "writeText").mockImplementation(mockWriteText);
- } else {
- Object.defineProperty(navigator, "clipboard", {
- value: {
- writeText: mockWriteText,
- },
- writable: true,
- configurable: true,
- });
- }
-
- render(
- ,
- );
-
- const copyButtons = screen.getAllByRole("button");
- const copyRequestButton = copyButtons.find((button) => button.getAttribute("title") === "Copy request");
-
- expect(copyRequestButton).toBeInTheDocument();
-
- await act(async () => {
- await user.click(copyRequestButton!);
- });
-
- expect(mockGetRawRequest).toHaveBeenCalled();
- expect(mockWriteText).toHaveBeenCalledWith(JSON.stringify({ test: "request data" }, null, 2));
- expect(mockNotificationsManager.success).toHaveBeenCalledWith("Request copied to clipboard");
- });
-
- it("should copy response to clipboard when copy button is clicked", async () => {
- const user = userEvent.setup();
- const mockGetRawRequest = vi.fn().mockReturnValue({ test: "request" });
- const mockFormattedResponse = vi.fn().mockReturnValue({ test: "response data" });
- const mockWriteText = vi.fn().mockResolvedValue(undefined);
-
- if (navigator.clipboard) {
- vi.spyOn(navigator.clipboard, "writeText").mockImplementation(mockWriteText);
- } else {
- Object.defineProperty(navigator, "clipboard", {
- value: {
- writeText: mockWriteText,
- },
- writable: true,
- configurable: true,
- });
- }
-
- render(
- ,
- );
-
- const copyButtons = screen.getAllByRole("button");
- const copyResponseButton = copyButtons.find((button) => button.getAttribute("title") === "Copy response");
-
- expect(copyResponseButton).toBeInTheDocument();
- expect(copyResponseButton).not.toBeDisabled();
-
- await act(async () => {
- await user.click(copyResponseButton!);
- });
-
- expect(mockFormattedResponse).toHaveBeenCalled();
- expect(mockWriteText).toHaveBeenCalledWith(JSON.stringify({ test: "response data" }, null, 2));
- expect(mockNotificationsManager.success).toHaveBeenCalledWith("Response copied to clipboard");
- });
-
- it("should call formattedResponse for the response panel and not getRawRequest", () => {
- const mockGetRawRequest = vi.fn().mockReturnValue({ requestData: "this should not appear in response" });
- const mockFormattedResponse = vi.fn().mockReturnValue({ responseData: "this should appear in response" });
-
- render(
- ,
- );
-
- expect(mockFormattedResponse).toHaveBeenCalled();
- expect(mockGetRawRequest).toHaveBeenCalled();
-
- const formattedResponseCallCount = mockFormattedResponse.mock.calls.length;
- expect(formattedResponseCallCount).toBeGreaterThanOrEqual(1);
-
- const responseData = mockFormattedResponse.mock.results[0].value;
- expect(responseData).toEqual({ responseData: "this should appear in response" });
- expect(responseData).not.toEqual({ requestData: "this should not appear in response" });
- });
-
- it("should show error response data when hasError is true and hasResponse is false", () => {
- const failedLogEntry: LogEntry = {
- ...baseLogEntry,
- messages: [],
- response: {},
- metadata: {
- status: "failure",
- error_information: {
- error_message: "Model not found",
- error_class: "NotFoundError",
- error_code: 404,
- },
- additional_usage_values: {
- cache_read_input_tokens: 0,
- cache_creation_input_tokens: 0,
- },
- },
- };
- const errorResponse = { error: { message: "Model not found", type: "NotFoundError", code: 404, param: null } };
- const mockGetRawRequest = vi.fn().mockReturnValue({ messages: [] });
- const mockFormattedResponse = vi.fn().mockReturnValue(errorResponse);
- render(
- ,
- );
- expect(screen.queryByText("Response data not available")).not.toBeInTheDocument();
- expect(mockFormattedResponse).toHaveBeenCalled();
- const copyButtons = screen.getAllByRole("button");
- const copyResponseButton = copyButtons.find((button) => button.getAttribute("title") === "Copy response");
- expect(copyResponseButton).not.toBeDisabled();
- });
-
- it("should show Response data not available when hasResponse and hasError are both false", () => {
- const mockGetRawRequest = vi.fn().mockReturnValue({ messages: [] });
- const mockFormattedResponse = vi.fn().mockReturnValue({});
- render(
- ,
- );
- expect(screen.getByText("Response data not available")).toBeInTheDocument();
- });
-
- it("should show error code in response header when hasError is true", () => {
- const errorInfo = { error_message: "Rate limit exceeded", error_class: "RateLimitError", error_code: 429 };
- const mockGetRawRequest = vi.fn().mockReturnValue({ messages: [] });
- const mockFormattedResponse = vi
- .fn()
- .mockReturnValue({ error: { message: "Rate limit exceeded", type: "RateLimitError", code: 429, param: null } });
- render(
- ,
- );
- expect(screen.getByText(/HTTP code 429/)).toBeInTheDocument();
- });
-});
diff --git a/ui/litellm-dashboard/src/components/view_logs/RequestResponsePanel.tsx b/ui/litellm-dashboard/src/components/view_logs/RequestResponsePanel.tsx
deleted file mode 100644
index da9323f8172..00000000000
--- a/ui/litellm-dashboard/src/components/view_logs/RequestResponsePanel.tsx
+++ /dev/null
@@ -1,146 +0,0 @@
-import { LogEntry } from "./columns";
-import NotificationsManager from "../molecules/notifications_manager";
-import { JsonView, defaultStyles } from "react-json-view-lite";
-import "react-json-view-lite/dist/index.css";
-
-interface RequestResponsePanelProps {
- row: {
- original: LogEntry;
- };
- hasMessages: string | boolean;
- hasResponse: string | boolean;
- hasError: boolean;
- errorInfo: any;
- getRawRequest: () => any;
- formattedResponse: () => any;
-}
-
-export function RequestResponsePanel({
- row,
- hasMessages,
- hasResponse,
- hasError,
- errorInfo,
- getRawRequest,
- formattedResponse,
-}: RequestResponsePanelProps) {
- const copyToClipboard = async (text: string) => {
- try {
- // Try modern clipboard API first
- if (navigator.clipboard && window.isSecureContext) {
- await navigator.clipboard.writeText(text);
- return true;
- } else {
- // Fallback for non-secure contexts (like 0.0.0.0)
- const textArea = document.createElement("textarea");
- textArea.value = text;
- textArea.style.position = "fixed";
- textArea.style.opacity = "0";
- document.body.appendChild(textArea);
- textArea.focus();
- textArea.select();
-
- const successful = document.execCommand("copy");
- document.body.removeChild(textArea);
-
- if (!successful) {
- throw new Error("execCommand failed");
- }
- return true;
- }
- } catch (error) {
- console.error("Copy failed:", error);
- return false;
- }
- };
-
- const handleCopyRequest = async () => {
- const success = await copyToClipboard(JSON.stringify(getRawRequest(), null, 2));
- if (success) {
- NotificationsManager.success("Request copied to clipboard");
- } else {
- NotificationsManager.fromBackend("Failed to copy request");
- }
- };
-
- const handleCopyResponse = async () => {
- const success = await copyToClipboard(JSON.stringify(formattedResponse(), null, 2));
- if (success) {
- NotificationsManager.success("Response copied to clipboard");
- } else {
- NotificationsManager.fromBackend("Failed to copy response");
- }
- };
-
- return (
-
- {/* Request Side */}
-
-
- {/* Response Side */}
-
-
-
- Response
- {hasError && • HTTP code {errorInfo?.error_code || 400}}
-
-
-
-
- {hasResponse || hasError ? (
-
-
-
- ) : (
-
Response data not available
- )}
-
-
-
- );
-}