+
Spend is bucketed by UTC day
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/autoRouterBenchmarks.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/autoRouterBenchmarks.test.ts
new file mode 100644
index 00000000000..57c059b5524
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/autoRouterBenchmarks.test.ts
@@ -0,0 +1,182 @@
+import { describe, expect, it } from "vitest";
+
+import {
+ ALL_ROUTERS,
+ bucketRows,
+ bucketTurnsTotal,
+ durationLabel,
+ expiredMissShare,
+ groupKey,
+ groupLabel,
+ pctLabel,
+ viewFor,
+ windowFor,
+ type AutoRouterBenchmarkGroup,
+ type AutoRouterBenchmarksResponse,
+ type AutoRouterCacheStats,
+} from "./autoRouterBenchmarks";
+
+const cache = (overrides: Partial
= {}): AutoRouterCacheStats => ({
+ coverage_pct: 99.6,
+ hit_rate_pct: 93.3,
+ same_model: { turns: 400, hits: 391, hit_rate_pct: 97.7 },
+ first_visit: { turns: 37, hits: 9, hit_rate_pct: 24.3 },
+ return_to_tier: { turns: 381, hits: 311, hit_rate_pct: 81.6 },
+ unordered_turns: 0,
+ return_misses_expired: 19,
+ return_misses_within_ttl: 51,
+ return_misses_unknown: 0,
+ ttl_5m_turns: 0,
+ ttl_1h_turns: 818,
+ ...overrides,
+});
+
+const totals = (overrides: Partial = {}) => ({
+ sessions: 94,
+ turns: 3073,
+ avg_turns_per_session: 32.7,
+ avg_session_seconds: 7560,
+ avg_tokens_per_session: 5_300_000,
+ spend: 359.86,
+ saved_spend: 2174.59,
+ baseline_spend: 2534.45,
+ saved_pct: 85.8,
+ saved_per_session: 23.13,
+ cache: cache(),
+ ...overrides,
+});
+
+const group = (overrides: Partial = {}): AutoRouterBenchmarkGroup => ({
+ router_name: "claude-auto",
+ router_type: "complexity",
+ ...totals(),
+ ...overrides,
+});
+
+const response = (groups: AutoRouterBenchmarkGroup[]): AutoRouterBenchmarksResponse => ({
+ start_date: "2026-07-06",
+ end_date: "2026-08-05",
+ routers_in_scope: groups.length,
+ totals: totals(),
+ groups,
+});
+
+describe("viewFor", () => {
+ it("maps the all-routers selection to the server totals, never a client sum", () => {
+ const data = response([group(), group({ router_name: "gpt-auto", sessions: 7 })]);
+ const view = viewFor(data, ALL_ROUTERS);
+ expect(view.stats).toBe(data.totals);
+ expect(view.label).toBe("All auto-routers");
+ });
+
+ it("maps a selected router to that group's slice with a scope of one", () => {
+ const other = group({ router_name: "gpt-auto", sessions: 7, saved_spend: 12.5 });
+ const data = response([group(), other]);
+ const view = viewFor(data, groupKey(other));
+ expect(view.stats).toBe(other);
+ expect(view.label).toBe("gpt-auto");
+ });
+
+ it("falls back to the all-routers view when the selected key no longer exists", () => {
+ const data = response([group()]);
+ const view = viewFor(data, "vanished complexity");
+ expect(view.stats).toBe(data.totals);
+ expect(view.label).toBe("All auto-routers");
+ });
+
+ it("distinguishes two groups sharing an alias by their router type", () => {
+ const a = group({ router_type: "complexity" });
+ const b = group({ router_type: "adaptive" });
+ const data = response([a, b]);
+ expect(groupKey(a)).not.toBe(groupKey(b));
+ expect(viewFor(data, groupKey(b)).stats).toBe(b);
+ expect(viewFor(data, groupKey(b)).label).toBe("claude-auto (adaptive)");
+ });
+});
+
+describe("groupLabel", () => {
+ it("uses the bare alias when it is unique", () => {
+ const groups = [group(), group({ router_name: "gpt-auto" })];
+ expect(groupLabel(groups[0], groups)).toBe("claude-auto");
+ });
+
+ it("appends the router type only when the alias is duplicated", () => {
+ const groups = [group({ router_type: "complexity" }), group({ router_type: "adaptive" })];
+ expect(groupLabel(groups[0], groups)).toBe("claude-auto (complexity)");
+ expect(groupLabel(groups[1], groups)).toBe("claude-auto (adaptive)");
+ });
+});
+
+describe("bucketRows", () => {
+ it("keeps the three buckets summing to the bucketed turn total", () => {
+ const stats = cache();
+ const rows = bucketRows(stats);
+ expect(rows.map((r) => r.turns)).toEqual([400, 37, 381]);
+ expect(bucketTurnsTotal(stats)).toBe(818);
+ });
+
+ it("renders the server's per-bucket rates as-is", () => {
+ expect(bucketRows(cache()).map((r) => r.hitRatePct)).toEqual([97.7, 24.3, 81.6]);
+ });
+
+ it("derives each bucket's share of the measured turns", () => {
+ expect(bucketRows(cache()).map((r) => r.sharePct)).toEqual([49, 5, 47]);
+ });
+
+ it("reports zero shares instead of dividing by zero when nothing was bucketed", () => {
+ const empty = { turns: 0, hits: 0, hit_rate_pct: 0 };
+ const rows = bucketRows(cache({ same_model: empty, first_visit: empty, return_to_tier: empty }));
+ expect(rows.map((r) => r.sharePct)).toEqual([0, 0, 0]);
+ });
+});
+
+describe("expiredMissShare", () => {
+ it("computes the expired share over every measured turn, not just return-to-tier misses", () => {
+ expect(expiredMissShare(cache())).toBeCloseTo((100 * 19) / 818);
+ });
+
+ it("is zero, not absent, when every return turn hit", () => {
+ expect(
+ expiredMissShare(cache({ return_to_tier: { turns: 10, hits: 10, hit_rate_pct: 100 }, return_misses_expired: 0 })),
+ ).toBe(0);
+ });
+
+ it("is absent only when no turns were measured at all", () => {
+ const empty = { turns: 0, hits: 0, hit_rate_pct: 0 };
+ const nothingMeasured = {
+ same_model: empty,
+ first_visit: empty,
+ return_to_tier: empty,
+ return_misses_expired: 0,
+ };
+ expect(expiredMissShare(cache(nothingMeasured))).toBeNull();
+ });
+});
+
+describe("windowFor", () => {
+ const noon = new Date("2026-08-05T12:00:00Z");
+
+ it("derives each picker range as UTC calendar days ending today", () => {
+ expect(windowFor("30d", noon)).toEqual({ start_date: "2026-07-06", end_date: "2026-08-05" });
+ expect(windowFor("7d", noon)).toEqual({ start_date: "2026-07-29", end_date: "2026-08-05" });
+ expect(windowFor("24h", noon)).toEqual({ start_date: "2026-08-04", end_date: "2026-08-05" });
+ });
+
+ it("uses UTC days, not the local calendar", () => {
+ const lateEvening = new Date("2026-08-05T23:30:00-05:00");
+ expect(windowFor("24h", lateEvening)).toEqual({ start_date: "2026-08-05", end_date: "2026-08-06" });
+ });
+});
+
+describe("formatting", () => {
+ it("renders session length in the largest sensible unit", () => {
+ expect(durationLabel(42)).toBe("42s");
+ expect(durationLabel(150)).toBe("2.5m");
+ expect(durationLabel(7560)).toBe("2.1h");
+ });
+
+ it("renders percentages at the requested precision", () => {
+ expect(pctLabel(93.3)).toBe("93.3%");
+ expect(pctLabel(85.8, 0)).toBe("86%");
+ });
+});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/autoRouterBenchmarks.ts b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/autoRouterBenchmarks.ts
new file mode 100644
index 00000000000..00793548278
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/autoRouterBenchmarks.ts
@@ -0,0 +1,105 @@
+import type { components } from "@/lib/http/schema";
+
+export type AutoRouterBenchmarksResponse = components["schemas"]["AutoRouterBenchmarksResponse"];
+export type AutoRouterBenchmarkTotals = components["schemas"]["AutoRouterBenchmarkTotals"];
+export type AutoRouterBenchmarkGroup = components["schemas"]["AutoRouterBenchmarkGroup"];
+export type AutoRouterCacheStats = components["schemas"]["AutoRouterCacheStats"];
+
+export const ALL_ROUTERS = "__all__";
+
+export type BenchmarkWindow = "30d" | "7d" | "24h";
+
+const WINDOW_DAYS: Record = { "30d": 30, "7d": 7, "24h": 1 };
+
+export const WINDOW_LABELS: Record = {
+ "30d": "Last 30 days",
+ "7d": "Last 7 days",
+ "24h": "Last 24 hours",
+};
+
+export const windowFor = (range: BenchmarkWindow, now: Date): { start_date: string; end_date: string } => ({
+ start_date: new Date(now.getTime() - WINDOW_DAYS[range] * 24 * 60 * 60 * 1000).toISOString().slice(0, 10),
+ end_date: now.toISOString().slice(0, 10),
+});
+
+export interface BenchmarkView {
+ label: string;
+ stats: AutoRouterBenchmarkTotals;
+}
+
+export const groupKey = (group: AutoRouterBenchmarkGroup): string => `${group.router_name} ${group.router_type}`;
+
+export const groupLabel = (group: AutoRouterBenchmarkGroup, groups: readonly AutoRouterBenchmarkGroup[]): string => {
+ const duplicated = groups.some((g) => g !== group && g.router_name === group.router_name);
+ return duplicated ? `${group.router_name} (${group.router_type})` : group.router_name;
+};
+
+export const viewFor = (data: AutoRouterBenchmarksResponse, selectedKey: string): BenchmarkView => {
+ const group = data.groups.find((g) => groupKey(g) === selectedKey);
+ if (selectedKey === ALL_ROUTERS || !group) {
+ return { label: "All auto-routers", stats: data.totals };
+ }
+ return { label: groupLabel(group, data.groups), stats: group };
+};
+
+export interface BucketRow {
+ key: "same_model" | "first_visit" | "return_to_tier";
+ label: string;
+ sublabel: string;
+ turns: number;
+ sharePct: number;
+ hitRatePct: number;
+ fill: string;
+}
+
+export const bucketTurnsTotal = (cache: AutoRouterCacheStats): number =>
+ cache.same_model.turns + cache.first_visit.turns + cache.return_to_tier.turns;
+
+const sharePctOf = (turns: number, total: number): number => (total > 0 ? Math.round((100 * turns) / total) : 0);
+
+export const bucketRows = (cache: AutoRouterCacheStats): BucketRow[] => {
+ const total = bucketTurnsTotal(cache);
+ return [
+ {
+ key: "same_model",
+ label: "Same model",
+ sublabel: "previous turn → same tier",
+ turns: cache.same_model.turns,
+ sharePct: sharePctOf(cache.same_model.turns, total),
+ hitRatePct: cache.same_model.hit_rate_pct,
+ fill: "bg-foreground",
+ },
+ {
+ key: "first_visit",
+ label: "First visit",
+ sublabel: "previous turn → a tier not used yet",
+ turns: cache.first_visit.turns,
+ sharePct: sharePctOf(cache.first_visit.turns, total),
+ hitRatePct: cache.first_visit.hit_rate_pct,
+ fill: "bg-foreground/30",
+ },
+ {
+ key: "return_to_tier",
+ label: "Return to tier",
+ sublabel: "previous turn → a tier used earlier",
+ turns: cache.return_to_tier.turns,
+ sharePct: sharePctOf(cache.return_to_tier.turns, total),
+ hitRatePct: cache.return_to_tier.hit_rate_pct,
+ fill: "bg-foreground/60",
+ },
+ ];
+};
+
+export const expiredMissShare = (cache: AutoRouterCacheStats): number | null => {
+ const total = bucketTurnsTotal(cache);
+ if (total <= 0) return null;
+ return (100 * cache.return_misses_expired) / total;
+};
+
+export const pctLabel = (value: number, digits: number = 1): string => `${value.toFixed(digits)}%`;
+
+export const durationLabel = (seconds: number): string => {
+ if (seconds < 60) return `${Math.round(seconds)}s`;
+ if (seconds < 3600) return `${(seconds / 60).toFixed(1)}m`;
+ return `${(seconds / 3600).toFixed(1)}h`;
+};
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useAutoRouterBenchmarks.ts b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useAutoRouterBenchmarks.ts
new file mode 100644
index 00000000000..ab87c5ef430
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useAutoRouterBenchmarks.ts
@@ -0,0 +1,11 @@
+import { $api } from "@/lib/http/api";
+
+import { windowFor, type BenchmarkWindow } from "./autoRouterBenchmarks";
+
+export const useAutoRouterBenchmarks = (accessToken: string | null, range: BenchmarkWindow) =>
+ $api.useQuery(
+ "get",
+ "/auto_router/benchmarks",
+ { params: { query: windowFor(range, new Date()) } },
+ { enabled: Boolean(accessToken), retry: false },
+ );
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.test.tsx
index 9fd27d80c37..e26a3629e8c 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.test.tsx
@@ -22,13 +22,13 @@ describe("useDailyActivityRange", () => {
it("queries every user's activity for an admin", () => {
renderHook(() => useDailyActivityRange("test-token", "u1", "proxy_admin"));
- expect(argsOfLastCall()).toEqual(["test-token", expect.any(Date), expect.any(Date), null]);
+ expect(argsOfLastCall()).toEqual(["test-token", expect.any(Date), expect.any(Date), null, true]);
});
it("scopes the query to the caller for a non-admin", () => {
renderHook(() => useDailyActivityRange("test-token", "u1", "internal_user"));
- expect(argsOfLastCall()).toEqual(["test-token", expect.any(Date), expect.any(Date), "u1"]);
+ expect(argsOfLastCall()).toEqual(["test-token", expect.any(Date), expect.any(Date), "u1", true]);
});
it("stays disabled until an access token is available", () => {
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.ts b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.ts
index 1c3f706726e..3a2a38c5955 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.ts
+++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.ts
@@ -35,7 +35,7 @@ export const useDailyActivityRange = (
const { data, loading, isFetchingMore } = usePaginatedDailyActivity({
fetchFn: userDailyActivityCall,
- args: [accessToken, startTime, endTime, effectiveUserId],
+ args: [accessToken, startTime, endTime, effectiveUserId, true],
enabled: !!accessToken && !!startTime && !!endTime,
});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/pricing_calculator/multi_cost_results.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/pricing_calculator/multi_cost_results.test.tsx
index 6dc9309b5e3..04ef60469f0 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/pricing_calculator/multi_cost_results.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/pricing_calculator/multi_cost_results.test.tsx
@@ -1,6 +1,6 @@
import React from "react";
import { describe, it, expect, vi, beforeEach } from "vitest";
-import { screen, within } from "@testing-library/react";
+import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "../../../../../../tests/test-utils";
import MultiCostResults from "./multi_cost_results";
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/pricing_calculator/multi_export_dropdown.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/pricing_calculator/multi_export_dropdown.test.tsx
index 02940dd1325..e40fe7dbbca 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/pricing_calculator/multi_export_dropdown.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/pricing_calculator/multi_export_dropdown.test.tsx
@@ -1,5 +1,5 @@
import React from "react";
-import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import { describe, it, expect, vi, beforeEach } from "vitest";
import { screen, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "../../../../../../tests/test-utils";
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/provider_discount_table.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/provider_discount_table.test.tsx
index 2e8dbb429f0..f9a0a40f07d 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/provider_discount_table.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/provider_discount_table.test.tsx
@@ -1,6 +1,6 @@
import React from "react";
import { describe, it, expect, vi, beforeEach } from "vitest";
-import { screen, within } from "@testing-library/react";
+import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "../../../../../tests/test-utils";
import ProviderDiscountTable from "./provider_discount_table";
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/provider_margin_table.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/provider_margin_table.tsx
index 9d11e13fb52..75939a23751 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/provider_margin_table.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/provider_margin_table.tsx
@@ -69,14 +69,6 @@ const ProviderMarginTable: React.FC = ({
setEditFixedAmount("");
};
- const handleKeyDown = (e: React.KeyboardEvent, provider: string) => {
- if (e.key === "Enter") {
- handleSaveEdit(provider);
- } else if (e.key === "Escape") {
- handleCancelEdit();
- }
- };
-
const formatMargin = (margin: number | { percentage?: number; fixed_amount?: number }): string => {
if (typeof margin === "number") {
return `${(margin * 100).toFixed(1)}%`;
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailDetail.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailDetail.tsx
index 1d959ccca95..2e007c06744 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailDetail.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailDetail.tsx
@@ -25,7 +25,7 @@ const statusColors: Record =
export function GuardrailDetail({ guardrailId, onBack, accessToken = null, startDate, endDate }: GuardrailDetailProps) {
const [activeTab, setActiveTab] = useState("overview");
const [evaluationModalOpen, setEvaluationModalOpen] = useState(false);
- const [logsPage, setLogsPage] = useState(1);
+ const [logsPage] = useState(1);
const logsPageSize = 50;
const {
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/GuardrailTestResults.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/GuardrailTestResults.tsx
index 3c974de3632..10ce709244b 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/GuardrailTestResults.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/GuardrailTestResults.tsx
@@ -1,11 +1,8 @@
import React, { useState } from "react";
import { Button, Card } from "@tremor/react";
-import { Typography } from "antd";
import { CopyOutlined, CheckCircleOutlined, ClockCircleOutlined, DownOutlined, RightOutlined } from "@ant-design/icons";
import NotificationsManager from "@/components/molecules/notifications_manager";
-const { Text } = Typography;
-
interface TestResult {
guardrailName: string;
response_text: string;
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/add_guardrail_form.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/add_guardrail_form.tsx
index 251ce631beb..5f15eaceb28 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/add_guardrail_form.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/add_guardrail_form.tsx
@@ -1,4 +1,4 @@
-import { Form, Input, Modal, Select, Tag, Typography, Button } from "antd";
+import { Form, Input, Modal, Select, Tag, Button } from "antd";
import React, { useEffect, useMemo, useState } from "react";
import NotificationsManager from "@/components/molecules/notifications_manager";
import {
@@ -30,7 +30,6 @@ import LLMJudgeFields from "./llm_judge/LLMJudgeFields";
import PiiConfiguration from "./pii_configuration";
import ToolPermissionRulesEditor, { ToolPermissionConfig } from "./tool_permission/ToolPermissionRulesEditor";
-const { Title, Text, Link } = Typography;
const { Option } = Select;
// Define human-friendly descriptions for each mode
@@ -163,11 +162,6 @@ const AddGuardrailForm: React.FC = ({ visible, onClose, a
const [currentStep, setCurrentStep] = useState(0);
const [providerParams, setProviderParams] = useState(null);
- // Azure Text Moderation state
- const [selectedCategories, setSelectedCategories] = useState([]);
- const [globalSeverityThreshold, setGlobalSeverityThreshold] = useState(2);
- const [categorySpecificThresholds, setCategorySpecificThresholds] = useState<{ [key: string]: number }>({});
-
// Content Filter state
const [selectedPatterns, setSelectedPatterns] = useState([]);
const [blockedWords, setBlockedWords] = useState([]);
@@ -297,11 +291,6 @@ const AddGuardrailForm: React.FC = ({ visible, onClose, a
setSelectedEntities([]);
setSelectedActions({});
- // Reset Azure Text Moderation selections when changing provider
- setSelectedCategories([]);
- setGlobalSeverityThreshold(2);
- setCategorySpecificThresholds({});
-
// Reset Content Filter selections
setSelectedPatterns([]);
setBlockedWords([]);
@@ -335,24 +324,6 @@ const AddGuardrailForm: React.FC = ({ visible, onClose, a
}));
};
- // Azure Text Moderation handlers
- const handleCategorySelect = (category: string) => {
- setSelectedCategories((prev) =>
- prev.includes(category) ? prev.filter((c) => c !== category) : [...prev, category],
- );
- };
-
- const handleGlobalSeverityChange = (threshold: number) => {
- setGlobalSeverityThreshold(threshold);
- };
-
- const handleCategorySeverityChange = (category: string, threshold: number) => {
- setCategorySpecificThresholds((prev) => ({
- ...prev,
- [category]: threshold,
- }));
- };
-
const nextStep = async () => {
try {
// Validate current step fields
@@ -388,53 +359,11 @@ const AddGuardrailForm: React.FC = ({ visible, onClose, a
setCurrentStep(currentStep - 1);
};
- const handleAddAndContinue = (competitorIntentOnly?: boolean) => {
- // Competitor intent only: just advance to next step (no category to add)
- if (competitorIntentOnly) {
- setCurrentStep(currentStep + 1);
- return;
- }
-
- if (!pendingCategorySelection || !guardrailSettings) return;
-
- const contentFilterSettings = guardrailSettings.content_filter_settings;
- if (!contentFilterSettings) return;
-
- const category = contentFilterSettings.content_categories?.find((c) => c.name === pendingCategorySelection);
- if (!category) return;
-
- // Check if already added
- if (selectedContentCategories.some((c) => c.category === pendingCategorySelection)) {
- setPendingCategorySelection("");
- setCurrentStep(currentStep + 1);
- return;
- }
-
- // Add the category
- setSelectedContentCategories([
- ...selectedContentCategories,
- {
- id: `category-${Date.now()}`,
- category: category.name,
- display_name: category.display_name,
- action: category.default_action as "BLOCK" | "MASK",
- severity_threshold: "medium",
- },
- ]);
-
- // Clear pending selection and advance to next step
- setPendingCategorySelection("");
- setCurrentStep(currentStep + 1);
- };
-
const resetForm = () => {
form.resetFields();
setSelectedProvider(null);
setSelectedEntities([]);
setSelectedActions({});
- setSelectedCategories([]);
- setGlobalSeverityThreshold(2);
- setCategorySpecificThresholds({});
setSelectedPatterns([]);
setBlockedWords([]);
setSelectedContentCategories([]);
@@ -965,48 +894,6 @@ const AddGuardrailForm: React.FC = ({ visible, onClose, a
}
};
- const renderStepButtons = () => {
- const totalSteps = shouldRenderContentFilterConfigSettings(selectedProvider) ? 5 : 2;
- const isLastStep = currentStep === totalSteps - 1;
- const isCategoriesStep = shouldRenderContentFilterConfigSettings(selectedProvider) && currentStep === 1;
- const hasPendingCategory = pendingCategorySelection !== "";
- const hasCompetitorIntentConfigured =
- competitorIntentEnabled && (competitorIntentConfig?.brand_self?.length ?? 0) > 0;
- const canContinueFromCategoriesStep = hasPendingCategory || hasCompetitorIntentConfigured;
-
- return (
-
- {currentStep > 0 && Previous }
- {isCategoriesStep ? (
- <>
- Skip
- handleAddAndContinue(hasCompetitorIntentConfigured)}
- disabled={!canContinueFromCategoriesStep}
- >
- {hasPendingCategory ? "Add & Continue →" : "Continue →"}
-
- >
- ) : (
- <>
- {!isLastStep && (
-
- Next
-
- )}
- {isLastStep && (
-
- Create Guardrail
-
- )}
- >
- )}
- Cancel
-
- );
- };
-
const renderEndpointSettings = () => {
return (
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/KeywordTable.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/KeywordTable.tsx
index 57e59423aa5..eed2c8a5129 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/KeywordTable.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/KeywordTable.tsx
@@ -1,8 +1,7 @@
import { DeleteOutlined } from "@ant-design/icons";
-import { Button, Select, Table, Typography } from "antd";
+import { Button, Select, Table } from "antd";
import React from "react";
-const { Text } = Typography;
const { Option } = Select;
interface BlockedWord {
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info.test.tsx
index 7bb7737e152..5fcf07d3f8b 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info.test.tsx
@@ -229,7 +229,7 @@ describe("Guardrail Info", () => {
vi.mocked(networking.getGuardrailProviderSpecificParams).mockResolvedValue({});
vi.mocked(networking.updateGuardrailCall).mockResolvedValue({ status: "success" });
- const { getByText, getByRole, getAllByRole, getByLabelText } = render(
+ const { getByText, getByLabelText } = render(
{}} accessToken="123" isAdmin={true} />,
);
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info.tsx
index 07df6ff15d9..54cac4bbe5c 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info.tsx
@@ -35,22 +35,6 @@ export interface GuardrailInfoProps {
isAdmin: boolean;
}
-interface ProviderParam {
- param: string;
- description: string;
- required: boolean;
- default_value?: string;
- options?: string[];
- type?: string;
- fields?: { [key: string]: ProviderParam };
- dict_key_options?: string[];
- dict_value_type?: string;
-}
-
-interface ProviderParamsResponse {
- [provider: string]: { [key: string]: ProviderParam };
-}
-
const GuardrailInfoView: React.FC = ({ guardrailId, onClose, accessToken, isAdmin }) => {
const [guardrailData, setGuardrailData] = useState(null);
const [guardrailProviderSpecificParams, setGuardrailProviderSpecificParams] = useState(null);
@@ -244,11 +228,6 @@ const GuardrailInfoView: React.FC = ({ guardrailId, onClose,
resetToolPermissionEditor();
}, [resetToolPermissionEditor]);
- const handleToolPermissionConfigChange = (config: ToolPermissionConfig) => {
- setToolPermissionConfig(config);
- setToolPermissionDirty(true);
- };
-
const handlePiiEntitySelect = (entity: string) => {
setSelectedPiiEntities((prev) => {
if (prev.includes(entity)) {
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/teams/useTeams.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/teams/useTeams.test.ts
index 28320b76597..66dfc43cebb 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/teams/useTeams.test.ts
+++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/teams/useTeams.test.ts
@@ -436,9 +436,6 @@ describe("useTeam", () => {
showSSOBanner: false,
});
- // Import useQueryClient to get access to query client
- const { useQueryClient } = await import("@tanstack/react-query");
-
// Manually test the queryFn logic by calling it directly
// This simulates what would happen if enabled check was bypassed
const testQueryFn = async () => {
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.test.ts
index bb14f6c3d21..1ca0581a391 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.test.ts
+++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.test.ts
@@ -305,7 +305,7 @@ describe("useAuthorized", () => {
const token = createJwt(decodedPayload);
document.cookie = `token=${token}; path=/;`;
- const { result } = renderHook(() => useAuthorized(), { wrapper });
+ renderHook(() => useAuthorized(), { wrapper });
await waitFor(() => {
expect(clearTokenCookiesMock).toHaveBeenCalled();
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.tsx
index be41434c151..476e43e49aa 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.tsx
@@ -333,7 +333,6 @@ const CreateMCPServer: React.FC = ({
if (!pendingRestoredValues) {
return;
}
- const transportReady = transportType || pendingRestoredValues.transport || "";
if (pendingRestoredValues.transport && !transportType) {
// wait until transportType state catches up so the URL field is mounted
return;
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPLogoSelector.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPLogoSelector.test.tsx
index 67b5d6bfe92..f126a030b9f 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPLogoSelector.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPLogoSelector.test.tsx
@@ -1,5 +1,5 @@
import React from "react";
-import { render, screen, fireEvent } from "@testing-library/react";
+import { render, screen } from "@testing-library/react";
import { describe, it, expect, vi, beforeEach } from "vitest";
import userEvent from "@testing-library/user-event";
import MCPLogoSelector from "./MCPLogoSelector";
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_connect.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_connect.tsx
index 7bdfd9c6b8f..74b77735377 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_connect.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_connect.tsx
@@ -1,14 +1,13 @@
/* eslint-disable react/no-unescaped-entities */
import React, { useState } from "react";
-import { Card, Typography, Space, Alert, Button, Switch, Form, Collapse } from "antd";
+import { Card, Typography, Space, Alert, Button, Switch, Form } from "antd";
import { TabPanel, TabPanels, TabGroup, TabList, Tab, Title as TremorTitle, Text as TremorText } from "@tremor/react";
import { CopyIcon, Code, Terminal, Globe, CheckIcon, ExternalLinkIcon, KeyIcon, ServerIcon, Zap } from "lucide-react";
import { getProxyBaseUrl } from "@/components/networking";
import { copyToClipboard as utilCopyToClipboard } from "@/utils/dataUtils";
const { Title, Text } = Typography;
-const { Panel } = Collapse;
interface CodeBlockProps {
code: string;
@@ -117,12 +116,6 @@ interface MCPConnectProps {
const MCPConnect: React.FC = ({ currentServerAccessGroups = [] }) => {
const proxyBaseUrl = getProxyBaseUrl();
const [copiedStates, setCopiedStates] = useState>({});
- const [serverHeaders, setServerHeaders] = useState>({
- openai: [],
- litellm: [],
- cursor: [],
- http: [],
- });
const [currentServer] = useState("Zapier_MCP"); // This should match the current server being viewed
const copyToClipboard = async (text: string, key: string) => {
@@ -135,22 +128,6 @@ const MCPConnect: React.FC = ({ currentServerAccessGroups = []
}
};
- const getHeadersConfig = (type: string) => {
- const headers: Record = {
- "x-litellm-api-key": "Bearer YOUR_LITELLM_API_KEY",
- };
-
- if (serverHeaders[type]?.length > 0) {
- // Format server names (replace spaces with underscores)
- const formattedServers = serverHeaders[type].map((s) => s.replace(/\s+/g, "_"));
-
- // Use comma-separated string (can include both servers and access groups)
- headers["x-mcp-servers"] = formattedServers.join(",");
- }
-
- return headers;
- };
-
const CodeBlock: React.FC<{
code: string;
copyKey: string;
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatUI.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatUI.test.tsx
index b5f2bf7b10c..3c94977f0dc 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatUI.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatUI.test.tsx
@@ -60,7 +60,7 @@ describe("ChatUI", () => {
});
it("should show the voice selector when the endpoint type is audio_speech", async () => {
- const { getByText, container } = render(
+ const { getByText } = render(
{
});
it("should allow the user to select a model", async () => {
- const { getByText, container } = render(
+ const { getByText } = render(
{
{ model_group: "ResponsesModel", mode: "responses" },
]);
- const { getByText, baseElement } = render(
+ const { getByText } = render(
= ({
chatHistory,
setChatHistory,
mcpEvents,
- setMCPEvents,
messageTraceId,
setMessageTraceId,
responsesSessionId,
- setResponsesSessionId,
useApiSessionManagement,
- setUseApiSessionManagement,
updateTextUI,
updateReasoningContent,
updateTimingData,
@@ -603,8 +589,6 @@ const ChatUI: React.FC = ({
NotificationsManager.fromBackend("Please select an MCP server to test");
return;
}
- // Resolve the real server ID (toolsets use toolset: prefix)
- const mcpServerId = rawSelected.startsWith("toolset:") ? rawSelected : rawSelected;
if (!selectedMCPDirectTool) {
NotificationsManager.fromBackend("Please select an MCP tool to call");
return;
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/RealtimePlayground.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/RealtimePlayground.tsx
index 68a150be8c0..2bf645fced2 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/RealtimePlayground.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/RealtimePlayground.tsx
@@ -37,8 +37,6 @@ const RealtimePlayground: React.FC = ({
const audioContextRef = useRef(null);
const mediaStreamRef = useRef(null);
const processorRef = useRef(null);
- const playbackQueueRef = useRef([]);
- const isPlayingRef = useRef(false);
const messagesEndRef = useRef(null);
const nextPlayTimeRef = useRef(0);
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/policies/_components/add_policy_form.tsx b/ui/litellm-dashboard/src/app/(dashboard)/policies/_components/add_policy_form.tsx
index 8da2d3af36f..74e8bd3c6d9 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/policies/_components/add_policy_form.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/policies/_components/add_policy_form.tsx
@@ -8,7 +8,6 @@ import NotificationsManager from "@/components/molecules/notifications_manager";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
const { Text } = Typography;
-const { Option } = Select;
interface AddPolicyFormProps {
visible: boolean;
@@ -162,7 +161,6 @@ const AddPolicyForm: React.FC = ({
const [form] = Form.useForm();
const [isSubmitting, setIsSubmitting] = useState(false);
const [resolvedGuardrails, setResolvedGuardrails] = useState([]);
- const [isLoadingResolved, setIsLoadingResolved] = useState(false);
const [modelConditionType, setModelConditionType] = useState<"model" | "regex">("model");
const [availableModels, setAvailableModels] = useState([]);
const [step, setStep] = useState<"pick_mode" | "simple_form">("pick_mode");
@@ -231,14 +229,11 @@ const AddPolicyForm: React.FC = ({
const loadResolvedGuardrails = async (policyId: string) => {
if (!accessToken) return;
- setIsLoadingResolved(true);
try {
const data = await getResolvedGuardrails(accessToken, policyId);
setResolvedGuardrails(data.resolved_guardrails || []);
} catch (error) {
console.error("Failed to load resolved guardrails:", error);
- } finally {
- setIsLoadingResolved(false);
}
};
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/policies/_components/policy_info.tsx b/ui/litellm-dashboard/src/app/(dashboard)/policies/_components/policy_info.tsx
index c49093f3238..0a5c7ef1186 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/policies/_components/policy_info.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/policies/_components/policy_info.tsx
@@ -53,7 +53,6 @@ const PolicyInfoView: React.FC = ({
const [policy, setPolicy] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [resolvedGuardrails, setResolvedGuardrails] = useState([]);
- const [isLoadingResolved, setIsLoadingResolved] = useState(false);
const fetchPolicy = useCallback(async () => {
if (!accessToken || !policyId) return;
@@ -64,14 +63,11 @@ const PolicyInfoView: React.FC = ({
setPolicy(data);
// Also fetch resolved guardrails
- setIsLoadingResolved(true);
try {
const resolvedData = await getResolvedGuardrails(accessToken, policyId);
setResolvedGuardrails(resolvedData.resolved_guardrails || []);
} catch (error) {
console.error("Error fetching resolved guardrails:", error);
- } finally {
- setIsLoadingResolved(false);
}
} catch (error) {
console.error("Error fetching policy:", error);
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectKeysTable.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectKeysTable.test.tsx
index 3685d30b414..506de6ad43e 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectKeysTable.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectKeysTable.test.tsx
@@ -8,6 +8,12 @@ vi.mock("@/components/common_components/DefaultProxyAdminTag", () => ({
default: ({ userId }: { userId: string }) => {userId} ,
}));
+const push = vi.fn();
+vi.mock("next/navigation", async () => ({
+ ...(await vi.importActual("next/navigation")),
+ useRouter: () => ({ push }),
+}));
+
const defaultProps = {
totalCount: 0,
isLoading: false,
@@ -100,6 +106,40 @@ describe("ProjectKeysTable", () => {
expect(screen.getByText("—")).toBeInTheDocument();
});
+ it("should link the key name to that key's detail on the Virtual Keys page", () => {
+ renderWithProviders(
+ ,
+ );
+ expect(screen.getByRole("link", { name: "My API Key" })).toHaveAttribute("href", "/ui/api-keys?key=tok-abc123");
+ });
+
+ it("should navigate to the key detail without a full page load when the key name is clicked", async () => {
+ const user = userEvent.setup();
+ push.mockClear();
+ renderWithProviders(
+ ,
+ );
+ await user.click(screen.getByRole("link", { name: "My API Key" }));
+ expect(push).toHaveBeenCalledWith("/ui/api-keys?key=tok-abc123");
+ });
+
+ it("should still link a key that has no alias", () => {
+ renderWithProviders(
+ ,
+ );
+ expect(screen.getByRole("link", { name: "—" })).toHaveAttribute("href", "/ui/api-keys?key=tok-no-alias");
+ });
+
+ it("should give each row a link to its own key", () => {
+ const keys = [makeKey({ token: "tok-1", key_alias: "Key One" }), makeKey({ token: "tok-2", key_alias: "Key Two" })];
+ renderWithProviders( );
+ expect(screen.getByRole("link", { name: "Key One" })).toHaveAttribute("href", "/ui/api-keys?key=tok-1");
+ expect(screen.getByRole("link", { name: "Key Two" })).toHaveAttribute("href", "/ui/api-keys?key=tok-2");
+ });
+
it("should display the owner using user.user_email when available", () => {
const key = makeKey({ user: { user_id: "u1", user_email: "alice@example.com", user_alias: null } });
renderWithProviders( );
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectKeysTableColumns.tsx b/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectKeysTableColumns.tsx
index b04a32844ea..591de71ce10 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectKeysTableColumns.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectKeysTableColumns.tsx
@@ -4,7 +4,8 @@ import { ColumnDef } from "@tanstack/react-table";
import DefaultProxyAdminTag from "@/components/common_components/DefaultProxyAdminTag";
import { KeyResponse } from "@/components/key_team_helpers/key_list";
-import { CellTooltip, DateCell } from "@/components/shared/table_cells";
+import { CellTooltip, DateCell, IdentityCell } from "@/components/shared/table_cells";
+import { keyDetailHref } from "@/utils/entityLinks";
function OwnerCell({ record }: { record: KeyResponse }) {
const email = record.user?.user_email ?? record.user_id ?? null;
@@ -29,9 +30,11 @@ export const getProjectKeysTableColumns = (): ColumnDef[] => [
header: "Key Name",
enableSorting: false,
cell: ({ row }) => (
-
- {row.original.key_alias || "—"}
-
+ {row.original.key_alias || "—"}}
+ href={row.original.token ? keyDetailHref(row.original.token) : undefined}
+ className="max-w-60"
+ />
),
},
{
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsPage.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsPage.test.tsx
index 1c06b61698b..0bba9d49fd3 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsPage.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsPage.test.tsx
@@ -1,5 +1,6 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import userEvent from "@testing-library/user-event";
+import type { UrlUpdateEvent } from "nuqs/adapters/testing";
import { renderWithProviders, screen, waitFor, within } from "../../../../../tests/test-utils";
import { ProjectsPage } from "./ProjectsPage";
import { ProjectResponse } from "@/app/(dashboard)/hooks/projects/useProjects";
@@ -20,7 +21,12 @@ vi.mock("./ProjectModals/CreateProjectModal", () => ({
}));
vi.mock("./ProjectDetailsPage", () => ({
- ProjectDetail: ({ projectId }: { projectId: string }) => {projectId}
,
+ ProjectDetail: ({ projectId, onBack }: { projectId: string; onBack: () => void }) => (
+
+ {projectId}
+ Back to projects
+
+ ),
}));
const mockProjects: ProjectResponse[] = [
@@ -200,6 +206,48 @@ describe("ProjectsPage", () => {
});
});
+ it("should open the detail view directly from a ?project= deep link", () => {
+ mockUseProjects.mockReturnValue({ data: mockProjects, isLoading: false });
+ renderWithProviders( , { searchParams: "?project=proj-2" });
+
+ expect(screen.getByTestId("project-detail")).toHaveTextContent("proj-2");
+ expect(screen.queryByRole("heading", { name: /projects/i })).not.toBeInTheDocument();
+ });
+
+ it("should push ?project= as a new history entry when a project is opened", async () => {
+ const user = userEvent.setup();
+ const onUrlUpdate = vi.fn<(event: UrlUpdateEvent) => void>();
+ mockUseProjects.mockReturnValue({ data: mockProjects, isLoading: false });
+ renderWithProviders( , { onUrlUpdate });
+
+ await user.click(screen.getByText("proj-1"));
+
+ await waitFor(() => {
+ expect(onUrlUpdate).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ queryString: "?project=proj-1",
+ options: expect.objectContaining({ history: "push" }),
+ }),
+ );
+ });
+ });
+
+ it("should clear ?project= and return to the list when the detail view is closed", async () => {
+ const user = userEvent.setup();
+ const onUrlUpdate = vi.fn<(event: UrlUpdateEvent) => void>();
+ mockUseProjects.mockReturnValue({ data: mockProjects, isLoading: false });
+ renderWithProviders( , { searchParams: "?project=proj-1", onUrlUpdate });
+
+ await user.click(screen.getByRole("button", { name: /back to projects/i }));
+
+ await waitFor(() => {
+ expect(onUrlUpdate).toHaveBeenLastCalledWith(expect.objectContaining({ queryString: "" }));
+ });
+ expect(onUrlUpdate.mock.calls.at(-1)?.[0].options.history).toBe("replace");
+ expect(screen.queryByTestId("project-detail")).not.toBeInTheDocument();
+ expect(screen.getByText("Alpha Project")).toBeInTheDocument();
+ });
+
it("should resolve team alias from the teams list in the Team column", () => {
mockUseTeams.mockReturnValue({
data: [{ team_id: "team-1", team_alias: "Engineering", models: [] }],
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsPage.tsx b/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsPage.tsx
index 58c2c4c3ad8..a7e2f70198f 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsPage.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsPage.tsx
@@ -3,6 +3,7 @@ import { useTeams } from "@/app/(dashboard)/hooks/teams/useTeams";
import { PlusOutlined } from "@ant-design/icons";
import { Button, Flex, Input, Layout, Space, theme, Typography } from "antd";
import { SearchIcon } from "lucide-react";
+import { parseAsString, useQueryState } from "nuqs";
import { useMemo, useState } from "react";
import { CreateProjectModal } from "./ProjectModals/CreateProjectModal";
import { ProjectDetail } from "./ProjectDetailsPage";
@@ -16,7 +17,10 @@ export function ProjectsPage() {
const { data: projects, isLoading } = useProjects();
const { data: teams, isLoading: isTeamsLoading } = useTeams();
- const [selectedProjectId, setSelectedProjectId] = useState(null);
+ const [selectedProjectId, setSelectedProjectId] = useQueryState(
+ "project",
+ parseAsString.withOptions({ history: "push" }),
+ );
const [isCreateModalVisible, setIsCreateModalVisible] = useState(false);
const [searchText, setSearchText] = useState("");
@@ -44,7 +48,12 @@ export function ProjectsPage() {
}, [projects, searchText, teamAliasMap]);
if (selectedProjectId) {
- return setSelectedProjectId(null)} />;
+ return (
+ void setSelectedProjectId(null, { history: "replace" })}
+ />
+ );
}
return (
@@ -76,7 +85,7 @@ export function ProjectsPage() {
projects={filteredProjects}
isLoading={isLoading}
isFiltered={searchText.trim().length > 0}
- onProjectClick={setSelectedProjectId}
+ onProjectClick={(id) => void setSelectedProjectId(id)}
teamAliasMap={teamAliasMap}
isTeamsLoading={isTeamsLoading}
/>
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsTable.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsTable.test.tsx
new file mode 100644
index 00000000000..a1b59f6035c
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsTable.test.tsx
@@ -0,0 +1,167 @@
+import { describe, it, expect, vi } from "vitest";
+import userEvent from "@testing-library/user-event";
+import { OnUrlUpdateFunction } from "nuqs/adapters/testing";
+import { renderWithProviders, screen, waitFor, within } from "../../../../../tests/test-utils";
+import { ProjectsTable } from "./ProjectsTable";
+import { ProjectResponse } from "@/app/(dashboard)/hooks/projects/useProjects";
+
+const makeProject = (index: number): ProjectResponse => ({
+ project_id: `proj-${String(index).padStart(2, "0")}`,
+ project_alias: `Project ${String(index).padStart(2, "0")}`,
+ description: null,
+ team_id: "team-1",
+ budget_id: null,
+ metadata: null,
+ models: [],
+ spend: 0,
+ model_spend: null,
+ model_rpm_limit: null,
+ model_tpm_limit: null,
+ blocked: false,
+ object_permission_id: null,
+ created_at: "2024-01-01T00:00:00Z",
+ created_by: "user-1",
+ updated_at: "2024-01-01T00:00:00Z",
+ updated_by: "user-1",
+ litellm_budget_table: null,
+});
+
+const allProjects = Array.from({ length: 14 }, (_, index) => makeProject(index + 1));
+
+interface RenderOptions {
+ projects?: ProjectResponse[];
+ isLoading?: boolean;
+ isFiltered?: boolean;
+ searchParams?: string;
+ onUrlUpdate?: OnUrlUpdateFunction;
+}
+
+const renderTable = ({
+ projects = allProjects,
+ isLoading = false,
+ isFiltered = false,
+ ...providers
+}: RenderOptions) => {
+ const table = (projectList: ProjectResponse[], loading: boolean, filtered: boolean) => (
+
+ );
+ const view = renderWithProviders(table(projects, isLoading, isFiltered), providers);
+ return {
+ ...view,
+ rerenderWith: (next: ProjectResponse[], filtered = false) => view.rerender(table(next, false, filtered)),
+ };
+};
+
+const firstDataRow = () => within(screen.getAllByRole("row")[1]);
+
+const dataRowCount = () => screen.getAllByRole("row").length - 1;
+
+describe("ProjectsTable pagination URL state", () => {
+ it("should render the second page of projects for a ?page=2 deep link", () => {
+ renderTable({ searchParams: "?page=2" });
+
+ expect(firstDataRow().getByText("Project 11")).toBeInTheDocument();
+ expect(screen.queryByText("Project 01")).not.toBeInTheDocument();
+ expect(screen.getByTestId("pagination-page")).toHaveTextContent("Page 2 of 2");
+ expect(screen.getByTestId("pagination-range")).toHaveTextContent("Showing 11-14 of 14");
+ });
+
+ it("should push ?page=2 onto history when the next page control is clicked", async () => {
+ const user = userEvent.setup();
+ const onUrlUpdate = vi.fn();
+ renderTable({ onUrlUpdate });
+
+ await user.click(screen.getByTestId("pagination-next"));
+
+ await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled());
+ const [update] = onUrlUpdate.mock.calls[0];
+ expect(update.searchParams.get("page")).toBe("2");
+ expect(update.options.history).toBe("push");
+ expect(firstDataRow().getByText("Project 11")).toBeInTheDocument();
+ });
+
+ it("should clear the page param when returning to the first page", async () => {
+ const user = userEvent.setup();
+ const onUrlUpdate = vi.fn();
+ renderTable({ searchParams: "?page=2", onUrlUpdate });
+
+ await user.click(screen.getByTestId("pagination-prev"));
+
+ await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled());
+ const [update] = onUrlUpdate.mock.calls[0];
+ expect(update.searchParams.get("page")).toBeNull();
+ expect(firstDataRow().getByText("Project 01")).toBeInTheDocument();
+ });
+
+ it("should keep the deep-linked page when the project list arrives after the first render", async () => {
+ const onUrlUpdate = vi.fn();
+ const stillLoading: RenderOptions = { projects: [], isLoading: true, searchParams: "?page=2", onUrlUpdate };
+ const { rerenderWith } = renderTable(stillLoading);
+ await waitFor(() => expect(screen.getAllByTestId("skeleton-row").length).toBeGreaterThan(0));
+
+ rerenderWith(allProjects);
+
+ await waitFor(() => expect(screen.getByTestId("pagination-page")).toHaveTextContent("Page 2 of 2"));
+ expect(firstDataRow().getByText("Project 11")).toBeInTheDocument();
+ expect(onUrlUpdate).not.toHaveBeenCalled();
+ });
+
+ it("should fall back to the first page, not the last remaining page, when a filter leaves fewer pages", async () => {
+ const manyProjects = Array.from({ length: 44 }, (_, index) => makeProject(index + 1));
+ const { rerenderWith } = renderTable({ projects: manyProjects, searchParams: "?page=5" });
+ expect(firstDataRow().getByText("Project 41")).toBeInTheDocument();
+
+ rerenderWith(allProjects.slice(0, 12), true);
+
+ await waitFor(() => expect(screen.getByTestId("pagination-page")).toHaveTextContent("Page 1 of 2"));
+ expect(firstDataRow().getByText("Project 01")).toBeInTheDocument();
+ expect(dataRowCount()).toBe(10);
+ });
+
+ it("should show the first page instead of an empty table for an out-of-range ?page=99", () => {
+ renderTable({ searchParams: "?page=99" });
+
+ expect(firstDataRow().getByText("Project 01")).toBeInTheDocument();
+ expect(dataRowCount()).toBe(10);
+ expect(screen.getByTestId("pagination-page")).toHaveTextContent("Page 1 of 2");
+ });
+
+ it("should return to the first page and write ?page_size= in one history entry when the page size changes", async () => {
+ const user = userEvent.setup();
+ const onUrlUpdate = vi.fn();
+ renderTable({ searchParams: "?page=2", onUrlUpdate });
+
+ await user.click(screen.getByTestId("pagination-page-size"));
+ await user.click(await screen.findByRole("option", { name: "25" }));
+
+ await waitFor(() => expect(screen.getByTestId("pagination-range")).toHaveTextContent("Showing 1-14 of 14"));
+ expect(firstDataRow().getByText("Project 01")).toBeInTheDocument();
+ expect(onUrlUpdate).toHaveBeenCalledTimes(1);
+ const lastUpdate = onUrlUpdate.mock.calls.at(-1)?.[0];
+ expect(lastUpdate.searchParams.get("page")).toBeNull();
+ expect(lastUpdate.searchParams.get("page_size")).toBe("25");
+ });
+
+ it("should apply both params from a ?page=2&page_size=25 deep link so the restored view matches", () => {
+ const manyProjects = Array.from({ length: 44 }, (_, index) => makeProject(index + 1));
+ renderTable({ projects: manyProjects, searchParams: "?page=2&page_size=25" });
+
+ expect(firstDataRow().getByText("Project 26")).toBeInTheDocument();
+ expect(screen.getByTestId("pagination-page")).toHaveTextContent("Page 2 of 2");
+ expect(screen.getByTestId("pagination-range")).toHaveTextContent("Showing 26-44 of 44");
+ });
+
+ it("should fall back to the default page size for a ?page_size= value outside the offered options", () => {
+ renderTable({ searchParams: "?page_size=7" });
+
+ expect(dataRowCount()).toBe(10);
+ expect(screen.getByTestId("pagination-page")).toHaveTextContent("Page 1 of 2");
+ });
+});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsTable.tsx b/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsTable.tsx
index ad85019ca6d..74242f3ed45 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsTable.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsTable.tsx
@@ -2,10 +2,11 @@
import { SortingState } from "@tanstack/react-table";
import { FolderKanban } from "lucide-react";
+import { parseAsInteger, useQueryStates } from "nuqs";
import { useMemo, useState } from "react";
import { ProjectResponse } from "@/app/(dashboard)/hooks/projects/useProjects";
-import { DataTable } from "@/components/shared/DataTable";
+import { DataTable, DataTablePagination } from "@/components/shared/DataTable";
import { getProjectsTableColumns } from "./ProjectsTableColumns";
@@ -18,7 +19,8 @@ interface ProjectsTableProps {
isTeamsLoading: boolean;
}
-const PAGE_SIZE_OPTIONS = [10, 25, 50];
+const DEFAULT_PAGE_SIZE = 10;
+const PAGE_SIZE_OPTIONS = [DEFAULT_PAGE_SIZE, 25, 50];
function EmptyState({ isFiltered }: { isFiltered: boolean }) {
return (
@@ -45,12 +47,20 @@ export function ProjectsTable({
isTeamsLoading,
}: ProjectsTableProps) {
const [sorting, setSorting] = useState([]);
+ const [{ page, page_size }, setPagination] = useQueryStates(
+ { page: parseAsInteger.withDefault(1), page_size: parseAsInteger.withDefault(DEFAULT_PAGE_SIZE) },
+ { history: "push" },
+ );
+ const pageSize = PAGE_SIZE_OPTIONS.includes(page_size) ? page_size : DEFAULT_PAGE_SIZE;
const columns = useMemo(() => {
const deps = { onProjectClick, teamAliasMap, isTeamsLoading };
return getProjectsTableColumns(deps);
}, [onProjectClick, teamAliasMap, isTeamsLoading]);
+ const pageCount = Math.max(Math.ceil(projects.length / pageSize), 1);
+ const pageIndex = page >= 1 && page <= pageCount ? page - 1 : 0;
+
return (
(
+ void setPagination({ page: nextPageIndex + 1 })}
+ onPageSizeChange={(nextPageSize) => void setPagination({ page_size: nextPageSize, page: null })}
+ pageSizeOptions={PAGE_SIZE_OPTIONS}
+ isLoading={isLoading}
+ />
+ )}
isLoading={isLoading}
loadingMessage="Loading projects…"
noDataMessage={ }
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/add_prompt_form.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/add_prompt_form.tsx
index 1bd831ca49d..c02efa82499 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/add_prompt_form.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/add_prompt_form.tsx
@@ -15,12 +15,6 @@ interface AddPromptFormProps {
onSuccess: () => void;
}
-interface PromptFormData {
- prompt_id: string;
- prompt_integration: string;
- prompt_file?: File;
-}
-
const AddPromptForm: React.FC = ({ visible, onClose, accessToken, onSuccess }) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/index.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/index.tsx
index 9bebabb8cf2..f9bba3ce661 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/index.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/index.tsx
@@ -7,7 +7,7 @@ import PromptInfoView from "./prompt_info";
import AddPromptForm from "./add_prompt_form";
import PromptEditorView from "./prompt_editor_view";
import NotificationsManager from "@/components/molecules/notifications_manager";
-import { isAdminRole, isProxyAdminRole } from "@/utils/roles";
+import { isProxyAdminRole } from "@/utils/roles";
import { Button } from "@/components/ui/button";
import {
AlertDialog,
@@ -47,7 +47,6 @@ const PromptsPanel: React.FC = ({ accessToken, userRole }) => {
const [isDeleting, setIsDeleting] = useState(false);
const [promptToDelete, setPromptToDelete] = useState<{ id: string; name: string } | null>(null);
- const isAdmin = userRole ? isAdminRole(userRole) : false;
// Admin Viewer follows the read-parity rule: see prompts, no writes.
const canModify = userRole ? isProxyAdminRole(userRole) : false;
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/VersionHistorySidePanel.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/VersionHistorySidePanel.test.tsx
index c76c64b89a6..97f671b1b71 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/VersionHistorySidePanel.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/VersionHistorySidePanel.test.tsx
@@ -182,7 +182,7 @@ describe("VersionHistorySidePanel", () => {
render( );
await waitFor(() => {
- const versionItems = screen.getAllByTestId("tag");
+ screen.getAllByTestId("tag");
// Should have Active tag for the selected version
expect(screen.getByText("Active")).toBeInTheDocument();
});
@@ -464,7 +464,7 @@ describe("VersionHistorySidePanel", () => {
render( );
await waitFor(() => {
- const versionElements = screen.getAllByTestId("tag");
+ screen.getAllByTestId("tag");
// Verify versions are displayed as they come from the API
expect(screen.getByText("v2")).toBeInTheDocument();
});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/index.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/index.tsx
index fa69a520145..e09ca787a69 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/index.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/index.tsx
@@ -44,7 +44,7 @@ const PromptEditorView: React.FC = ({ onClose, onSuccess,
};
const [prompt, setPrompt] = useState(getInitialPrompt());
- const [editMode, setEditMode] = useState(!!initialPromptData);
+ const [editMode] = useState(!!initialPromptData);
const [showHistoryModal, setShowHistoryModal] = useState(false);
// Construct versioned ID from prompt_id and version field
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/transform-request/TransformRequestPanel.tsx b/ui/litellm-dashboard/src/app/(dashboard)/transform-request/TransformRequestPanel.tsx
index 0c41547b9b7..0c7f10eab0e 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/transform-request/TransformRequestPanel.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/transform-request/TransformRequestPanel.tsx
@@ -11,12 +11,6 @@ interface TransformRequestPanelProps {
accessToken: string | null;
}
-interface TransformResponse {
- raw_request_api_base: string;
- raw_request_body: Record;
- raw_request_headers: Record;
-}
-
const TransformRequestPanel: React.FC = ({ accessToken }) => {
const [originalRequestJSON, setOriginalRequestJSON] = useState(`{
"model": "openai/gpt-4o",
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/ui-theme/UIThemeSettings.tsx b/ui/litellm-dashboard/src/app/(dashboard)/ui-theme/UIThemeSettings.tsx
index 035d87c4e9b..4da93d8e0b9 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/ui-theme/UIThemeSettings.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/ui-theme/UIThemeSettings.tsx
@@ -15,7 +15,7 @@ interface UIThemeSettingsProps {
}
const UIThemeSettings: React.FC = ({ userID, userRole, accessToken }) => {
- const { logoUrl, setLogoUrl, faviconUrl, setFaviconUrl } = useTheme();
+ const { setLogoUrl, setFaviconUrl } = useTheme();
const [logoUrlInput, setLogoUrlInput] = useState("");
const [faviconUrlInput, setFaviconUrlInput] = useState("");
const [loading, setLoading] = useState(false);
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx
index e73dddd9788..c3645d6371e 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx
@@ -1093,40 +1093,5 @@ const UsagePage: React.FC = ({ teams, organizations }) => {
};
// Add this helper function to process model-specific activity data
-const getModelActivityData = (userSpendData: { results: DailyData[]; metadata: any }) => {
- const modelData: {
- [key: string]: {
- total_requests: number;
- total_tokens: number;
- daily_data: Array<{
- date: string;
- api_requests: number;
- total_tokens: number;
- }>;
- };
- } = {};
-
- userSpendData.results.forEach((day: DailyData) => {
- Object.entries(day.breakdown.models || {}).forEach(([model, metrics]) => {
- if (!modelData[model]) {
- modelData[model] = {
- total_requests: 0,
- total_tokens: 0,
- daily_data: [],
- };
- }
-
- modelData[model].total_requests += metrics.metrics.api_requests;
- modelData[model].total_tokens += metrics.metrics.total_tokens;
- modelData[model].daily_data.push({
- date: day.date,
- api_requests: metrics.metrics.api_requests,
- total_tokens: metrics.metrics.total_tokens,
- });
- });
- });
-
- return modelData;
-};
export default UsagePage;
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsageViewSelect/UsageViewSelect.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsageViewSelect/UsageViewSelect.test.tsx
index b33129fcdb4..aa9ff921ee1 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsageViewSelect/UsageViewSelect.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsageViewSelect/UsageViewSelect.test.tsx
@@ -7,13 +7,10 @@ vi.mock("antd", async () => {
function Select(props: any) {
const { value, onChange, options, optionRender, labelRender, ...rest } = props;
- const selectedOption = options?.find((opt: any) => opt.value === value);
- const renderedLabel = labelRender ? labelRender({ value, label: selectedOption?.label }) : selectedOption?.label;
- const optionElements = options?.map((opt: any) => {
- const rendered = optionRender ? optionRender({ value: opt.value, label: opt.label }) : opt.label;
- return React.createElement("option", { key: opt.value, value: opt.value }, opt.label);
- });
+ const optionElements = options?.map((opt: any) =>
+ React.createElement("option", { key: opt.value, value: opt.value }, opt.label),
+ );
const optionRenderOutputs = options
?.map((opt: any) => {
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/edit_user.tsx b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/edit_user.tsx
index de031984846..0f3e0dedb52 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/edit_user.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/edit_user.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useState } from "react";
+import { useEffect } from "react";
import { TextInput, SelectItem } from "@tremor/react";
import { Button as Button2, Modal, Form, Select as Select2, InputNumber } from "antd";
@@ -15,7 +15,6 @@ interface EditUserModalProps {
}
const EditUserModal: React.FC = ({ visible, possibleUIRoles, onCancel, user, onSubmit }) => {
- const [editedUser, setEditedUser] = useState(user);
const [form] = Form.useForm();
useEffect(() => {
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/vector_store_info.tsx b/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/vector_store_info.tsx
index ec20a3fd318..e5646037d14 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/vector_store_info.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/vector_store_info.tsx
@@ -36,7 +36,6 @@ const VectorStoreInfoView: React.FC = ({
const [isEditing, setIsEditing] = useState(editVectorStore);
const [metadataString, setMetadataString] = useState("{}");
const [credentials, setCredentials] = useState([]);
- const [activeTab, setActiveTab] = useState(editVectorStore ? "details" : "details");
const fetchVectorStoreDetails = async () => {
if (!accessToken) return;
diff --git a/ui/litellm-dashboard/src/app/chat/page.tsx b/ui/litellm-dashboard/src/app/chat/page.tsx
index b6dccef47c6..7b6b26c6436 100644
--- a/ui/litellm-dashboard/src/app/chat/page.tsx
+++ b/ui/litellm-dashboard/src/app/chat/page.tsx
@@ -65,7 +65,6 @@ export default function ChatConversationPage() {
updateLastAssistantMessage,
truncateFromMessage,
} = useChatShell();
- const hadActiveConversationOnMountRef = useRef(activeConversationId !== null);
const [selectedModel, setSelectedModel] = useState(null);
const [models, setModels] = useState([]);
diff --git a/ui/litellm-dashboard/src/components/AIHub/UsefulLinksManagement.tsx b/ui/litellm-dashboard/src/components/AIHub/UsefulLinksManagement.tsx
index 42acfd94185..f29504fac50 100644
--- a/ui/litellm-dashboard/src/components/AIHub/UsefulLinksManagement.tsx
+++ b/ui/litellm-dashboard/src/components/AIHub/UsefulLinksManagement.tsx
@@ -23,7 +23,6 @@ const UsefulLinksManagement: React.FC = ({ accessTok
const [links, setLinks] = useState ([]);
const [newLink, setNewLink] = useState({ url: "", displayName: "" });
const [editingLink, setEditingLink] = useState (null);
- const [loading, setLoading] = useState(false);
const [isExpanded, setIsExpanded] = useState(true);
const [isRearranging, setIsRearranging] = useState(false);
const [originalLinksOrder, setOriginalLinksOrder] = useState ([]);
@@ -32,7 +31,6 @@ const UsefulLinksManagement: React.FC = ({ accessTok
if (!accessToken) return;
try {
- setLoading(true);
const response = await getPublicModelHubInfo();
if (response && response.useful_links) {
@@ -73,8 +71,6 @@ const UsefulLinksManagement: React.FC = ({ accessTok
} catch (error) {
console.error("Error fetching useful links:", error);
setLinks([]);
- } finally {
- setLoading(false);
}
};
diff --git a/ui/litellm-dashboard/src/components/CreateUserButton.tsx b/ui/litellm-dashboard/src/components/CreateUserButton.tsx
index 2b0c801f0bd..8de9f010fa3 100644
--- a/ui/litellm-dashboard/src/components/CreateUserButton.tsx
+++ b/ui/litellm-dashboard/src/components/CreateUserButton.tsx
@@ -15,7 +15,7 @@ import {
Tooltip,
Typography,
} from "antd";
-import React, { useEffect, useMemo, useState } from "react";
+import React, { useEffect, useState } from "react";
import BulkCreateUsers from "./bulk_create_users_button";
import TeamDropdown from "./common_components/team_dropdown";
import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key";
@@ -29,7 +29,7 @@ import {
} from "./networking";
import OnboardingModal, { InvitationLink } from "./onboarding_link";
const { Option } = Select;
-const { Text, Link, Title } = Typography;
+const { Text, Link } = Typography;
// Helper function to generate UUID compatible across all environments
const generateUUID = (): string => {
if (typeof crypto !== "undefined" && crypto.randomUUID) {
@@ -80,11 +80,6 @@ export const CreateUserButton: React.FC = ({
const { data: organizations = [] } = useOrganizations();
// Derive teams from the user's organizations, falling back to the teams prop
- const availableTeams = useMemo(() => {
- const orgTeams = organizations.flatMap((org) => org.teams || []);
- if (orgTeams.length > 0) return orgTeams;
- return teams || [];
- }, [organizations, teams]);
useEffect(() => {
const fetchData = async () => {
diff --git a/ui/litellm-dashboard/src/components/EntityUsageExport/utils.test.ts b/ui/litellm-dashboard/src/components/EntityUsageExport/utils.test.ts
index dfeecdc4fa6..e91b7b73a1e 100644
--- a/ui/litellm-dashboard/src/components/EntityUsageExport/utils.test.ts
+++ b/ui/litellm-dashboard/src/components/EntityUsageExport/utils.test.ts
@@ -1876,7 +1876,7 @@ describe("EntityUsageExport utils", () => {
it("should create CSV file and trigger download", () => {
const createObjectURLSpy = vi.spyOn(window.URL, "createObjectURL").mockReturnValue("blob:mock-url");
- const revokeObjectURLSpy = vi.spyOn(window.URL, "revokeObjectURL");
+ vi.spyOn(window.URL, "revokeObjectURL");
const createElementSpy = vi.spyOn(document, "createElement");
const appendChildSpy = vi.spyOn(document.body, "appendChild");
const removeChildSpy = vi.spyOn(document.body, "removeChild");
@@ -1892,7 +1892,7 @@ describe("EntityUsageExport utils", () => {
it("should generate correct filename", () => {
const anchorElement = document.createElement("a");
- const createElementSpy = vi.spyOn(document, "createElement").mockReturnValue(anchorElement);
+ vi.spyOn(document, "createElement").mockReturnValue(anchorElement);
const today = new Date().toISOString().split("T")[0];
@@ -1935,7 +1935,7 @@ describe("EntityUsageExport utils", () => {
it("should create JSON file and trigger download", () => {
const createObjectURLSpy = vi.spyOn(window.URL, "createObjectURL").mockReturnValue("blob:mock-url");
- const revokeObjectURLSpy = vi.spyOn(window.URL, "revokeObjectURL");
+ vi.spyOn(window.URL, "revokeObjectURL");
const createElementSpy = vi.spyOn(document, "createElement");
const appendChildSpy = vi.spyOn(document.body, "appendChild");
const removeChildSpy = vi.spyOn(document.body, "removeChild");
@@ -1955,7 +1955,7 @@ describe("EntityUsageExport utils", () => {
it("should generate correct filename", () => {
const anchorElement = document.createElement("a");
- const createElementSpy = vi.spyOn(document, "createElement").mockReturnValue(anchorElement);
+ vi.spyOn(document, "createElement").mockReturnValue(anchorElement);
const today = new Date().toISOString().split("T")[0];
const mockDateRange: DateRangePickerValue = {
diff --git a/ui/litellm-dashboard/src/components/HelpLink.test.tsx b/ui/litellm-dashboard/src/components/HelpLink.test.tsx
index b1709436c72..e502126477a 100644
--- a/ui/litellm-dashboard/src/components/HelpLink.test.tsx
+++ b/ui/litellm-dashboard/src/components/HelpLink.test.tsx
@@ -1,5 +1,5 @@
import React from "react";
-import { describe, it, expect, vi } from "vitest";
+import { describe, it, expect } from "vitest";
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "../../tests/test-utils";
diff --git a/ui/litellm-dashboard/src/components/ModelSelect/ModelSelect.tsx b/ui/litellm-dashboard/src/components/ModelSelect/ModelSelect.tsx
index 0965683c241..e993fed2408 100644
--- a/ui/litellm-dashboard/src/components/ModelSelect/ModelSelect.tsx
+++ b/ui/litellm-dashboard/src/components/ModelSelect/ModelSelect.tsx
@@ -2,7 +2,7 @@ import { ProxyModel, useAllProxyModels } from "@/app/(dashboard)/hooks/models/us
import { useOrganization } from "@/app/(dashboard)/hooks/organizations/useOrganizations";
import { useTeam } from "@/app/(dashboard)/hooks/teams/useTeams";
import { useCurrentUser } from "@/app/(dashboard)/hooks/users/useCurrentUser";
-import { Select, Skeleton, Tooltip, type SelectProps } from "antd";
+import { Select, Skeleton, Tooltip } from "antd";
import { Organization, Team } from "../networking";
import { splitWildcardModels } from "./modelUtils";
@@ -93,8 +93,7 @@ const filterModels = (
export const ModelSelect = (props: ModelSelectProps) => {
const { teamID, organizationID, options, context, dataTestId, value = [], onChange, style } = props;
- const { includeUserModels, showAllTeamModelsOption, showAllProxyModelsOverride, includeSpecialOptions } =
- options || {};
+ const { showAllProxyModelsOverride, includeSpecialOptions } = options || {};
const { data: allProxyModels, isLoading: isLoadingAllProxyModels } = useAllProxyModels();
const { data: team, isLoading: isLoadingTeam } = useTeam(teamID);
const { data: organization, isLoading: isLoadingOrganization } = useOrganization(organizationID);
@@ -113,10 +112,6 @@ export const ModelSelect = (props: ModelSelectProps) => {
return ;
}
- const optionRender: NonNullable = (option) => {
- return {option.label} ;
- };
-
const handleChange = (values: string[]) => {
const specialValues = values.filter(isSpecialOption);
diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/Modals/EditSSOSettingsModal.test.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/Modals/EditSSOSettingsModal.test.tsx
index 7415683af83..7536286b404 100644
--- a/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/Modals/EditSSOSettingsModal.test.tsx
+++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/Modals/EditSSOSettingsModal.test.tsx
@@ -320,7 +320,7 @@ describe("EditSSOSettingsModal", () => {
useEditSSOSettings: { mutateAsync: mockMutateAsync, isPending: false },
});
- const { mockOnSuccess } = renderComponent();
+ renderComponent();
fireEvent.click(screen.getByTestId(TEST_IDS.TRIGGER_FORM_SUBMIT));
diff --git a/ui/litellm-dashboard/src/components/Teams.tsx b/ui/litellm-dashboard/src/components/Teams.tsx
index edf376cb8d4..95642b93019 100644
--- a/ui/litellm-dashboard/src/components/Teams.tsx
+++ b/ui/litellm-dashboard/src/components/Teams.tsx
@@ -22,16 +22,13 @@ import AgentSelector from "./agent_management/AgentSelector";
import ModelAliasManager from "./common_components/ModelAliasManager";
import PremiumLoggingSettings from "./common_components/PremiumLoggingSettings";
import RouterSettingsAccordion, { RouterSettingsAccordionValue } from "./common_components/RouterSettingsAccordion";
-import {
- fetchAvailableModelsForTeamOrKey,
- unfurlWildcardModelsInList,
-} from "./key_team_helpers/fetch_available_models_team_key";
+import { fetchAvailableModelsForTeamOrKey } from "./key_team_helpers/fetch_available_models_team_key";
import type { Team } from "./key_team_helpers/key_list";
import MCPServerSelector from "./mcp_server_management/MCPServerSelector";
import MCPToolPermissions from "./mcp_server_management/MCPToolPermissions";
import NotificationsManager from "./molecules/notifications_manager";
import { extractProxyErrorMessage } from "@/lib/http/client";
-import { Organization, fetchMCPAccessGroups, getGuardrailsList, getPoliciesList, teamDeleteCall } from "./networking";
+import { Organization, getGuardrailsList, getPoliciesList, teamDeleteCall } from "./networking";
import NumericalInput from "./shared/numerical_input";
import VectorStoreSelector from "./vector_store_management/VectorStoreSelector";
import SearchToolSelector from "./search_tools/SearchToolSelector";
@@ -43,35 +40,10 @@ interface TeamProps {
premiumUser?: boolean;
}
-interface EditTeamModalProps {
- visible: boolean;
- onCancel: () => void;
- team: any; // Assuming TeamType is a type representing your team object
- onSubmit: (data: FormData) => void; // Assuming FormData is the type of data to be submitted
-}
-
import DeleteResourceModal from "./common_components/DeleteResourceModal";
import { teamCreateCall } from "./networking";
import { ModelSelect } from "./ModelSelect/ModelSelect";
-const getOrganizationModels = (organization: Organization | null, userModels: string[]) => {
- let tempModelsToPick = [];
-
- if (organization) {
- if (organization.models.length > 0) {
- tempModelsToPick = organization.models;
- } else {
- // show all available models if the team has no models set
- tempModelsToPick = userModels;
- }
- } else {
- // no team set, show all available models
- tempModelsToPick = userModels;
- }
-
- return unfurlWildcardModelsInList(tempModelsToPick, userModels);
-};
-
const canCreateOrManageTeams = (
userRole: string | null,
userID: string | null,
@@ -112,18 +84,6 @@ const getAdminOrganizations = (
return [];
};
-const getOrganizationAlias = (
- organizationId: string | null | undefined,
- organizations: Organization[] | null | undefined,
-): string => {
- if (!organizationId || !organizations) {
- return organizationId || "N/A";
- }
-
- const organization = organizations.find((org) => org.organization_id === organizationId);
- return organization?.organization_alias || organizationId;
-};
-
// @deprecated
const Teams: React.FC = ({ accessToken, userID, userRole, premiumUser = false }) => {
const { data: organizationsData } = useOrganizations();
@@ -135,35 +95,25 @@ const Teams: React.FC = ({ accessToken, userID, userRole, premiumUser
const [currentOrgForCreateTeam, setCurrentOrgForCreateTeam] = useState(null);
const [form] = Form.useForm();
- const [memberForm] = Form.useForm();
- const [value, setValue] = useState("");
- const [editModalVisible, setEditModalVisible] = useState(false);
const [selectedTeam, setSelectedTeam] = useState(null);
const [selectedTeamId, setSelectedTeamId] = useQueryState("team", parseAsString.withOptions({ history: "push" }));
const [editTeam, setEditTeam] = useState(false);
const [isTeamModalVisible, setIsTeamModalVisible] = useState(false);
- const [isAddMemberModalVisible, setIsAddMemberModalVisible] = useState(false);
- const [isEditMemberModalVisible, setIsEditMemberModalVisible] = useState(false);
const [userModels, setUserModels] = useState([]);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [teamToDelete, setTeamToDelete] = useState(null);
- const [modelsToPick, setModelsToPick] = useState([]);
const [isTeamDeleting, setIsTeamDeleting] = useState(false);
// Add this state near the other useState declarations
const [guardrailsList, setGuardrailsList] = useState([]);
const [policiesList, setPoliciesList] = useState([]);
const [loggingSettings, setLoggingSettings] = useState([]);
- const [mcpAccessGroups, setMcpAccessGroups] = useState([]);
- const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false);
const [modelAliases, setModelAliases] = useState<{ [key: string]: string }>({});
const [routerSettings, setRouterSettings] = useState(null);
const [routerSettingsKey, setRouterSettingsKey] = useState(0);
useEffect(() => {
- const models = getOrganizationModels(currentOrgForCreateTeam, userModels);
- setModelsToPick(models);
form.setFieldValue("models", []);
}, [currentOrgForCreateTeam, userModels]);
@@ -220,22 +170,6 @@ const Teams: React.FC = ({ accessToken, userID, userRole, premiumUser
fetchPolicies();
}, [accessToken]);
- const fetchMcpAccessGroups = async () => {
- try {
- if (accessToken == null) {
- return;
- }
- const groups = await fetchMCPAccessGroups(accessToken);
- setMcpAccessGroups(groups);
- } catch (error) {
- console.error("Failed to fetch MCP access groups:", error);
- }
- };
-
- useEffect(() => {
- fetchMcpAccessGroups();
- }, [accessToken]);
-
const handleOk = () => {
setIsTeamModalVisible(false);
form.resetFields();
@@ -245,12 +179,6 @@ const Teams: React.FC = ({ accessToken, userID, userRole, premiumUser
setRouterSettingsKey((prev) => prev + 1);
};
- const handleMemberOk = () => {
- setIsAddMemberModalVisible(false);
- setIsEditMemberModalVisible(false);
- memberForm.resetFields();
- };
-
const handleCancel = () => {
setIsTeamModalVisible(false);
form.resetFields();
@@ -260,12 +188,6 @@ const Teams: React.FC = ({ accessToken, userID, userRole, premiumUser
setRouterSettingsKey((prev) => prev + 1);
};
- const handleMemberCancel = () => {
- setIsAddMemberModalVisible(false);
- setIsEditMemberModalVisible(false);
- memberForm.resetFields();
- };
-
const handleDelete = async (team: Team) => {
// Set the team to delete and open the confirmation modal
setTeamToDelete(team);
@@ -749,15 +671,7 @@ const Teams: React.FC = ({ accessToken, userID, userRole, premiumUser
/>
- {
- if (!mcpAccessGroupsLoaded) {
- fetchMcpAccessGroups();
- setMcpAccessGroupsLoaded(true);
- }
- }}
- >
+
Additional Settings
diff --git a/ui/litellm-dashboard/src/components/ToolDetail.tsx b/ui/litellm-dashboard/src/components/ToolDetail.tsx
index 06f14638141..d90689fc627 100644
--- a/ui/litellm-dashboard/src/components/ToolDetail.tsx
+++ b/ui/litellm-dashboard/src/components/ToolDetail.tsx
@@ -24,12 +24,9 @@ import {
fetchToolPolicyOptions,
getToolUsageLogs,
keyListCall,
- teamListCall,
updateToolPolicy,
- type ToolPolicyOption,
type ToolPolicyOverrideRow,
} from "@/components/networking";
-import type { Team } from "@/components/key_team_helpers/key_list";
interface ToolDetailProps {
toolName: string;
@@ -87,12 +84,6 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) {
staleTime: 60_000,
});
- const { data: teamsData } = useQuery({
- queryKey: ["teams-list-tool-detail"],
- queryFn: () => teamListCall(accessToken!, null, null),
- enabled: !!accessToken,
- });
-
const { data: keysData } = useQuery({
queryKey: ["keys-list-tool-detail"],
queryFn: () => keyListCall(accessToken!, null, null, null, null, null, 1, 100),
@@ -122,24 +113,6 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) {
}));
}, [logsData?.logs]);
- const teams: Team[] = useMemo(() => {
- const arr = Array.isArray(teamsData) ? teamsData : teamsData?.data ?? [];
- return arr.map((t: { team_id?: string; id?: string; team_alias?: string }) => ({
- team_id: t.team_id ?? t.id ?? "",
- team_alias: t.team_alias ?? t.team_id ?? "",
- models: [],
- max_budget: null,
- budget_duration: null,
- tpm_limit: null,
- rpm_limit: null,
- organization_id: "",
- created_at: "",
- keys: [],
- members_with_roles: [],
- spend: 0,
- }));
- }, [teamsData]);
-
const keys: KeyOption[] = useMemo(() => {
const keysRes = keysData?.keys ?? keysData?.data ?? [];
return keysRes.map((k: { token?: string; api_key?: string; key_hash?: string; key_alias?: string }) => ({
diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopKeyView.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopKeyView.tsx
index 60f25837589..1542b313334 100644
--- a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopKeyView.tsx
+++ b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopKeyView.tsx
@@ -20,7 +20,7 @@ interface TopKeyViewProps {
}
const TopKeyView: React.FC = ({ topKeys, teams, showTags = false, topKeysLimit, setTopKeysLimit }) => {
- const { accessToken, userRole, userId: userID, premiumUser } = useAuthorized();
+ const { accessToken } = useAuthorized();
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedKey, setSelectedKey] = useState(null);
const [keyData, setKeyData] = useState(undefined);
diff --git a/ui/litellm-dashboard/src/components/add_model/AddModelForm.tsx b/ui/litellm-dashboard/src/components/add_model/AddModelForm.tsx
index 03be04b9919..307f3665e83 100644
--- a/ui/litellm-dashboard/src/components/add_model/AddModelForm.tsx
+++ b/ui/litellm-dashboard/src/components/add_model/AddModelForm.tsx
@@ -66,7 +66,7 @@ const AddModelForm: React.FC = ({
} = useProviderFields();
const { data: guardrailsData } = useGuardrails();
const guardrailsList = guardrailsData?.guardrails.map((g) => g.guardrail_name);
- const { data: tagsList, isLoading: isTagsLoading, error: tagsError } = useTags();
+ const { data: tagsList } = useTags();
const handleTestConnection = async () => {
setIsTestingConnection(true);
diff --git a/ui/litellm-dashboard/src/components/add_model/RouterConfigBuilder.tsx b/ui/litellm-dashboard/src/components/add_model/RouterConfigBuilder.tsx
index 08acf993e2c..b28d402f116 100644
--- a/ui/litellm-dashboard/src/components/add_model/RouterConfigBuilder.tsx
+++ b/ui/litellm-dashboard/src/components/add_model/RouterConfigBuilder.tsx
@@ -126,13 +126,6 @@ const RouterConfigBuilder: React.FC = ({ modelInfo, va
};
// Handle utterances change (convert textarea string to array)
- const handleUtterancesChange = (routeId: string, utterancesText: string) => {
- const utterancesArray = utterancesText
- .split("\n")
- .map((line) => line.trim()) // Only trims leading/trailing whitespace, preserves internal spaces
- .filter((line) => line.length > 0);
- updateRoute(routeId, "utterances", utterancesArray);
- };
// Prepare model options for dropdowns
const modelOptions = modelInfo.map((model) => ({
diff --git a/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.tsx b/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.tsx
index 00d3364ac27..08041413d1f 100644
--- a/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.tsx
+++ b/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.tsx
@@ -191,11 +191,6 @@ const AddAutoRouterTab: React.FC = ({
const isAdmin = all_admin_roles.includes(userRole);
- const modelGroupOptions = Array.from(new Set(modelInfo.map((option) => option.model_group))).map((model_group) => ({
- value: model_group,
- label: model_group,
- }));
-
const availability = React.useMemo(
() =>
buildModelAvailability(
diff --git a/ui/litellm-dashboard/src/components/add_model/handle_add_model_submit.tsx b/ui/litellm-dashboard/src/components/add_model/handle_add_model_submit.tsx
index 71590660765..908a4c498dc 100644
--- a/ui/litellm-dashboard/src/components/add_model/handle_add_model_submit.tsx
+++ b/ui/litellm-dashboard/src/components/add_model/handle_add_model_submit.tsx
@@ -197,7 +197,7 @@ export const handleAddModelSubmit = async (values: any, accessToken: string, for
model_info: modelInfoObj,
};
- const response: any = await modelCreateCall(accessToken, new_model);
+ await modelCreateCall(accessToken, new_model);
}
callback && callback();
diff --git a/ui/litellm-dashboard/src/components/add_model/model_connection_test.tsx b/ui/litellm-dashboard/src/components/add_model/model_connection_test.tsx
index 5b284fd26f5..62bb5975478 100644
--- a/ui/litellm-dashboard/src/components/add_model/model_connection_test.tsx
+++ b/ui/litellm-dashboard/src/components/add_model/model_connection_test.tsx
@@ -24,7 +24,6 @@ const ModelConnectionTest: React.FC = ({
onTestComplete,
}) => {
const [error, setError] = React.useState(null);
- const [rawRequest, setRawRequest] = React.useState(null);
const [rawResponse, setRawResponse] = React.useState(null);
const [isLoading, setIsLoading] = React.useState(true);
const [isSuccess, setIsSuccess] = React.useState(false);
@@ -34,7 +33,6 @@ const ModelConnectionTest: React.FC = ({
setIsLoading(true);
setShowDetails(false);
setError(null);
- setRawRequest(null);
setRawResponse(null);
setIsSuccess(false);
@@ -51,7 +49,7 @@ const ModelConnectionTest: React.FC = ({
return;
}
- const { litellmParamsObj, modelInfoObj, modelName: returnedModelName } = result[0];
+ const { litellmParamsObj, modelInfoObj } = result[0];
const response = await testConnectionRequest(accessToken, litellmParamsObj, modelInfoObj, modelInfoObj?.mode);
if (response.status === "success") {
@@ -61,7 +59,6 @@ const ModelConnectionTest: React.FC = ({
} else {
const errorMessage = response.result?.error || response.message || "Unknown error";
setError(errorMessage);
- setRawRequest(litellmParamsObj);
setRawResponse(response.result?.raw_request_typed_dict);
setIsSuccess(false);
}
diff --git a/ui/litellm-dashboard/src/components/add_pass_through.tsx b/ui/litellm-dashboard/src/components/add_pass_through.tsx
index c0343e268a1..0c9fbfb0347 100644
--- a/ui/litellm-dashboard/src/components/add_pass_through.tsx
+++ b/ui/litellm-dashboard/src/components/add_pass_through.tsx
@@ -37,7 +37,6 @@ const AddPassThroughEndpoint: React.FC = ({
const [form] = Form.useForm();
const [isModalVisible, setIsModalVisible] = useState(false);
const [isLoading, setIsLoading] = useState(false);
- const [selectedModel, setSelectedModel] = useState("");
const [pathValue, setPathValue] = useState("");
const [targetValue, setTargetValue] = useState("");
const [includeSubpath, setIncludeSubpath] = useState(true);
@@ -107,11 +106,6 @@ const AddPassThroughEndpoint: React.FC = ({
}
};
- const copyToClipboard = (text: string) => {
- navigator.clipboard.writeText(text);
- NotificationsManager.success("Copied to clipboard!");
- };
-
return (
setIsModalVisible(true)}>
diff --git a/ui/litellm-dashboard/src/components/agent_management/AgentSelector.test.tsx b/ui/litellm-dashboard/src/components/agent_management/AgentSelector.test.tsx
index e483fd833c6..a41167df673 100644
--- a/ui/litellm-dashboard/src/components/agent_management/AgentSelector.test.tsx
+++ b/ui/litellm-dashboard/src/components/agent_management/AgentSelector.test.tsx
@@ -1,5 +1,4 @@
import { render, screen, waitFor } from "@testing-library/react";
-import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi, beforeEach } from "vitest";
// Mock networking module
diff --git a/ui/litellm-dashboard/src/components/bulk_create_users_button.tsx b/ui/litellm-dashboard/src/components/bulk_create_users_button.tsx
index 23b69722546..8faff9ca72f 100644
--- a/ui/litellm-dashboard/src/components/bulk_create_users_button.tsx
+++ b/ui/litellm-dashboard/src/components/bulk_create_users_button.tsx
@@ -80,24 +80,6 @@ const BulkCreateUsersButton: React.FC = ({
setBaseUrl(base.toString());
}, [accessToken]);
- const downloadTemplate = () => {
- const template = [
- ["user_email", "user_role", "teams", "max_budget", "budget_duration", "models"],
- ["user@example.com", "internal_user", "team-id-1,team-id-2", "100", "30d", "gpt-3.5-turbo,gpt-4"],
- ];
-
- const csv = Papa.unparse(template);
- const blob = new Blob([csv], { type: "text/csv" });
- const url = window.URL.createObjectURL(blob);
- const a = document.createElement("a");
- a.href = url;
- a.download = "bulk_users_template.csv";
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- window.URL.revokeObjectURL(url);
- };
-
const handleFileUpload = (file: File) => {
// Reset all error states
setParseError(null);
diff --git a/ui/litellm-dashboard/src/components/chat_ui/CodeSnippets.tsx b/ui/litellm-dashboard/src/components/chat_ui/CodeSnippets.tsx
index 576b094b5d3..f458e563b4d 100644
--- a/ui/litellm-dashboard/src/components/chat_ui/CodeSnippets.tsx
+++ b/ui/litellm-dashboard/src/components/chat_ui/CodeSnippets.tsx
@@ -43,9 +43,6 @@ export const generateCodeSnippet = (params: GenerateCodeParams): string => {
selectedVectorStores,
selectedGuardrails,
selectedPolicies,
- selectedMCPServers,
- mcpServers,
- mcpServerToolRestrictions,
selectedVoice,
endpointType,
selectedModel,
diff --git a/ui/litellm-dashboard/src/components/chat_ui/MCPEventsDisplay.tsx b/ui/litellm-dashboard/src/components/chat_ui/MCPEventsDisplay.tsx
index 895968c3dfa..b7ee480458a 100644
--- a/ui/litellm-dashboard/src/components/chat_ui/MCPEventsDisplay.tsx
+++ b/ui/litellm-dashboard/src/components/chat_ui/MCPEventsDisplay.tsx
@@ -1,8 +1,7 @@
import React from "react";
-import { Typography, Collapse } from "antd";
+import { Collapse } from "antd";
import type { MCPEvent } from "@/components/mcp_tools/types";
-const { Text } = Typography;
const { Panel } = Collapse;
interface MCPEventsDisplayProps {
diff --git a/ui/litellm-dashboard/src/components/common_components/DeleteResourceModal.tsx b/ui/litellm-dashboard/src/components/common_components/DeleteResourceModal.tsx
index 26419585a4c..5a6160483a0 100644
--- a/ui/litellm-dashboard/src/components/common_components/DeleteResourceModal.tsx
+++ b/ui/litellm-dashboard/src/components/common_components/DeleteResourceModal.tsx
@@ -32,7 +32,7 @@ export default function DeleteResourceModal({
confirmLoading,
requiredConfirmation,
}: DeleteResourceModalProps) {
- const { Title, Text } = Typography;
+ const { Text } = Typography;
const { token } = theme.useToken();
const [requiredConfirmationInput, setRequiredConfirmationInput] = useState("");
diff --git a/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx b/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx
index f9005e099a5..cf1a5727948 100644
--- a/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx
+++ b/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx
@@ -158,8 +158,6 @@ const EditAutoRouterModal: React.FC = ({
const [loading, setLoading] = useState(false);
const [modelAccessGroups, setModelAccessGroups] = useState([]);
const [modelInfo, setModelInfo] = useState([]);
- const [showCustomDefaultModel, setShowCustomDefaultModel] = useState(false);
- const [showCustomEmbeddingModel, setShowCustomEmbeddingModel] = useState(false);
const [showValidationErrors, setShowValidationErrors] = useState(false);
const [routerConfig, setRouterConfig] = useState(null);
const [customTechnicalKeywords, setCustomTechnicalKeywords] = useState([]);
@@ -308,11 +306,6 @@ const EditAutoRouterModal: React.FC = ({
auto_router_embedding_model: modelData.litellm_params?.auto_router_embedding_model || "",
model_access_group: modelData.model_info?.access_groups || [],
});
-
- // Check if using custom models
- const allModelGroups = new Set(modelInfo.map((model) => model.model_group));
- setShowCustomDefaultModel(!allModelGroups.has(modelData.litellm_params?.auto_router_default_model));
- setShowCustomEmbeddingModel(!allModelGroups.has(modelData.litellm_params?.auto_router_embedding_model));
} catch (error) {
console.error("Error parsing auto router config:", error);
NotificationsManager.fromBackend("Error loading auto router configuration");
@@ -516,9 +509,6 @@ const EditAutoRouterModal: React.FC = ({
>
{
- setShowCustomDefaultModel(value === "custom");
- }}
options={[...modelOptions, { value: "custom", label: "Enter custom model name" }]}
showSearch={true}
/>
@@ -532,9 +522,6 @@ const EditAutoRouterModal: React.FC = ({
>
{
- setShowCustomEmbeddingModel(value === "custom");
- }}
options={[...modelOptions, { value: "custom", label: "Enter custom model name" }]}
showSearch={true}
/>
diff --git a/ui/litellm-dashboard/src/components/llm_calls/chat_completion.tsx b/ui/litellm-dashboard/src/components/llm_calls/chat_completion.tsx
index c20d758fe91..549f309b4ea 100644
--- a/ui/litellm-dashboard/src/components/llm_calls/chat_completion.tsx
+++ b/ui/litellm-dashboard/src/components/llm_calls/chat_completion.tsx
@@ -73,10 +73,6 @@ export async function makeOpenAIChatCompletionRequest(
let firstTokenReceived = false;
let timeToFirstToken: number | undefined = undefined;
- // For collecting complete response text
- let fullResponseContent = "";
- let fullReasoningContent = "";
-
// Track MCP metadata cumulatively across chunks
let mcpMetadata: {
mcp_list_tools?: any[];
@@ -167,7 +163,6 @@ export async function makeOpenAIChatCompletionRequest(
if (chunk.choices[0]?.delta?.content) {
const content = chunk.choices[0].delta.content;
updateUI(content, chunk.model);
- fullResponseContent += content;
}
// Process image generation if present
@@ -181,7 +176,6 @@ export async function makeOpenAIChatCompletionRequest(
if (onReasoningContent) {
onReasoningContent(reasoningContent);
}
- fullReasoningContent += reasoningContent;
}
// Check for search results in provider_specific_fields
diff --git a/ui/litellm-dashboard/src/components/model_add/reuse_credentials.tsx b/ui/litellm-dashboard/src/components/model_add/reuse_credentials.tsx
index 715bb3bcbf8..85ba82ea046 100644
--- a/ui/litellm-dashboard/src/components/model_add/reuse_credentials.tsx
+++ b/ui/litellm-dashboard/src/components/model_add/reuse_credentials.tsx
@@ -2,7 +2,7 @@ import React from "react";
import { Form, Button, Tooltip, Typography, Modal } from "antd";
import { TextInput } from "@tremor/react";
import { CredentialItem } from "../networking";
-const { Title, Link } = Typography;
+const { Link } = Typography;
interface ReuseCredentialsModalProps {
isVisible: boolean;
diff --git a/ui/litellm-dashboard/src/components/model_filters.tsx b/ui/litellm-dashboard/src/components/model_filters.tsx
index c7b79f70690..bc9ca6bab44 100644
--- a/ui/litellm-dashboard/src/components/model_filters.tsx
+++ b/ui/litellm-dashboard/src/components/model_filters.tsx
@@ -121,13 +121,6 @@ const ModelFilters: React.FC = ({
};
// Expose filter values and reset function
- const filterValues = {
- searchTerm,
- selectedProvider,
- selectedMode,
- selectedFeature,
- resetFilters,
- };
const filtersContent = (
diff --git a/ui/litellm-dashboard/src/components/navbar.test.tsx b/ui/litellm-dashboard/src/components/navbar.test.tsx
index 4a8fe2a6094..72c15990f20 100644
--- a/ui/litellm-dashboard/src/components/navbar.test.tsx
+++ b/ui/litellm-dashboard/src/components/navbar.test.tsx
@@ -1,5 +1,5 @@
import userEvent from "@testing-library/user-event";
-import React, { useState } from "react";
+import React from "react";
import { describe, expect, it, vi } from "vitest";
import { renderWithProviders, screen, waitFor } from "../../tests/test-utils";
import Navbar from "./navbar";
diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx
index 8e91f7feecc..17a5ca37990 100644
--- a/ui/litellm-dashboard/src/components/networking.tsx
+++ b/ui/litellm-dashboard/src/components/networking.tsx
@@ -390,7 +390,6 @@ export const getAgentCreateMetadata = async (): Promise
=> {
// Global variable for the header name
let globalLitellmHeaderName: string = "Authorization";
-const MCP_AUTH_HEADER: string = "x-mcp-auth";
// Function to set the global header name
export function setGlobalLitellmHeaderName(headerName: string = "Authorization") {
@@ -1389,6 +1388,7 @@ export const userDailyActivityCall = async (
endTime: Date,
page: number = 1,
userId: string | null = null,
+ includeCurrentUtcDay: boolean = false,
) => {
/**
* Get daily user activity on proxy
@@ -1401,6 +1401,7 @@ export const userDailyActivityCall = async (
page,
extraQueryParams: {
user_id: userId,
+ include_current_utc_day: includeCurrentUtcDay ? "true" : undefined,
},
});
};
diff --git a/ui/litellm-dashboard/src/components/onboarding_link.tsx b/ui/litellm-dashboard/src/components/onboarding_link.tsx
index 0c27287a9e4..b5f3c6d3e17 100644
--- a/ui/litellm-dashboard/src/components/onboarding_link.tsx
+++ b/ui/litellm-dashboard/src/components/onboarding_link.tsx
@@ -58,7 +58,7 @@ export default function OnboardingModal({
invitationLinkData,
modalType = "invitation",
}: OnboardingProps) {
- const { Title, Paragraph } = Typography;
+ const { Paragraph } = Typography;
const handleInvitationOk = () => {
setIsInvitationLinkModalVisible(false);
};
diff --git a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx
index 00f36f016b7..b167e44fe04 100644
--- a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx
+++ b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx
@@ -93,26 +93,6 @@ interface UserOption {
user: User;
}
-const getPredefinedTags = (data: any[] | null) => {
- let allTags = [];
-
- if (data) {
- for (let key of data) {
- if (key["metadata"] && key["metadata"]["tags"]) {
- allTags.push(...key["metadata"]["tags"]);
- }
- }
- }
-
- // Deduplicate using Set
- const uniqueTags = Array.from(new Set(allTags)).map((tag) => ({
- value: tag,
- label: tag,
- }));
-
- return uniqueTags;
-};
-
export const fetchTeamModels = async (
userID: string,
userRole: string,
@@ -178,7 +158,6 @@ const CreateKey: React.FC = ({ team, teams, data, addKey, autoOp
const [form] = Form.useForm();
const [isModalVisible, setIsModalVisible] = useState(false);
const [apiKey, setApiKey] = useState(null);
- const [softBudget, setSoftBudget] = useState(null);
const [userModels, setUserModels] = useState([]);
const [modelsToPick, setModelsToPick] = useState([]);
const [keyOwner, setKeyOwner] = useState("you");
@@ -192,11 +171,9 @@ const CreateKey: React.FC = ({ team, teams, data, addKey, autoOp
const [selectedOrganizationId, setSelectedOrganizationId] = useState(null);
const [selectedProjectId, setSelectedProjectId] = useState(null);
const [isCreateUserModalVisible, setIsCreateUserModalVisible] = useState(false);
- const [newlyCreatedUserId, setNewlyCreatedUserId] = useState(null);
const [possibleUIRoles, setPossibleUIRoles] = useState>>({});
const [userOptions, setUserOptions] = useState([]);
const [userSearchLoading, setUserSearchLoading] = useState(false);
- const [mcpAccessGroups, setMcpAccessGroups] = useState([]);
const [disabledCallbacks, setDisabledCallbacks] = useState([]);
const [keyType, setKeyType] = useState("llm_api");
const [modelAliases, setModelAliases] = useState<{ [key: string]: string }>({});
@@ -578,7 +555,6 @@ const CreateKey: React.FC = ({ team, teams, data, addKey, autoOp
queryClient.invalidateQueries({ queryKey: keyKeys.lists() });
setApiKey(response["key"]);
- setSoftBudget(response["soft_budget"]);
NotificationsManager.success("Virtual Key Created");
form.resetFields();
setBudgetLimits([]);
@@ -592,10 +568,6 @@ const CreateKey: React.FC = ({ team, teams, data, addKey, autoOp
}
};
- const handleCopy = () => {
- NotificationsManager.success("Virtual Key copied to clipboard");
- };
-
// Fetch available models when team or auth changes.
// Note: Model prefill from URL params is handled by the useEffect below, which
// watches for pendingPrefillModels + modelsToPick to both be populated.
@@ -657,7 +629,6 @@ const CreateKey: React.FC = ({ team, teams, data, addKey, autoOp
// Add a callback function to handle user creation
const handleUserCreated = (userId: string) => {
- setNewlyCreatedUserId(userId);
form.setFieldsValue({ user_id: userId });
setIsCreateUserModalVisible(false);
};
diff --git a/ui/litellm-dashboard/src/components/organization/organization_view.tsx b/ui/litellm-dashboard/src/components/organization/organization_view.tsx
index 10af5bc1a07..a9f79810e1d 100644
--- a/ui/litellm-dashboard/src/components/organization/organization_view.tsx
+++ b/ui/litellm-dashboard/src/components/organization/organization_view.tsx
@@ -69,7 +69,7 @@ const OrganizationInfoView: React.FC = ({
user_id: values.user_id,
role: values.role,
};
- const response = await organizationMemberAddCall(accessToken, organizationId, member);
+ await organizationMemberAddCall(accessToken, organizationId, member);
NotificationsManager.success("Organization member added successfully");
setIsAddMemberModalVisible(false);
@@ -90,7 +90,7 @@ const OrganizationInfoView: React.FC = ({
role: values.role,
};
- const response = await organizationMemberUpdateCall(accessToken, organizationId, member);
+ await organizationMemberUpdateCall(accessToken, organizationId, member);
NotificationsManager.success("Organization member updated successfully");
setIsEditMemberModalVisible(false);
queryClient.invalidateQueries({ queryKey: organizationKeys.all });
diff --git a/ui/litellm-dashboard/src/components/pass_through_info.tsx b/ui/litellm-dashboard/src/components/pass_through_info.tsx
index 579911589f5..37d7e5d5da1 100644
--- a/ui/litellm-dashboard/src/components/pass_through_info.tsx
+++ b/ui/litellm-dashboard/src/components/pass_through_info.tsx
@@ -72,7 +72,7 @@ const PassThroughInfoView: React.FC = ({
onEndpointUpdated,
}) => {
const [endpointData, setEndpointData] = useState(initialEndpointData);
- const [loading, setLoading] = useState(false);
+ const [loading] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const [authEnabled, setAuthEnabled] = useState(initialEndpointData?.auth || false);
const [selectedMethods, setSelectedMethods] = useState(initialEndpointData?.methods || []);
diff --git a/ui/litellm-dashboard/src/components/per_user_usage.tsx b/ui/litellm-dashboard/src/components/per_user_usage.tsx
index 30933700cf3..e542be8df8f 100644
--- a/ui/litellm-dashboard/src/components/per_user_usage.tsx
+++ b/ui/litellm-dashboard/src/components/per_user_usage.tsx
@@ -55,13 +55,11 @@ const PerUserUsage: React.FC = ({ accessToken, selectedTags,
total_pages: 0,
});
- const [loading, setLoading] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const fetchPerUserData = async () => {
if (!accessToken) return;
- setLoading(true);
try {
const response = await perUserAnalyticsCall(
accessToken,
@@ -72,8 +70,6 @@ const PerUserUsage: React.FC = ({ accessToken, selectedTags,
setPerUserData(response);
} catch (error) {
console.error("Failed to fetch per-user data:", error);
- } finally {
- setLoading(false);
}
};
diff --git a/ui/litellm-dashboard/src/components/price_data_reload.tsx b/ui/litellm-dashboard/src/components/price_data_reload.tsx
index e0ec0fb794d..f56387d283c 100644
--- a/ui/litellm-dashboard/src/components/price_data_reload.tsx
+++ b/ui/litellm-dashboard/src/components/price_data_reload.tsx
@@ -60,9 +60,7 @@ const PriceDataReload: React.FC = ({
const [showScheduleModal, setShowScheduleModal] = useState(false);
const [hours, setHours] = useState(6);
const [reloadStatus, setReloadStatus] = useState(null);
- const [loadingStatus, setLoadingStatus] = useState(false);
const [sourceInfo, setSourceInfo] = useState(null);
- const [loadingSource, setLoadingSource] = useState(false);
// Fetch status on component mount and periodically
useEffect(() => {
@@ -81,7 +79,6 @@ const PriceDataReload: React.FC = ({
const fetchReloadStatus = async () => {
if (!accessToken) return;
- setLoadingStatus(true);
try {
const status = await getModelCostMapReloadStatus(accessToken);
setReloadStatus(status);
@@ -94,22 +91,17 @@ const PriceDataReload: React.FC = ({
last_run: null,
next_run: null,
});
- } finally {
- setLoadingStatus(false);
}
};
const fetchSourceInfo = async () => {
if (!accessToken) return;
- setLoadingSource(true);
try {
const info = await getModelCostMapSource(accessToken);
setSourceInfo(info);
} catch (error) {
console.error("Failed to fetch cost map source info:", error);
- } finally {
- setLoadingSource(false);
}
};
diff --git a/ui/litellm-dashboard/src/components/settings.tsx b/ui/litellm-dashboard/src/components/settings.tsx
index 72ddb26f045..904fd4d611e 100644
--- a/ui/litellm-dashboard/src/components/settings.tsx
+++ b/ui/litellm-dashboard/src/components/settings.tsx
@@ -20,13 +20,11 @@ import {
} from "@tremor/react";
import React, { useEffect, useState } from "react";
-import { Button as Button2, Form, Input, Modal, Select, Typography } from "antd";
+import { Button as Button2, Form, Input, Modal, Select } from "antd";
import EmailSettings from "./email_settings";
import { Logo } from "@/components/molecules/logo/Logo";
import NotificationsManager from "./molecules/notifications_manager";
-const { Title, Paragraph } = Typography;
-
import FormItem from "antd/es/form/FormItem";
import AlertingSettings from "./alerting/alerting_settings";
import CloudZeroCostTracking from "./CloudZeroCostTracking/CloudZeroCostTracking";
@@ -48,12 +46,6 @@ interface SettingsPageProps {
premiumUser: boolean;
}
-interface genericCallbackParams {
- litellm_callback_name: string; // what to send in request
- ui_callback_name: string; // what to show on UI
- litellm_callback_params: string[] | null; // known required params for this callback
-}
-
const assetsLogoFolder = "/ui/assets/logos/";
export const backendCallbackLogoSrc = (logo: string | null | undefined): string | undefined => {
@@ -214,7 +206,6 @@ const Settings: React.FC = ({ accessToken, userRole, userID,
const [callbacks, setCallbacks] = useState([]);
const [isLoadingCallbacks, setIsLoadingCallbacks] = useState(true);
const [alerts, setAlerts] = useState([]);
- const [isModalVisible, setIsModalVisible] = useState(false);
const [addForm] = Form.useForm();
const [editForm] = Form.useForm();
const [selectedCallback, setSelectedCallback] = useState(null);
@@ -418,119 +409,6 @@ const Settings: React.FC = ({ accessToken, userRole, userID,
}
NotificationsManager.success("Alerts updated successfully");
};
- const handleSaveChanges = (callback: any) => {
- if (!accessToken) {
- return;
- }
-
- const updatedVariables = Object.fromEntries(
- Object.entries(callback.variables).map(([key, value]) => [
- key,
- (document.querySelector(`input[name="${key}"]`) as HTMLInputElement)?.value || value,
- ]),
- );
-
- const payload = {
- environment_variables: updatedVariables,
- litellm_settings: {
- success_callback: [callback.name],
- },
- };
-
- try {
- setCallbacksCall(accessToken, payload);
- } catch (error) {
- NotificationsManager.fromBackend(error);
- }
- NotificationsManager.success("Callback updated successfully");
- };
-
- const handleOk = () => {
- if (!accessToken) {
- return;
- }
- // Handle form submission
- addForm.validateFields().then((values) => {
- // Call API to add the callback
- let payload;
- if (values.callback === "langfuse" || values.callback === "langfuse_otel") {
- payload = {
- environment_variables: {
- LANGFUSE_PUBLIC_KEY: values.langfusePublicKey,
- LANGFUSE_SECRET_KEY: values.langfusePrivateKey,
- },
- litellm_settings: {
- success_callback: [values.callback],
- },
- };
- setCallbacksCall(accessToken, payload);
- let newCallback: AlertingObject = {
- name: values.callback,
- variables: {
- SLACK_WEBHOOK_URL: null,
- LANGFUSE_HOST: null,
- LANGFUSE_PUBLIC_KEY: values.langfusePublicKey,
- LANGFUSE_SECRET_KEY: values.langfusePrivateKey,
- OPENMETER_API_KEY: null,
- },
- };
- // add langfuse to callbacks
- setCallbacks(callbacks ? [...callbacks, newCallback] : [newCallback]);
- } else if (values.callback === "slack") {
- payload = {
- general_settings: {
- alerting: ["slack"],
- alerting_threshold: 300,
- },
- environment_variables: {
- SLACK_WEBHOOK_URL: values.slackWebhookUrl,
- },
- };
- setCallbacksCall(accessToken, payload);
-
- let newCallback: AlertingObject = {
- name: values.callback,
- variables: {
- SLACK_WEBHOOK_URL: values.slackWebhookUrl,
- LANGFUSE_HOST: null,
- LANGFUSE_PUBLIC_KEY: null,
- LANGFUSE_SECRET_KEY: null,
- OPENMETER_API_KEY: null,
- },
- };
- setCallbacks(callbacks ? [...callbacks, newCallback] : [newCallback]);
- } else if (values.callback == "openmeter") {
- payload = {
- environment_variables: {
- OPENMETER_API_KEY: values.openMeterApiKey,
- },
- litellm_settings: {
- success_callback: [values.callback],
- },
- };
- setCallbacksCall(accessToken, payload);
- let newCallback: AlertingObject = {
- name: values.callback,
- variables: {
- SLACK_WEBHOOK_URL: null,
- LANGFUSE_HOST: null,
- LANGFUSE_PUBLIC_KEY: null,
- LANGFUSE_SECRET_KEY: null,
- OPENMETER_API_KEY: values.openMeterAPIKey,
- },
- };
- // add langfuse to callbacks
- setCallbacks(callbacks ? [...callbacks, newCallback] : [newCallback]);
- } else {
- payload = {
- error: "Invalid callback value",
- };
- }
- setIsModalVisible(false);
- addForm.resetFields();
- setSelectedCallback(null);
- });
- };
const handleDeleteCallback = (callback: any) => {
setCallbackToDelete(callback);
diff --git a/ui/litellm-dashboard/src/components/shared/DataTable/columnMeta.ts b/ui/litellm-dashboard/src/components/shared/DataTable/columnMeta.ts
index eff4e0cb7db..39a99f1e861 100644
--- a/ui/litellm-dashboard/src/components/shared/DataTable/columnMeta.ts
+++ b/ui/litellm-dashboard/src/components/shared/DataTable/columnMeta.ts
@@ -4,6 +4,7 @@ import type * as React from "react";
import type { ColumnPinnedSide, DataTableSkeletonShape } from "./types";
declare module "@tanstack/react-table" {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- declaration merging requires the type parameters to match the upstream ColumnMeta signature exactly (TS2428)
interface ColumnMeta {
numeric?: boolean;
className?: string;
diff --git a/ui/litellm-dashboard/src/components/shared/table_cells/identity_cell.test.tsx b/ui/litellm-dashboard/src/components/shared/table_cells/identity_cell.test.tsx
index f3ec203adbb..9a91e163baf 100644
--- a/ui/litellm-dashboard/src/components/shared/table_cells/identity_cell.test.tsx
+++ b/ui/litellm-dashboard/src/components/shared/table_cells/identity_cell.test.tsx
@@ -4,6 +4,9 @@ import { describe, expect, it, vi } from "vitest";
import { IdentityCell } from "./identity_cell";
+const routerPush = vi.fn();
+vi.mock("next/navigation", () => ({ useRouter: () => ({ push: routerPush }) }));
+
describe("IdentityCell", () => {
it("renders the title", () => {
render( );
@@ -38,4 +41,25 @@ describe("IdentityCell", () => {
await user.click(button);
expect(onClick).toHaveBeenCalledTimes(1);
});
+
+ it("renders an anchor with the chevron affordance and routes through the client router when href is set", async () => {
+ const user = userEvent.setup();
+ render( );
+ const link = screen.getByRole("link", { name: /routing-qa-key-alpha/ });
+ expect(link).toHaveAttribute("href", "/api-keys?key=abc123");
+ expect(link.querySelector(".lucide-chevron-right")).not.toBeNull();
+ expect(link.className).toContain("hover:bg-muted");
+ await user.click(link);
+ expect(routerPush).toHaveBeenCalledWith("/api-keys?key=abc123");
+ });
+
+ it("leaves modifier clicks to the browser so open-in-new-tab keeps working", async () => {
+ routerPush.mockClear();
+ const user = userEvent.setup();
+ render( );
+ await user.keyboard("{Meta>}");
+ await user.click(screen.getByRole("link", { name: /routing-qa-key-alpha/ }));
+ await user.keyboard("{/Meta}");
+ expect(routerPush).not.toHaveBeenCalled();
+ });
});
diff --git a/ui/litellm-dashboard/src/components/shared/table_cells/identity_cell.tsx b/ui/litellm-dashboard/src/components/shared/table_cells/identity_cell.tsx
index 97263436454..62ae04bbf88 100644
--- a/ui/litellm-dashboard/src/components/shared/table_cells/identity_cell.tsx
+++ b/ui/litellm-dashboard/src/components/shared/table_cells/identity_cell.tsx
@@ -1,6 +1,7 @@
"use client";
import { ChevronRight } from "lucide-react";
+import { useRouter } from "next/navigation";
import * as React from "react";
import { cn } from "@/lib/cva.config";
@@ -10,11 +11,19 @@ interface IdentityCellProps {
subtitle?: React.ReactNode;
badge?: React.ReactNode;
onClick?: () => void;
+ href?: string;
className?: string;
titleClassName?: string;
}
-export function IdentityCell({ title, subtitle, badge, onClick, className, titleClassName }: IdentityCellProps) {
+const INTERACTIVE_CELL_CLASSES =
+ "group -mx-2 flex w-[calc(100%+1rem)] cursor-pointer items-center gap-2 rounded-md px-2 py-1 text-left transition-colors hover:bg-muted";
+
+const HoverChevron = () => (
+
+);
+
+export function IdentityCell({ title, subtitle, badge, onClick, href, className, titleClassName }: IdentityCellProps) {
const hasSubtitleRow = (subtitle != null && subtitle !== "") || badge != null;
const body = (
@@ -31,21 +40,37 @@ export function IdentityCell({ title, subtitle, badge, onClick, className, title
);
+ if (href != null) {
+ return ;
+ }
+
if (onClick != null) {
return (
-
+
{body}
-
+
);
}
return {body}
;
}
+
+function IdentityCellLink({ href, className, body }: { href: string; className?: string; body: React.ReactNode }) {
+ const router = useRouter();
+
+ const handleClick = (e: React.MouseEvent) => {
+ const hasModifierKey = e.metaKey || e.ctrlKey || e.shiftKey;
+ const isNativeNewTabClick = hasModifierKey || e.button === 1;
+ if (isNativeNewTabClick) return;
+ e.preventDefault();
+ router.push(href);
+ };
+
+ return (
+
+ {body}
+
+
+ );
+}
diff --git a/ui/litellm-dashboard/src/components/team/LoggingSettings.test.tsx b/ui/litellm-dashboard/src/components/team/LoggingSettings.test.tsx
index e724e32d1c4..f998d7d3ecf 100644
--- a/ui/litellm-dashboard/src/components/team/LoggingSettings.test.tsx
+++ b/ui/litellm-dashboard/src/components/team/LoggingSettings.test.tsx
@@ -1,6 +1,5 @@
import React from "react";
import { describe, it, expect, vi, beforeEach } from "vitest";
-import userEvent from "@testing-library/user-event";
import { renderWithProviders, screen, fireEvent } from "../../../tests/test-utils";
import LoggingSettings from "./LoggingSettings";
@@ -10,7 +9,6 @@ describe("LoggingSettings", () => {
});
it("passes a number to updateCallbackVar when user inputs a number in NumericalInput", async () => {
- const user = userEvent.setup();
const mockOnChange = vi.fn();
// Create initial config with a callback that has number parameters (LangSmith has langsmith_sampling_rate)
diff --git a/ui/litellm-dashboard/src/components/team/TeamInfo.tsx b/ui/litellm-dashboard/src/components/team/TeamInfo.tsx
index bbe5dc05a88..b7bbbba3aed 100644
--- a/ui/litellm-dashboard/src/components/team/TeamInfo.tsx
+++ b/ui/litellm-dashboard/src/components/team/TeamInfo.tsx
@@ -52,7 +52,6 @@ import MCPServerSelector from "../mcp_server_management/MCPServerSelector";
import MCPToolPermissions from "../mcp_server_management/MCPToolPermissions";
import { ModelSelect } from "../ModelSelect/ModelSelect";
import NotificationsManager from "../molecules/notifications_manager";
-import { fetchMCPAccessGroups } from "../networking";
import ObjectPermissionsView from "../object_permissions_view";
import NumericalInput from "../shared/numerical_input";
import VectorStoreSelector from "../vector_store_management/VectorStoreSelector";
@@ -161,29 +160,6 @@ export interface TeamInfoProps {
premiumUser?: boolean;
}
-const getOrganizationModels = (organization: Organization | null, userModels: string[]) => {
- let tempModelsToPick = [];
-
- if (organization) {
- // Check if organization has "all-proxy-models" in its models array
- if (organization.models.includes("all-proxy-models")) {
- // Treat as all-proxy-models (use userModels)
- tempModelsToPick = userModels;
- } else if (organization.models.length > 0) {
- // Organization has specific models
- tempModelsToPick = organization.models;
- } else {
- // Empty array [] is treated as all-proxy-models
- tempModelsToPick = userModels;
- }
- } else {
- // No organization, show all available models
- tempModelsToPick = userModels;
- }
-
- return unfurlWildcardModelsInList(tempModelsToPick, userModels);
-};
-
const TeamInfoView: React.FC = ({
teamId,
onClose,
@@ -203,8 +179,6 @@ const TeamInfoView: React.FC = ({
const [isEditMemberModalVisible, setIsEditMemberModalVisible] = useState(false);
const [selectedEditMember, setSelectedEditMember] = useState(null);
const [isEditing, setIsEditing] = useState(false);
- const [mcpAccessGroups, setMcpAccessGroups] = useState([]);
- const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false);
const [copiedStates, setCopiedStates] = useState>({});
const { data: guardrailsData, isLoading: isGuardrailsLoading } = useGuardrails();
const globalGuardrailNames = guardrailsData?.globalGuardrailNames ?? new Set();
@@ -293,23 +267,6 @@ const TeamInfoView: React.FC = ({
fetchOrganization();
}, [accessToken, teamData?.team_info?.organization_id]);
- // Compute modelsToPick based on organization and userModels
- const modelsToPick = useMemo(() => {
- return getOrganizationModels(organization, userModels);
- }, [organization, userModels]);
-
- const fetchMcpAccessGroups = async () => {
- if (!accessToken) return;
- if (mcpAccessGroupsLoaded) return;
- try {
- const groups = await fetchMCPAccessGroups(accessToken);
- setMcpAccessGroups(groups);
- setMcpAccessGroupsLoaded(true);
- } catch (error) {
- console.error("Failed to fetch MCP access groups:", error);
- }
- };
-
useEffect(() => {
const fetchPolicies = async () => {
try {
@@ -653,7 +610,7 @@ const TeamInfoView: React.FC = ({
}
}
- const response = await teamUpdateCall(accessToken, updateData);
+ await teamUpdateCall(accessToken, updateData);
queryClient.invalidateQueries({ queryKey: organizationKeys.all });
NotificationsManager.success("Team settings updated successfully");
diff --git a/ui/litellm-dashboard/src/components/templates/KeyInfoView.handleKeyUpdate.test.tsx b/ui/litellm-dashboard/src/components/templates/KeyInfoView.handleKeyUpdate.test.tsx
index fa8ffcd7366..3b001e39a25 100644
--- a/ui/litellm-dashboard/src/components/templates/KeyInfoView.handleKeyUpdate.test.tsx
+++ b/ui/litellm-dashboard/src/components/templates/KeyInfoView.handleKeyUpdate.test.tsx
@@ -196,32 +196,28 @@ vi.mock("lucide-react", async () => {
});
// Heavy children -> async factories & local React
-vi.mock("../organisms/RegenerateKeyModal", async () => {
- const React = await import("react");
+vi.mock("../organisms/RegenerateKeyModal", () => {
function RegenerateKeyModal() {
return null;
}
(RegenerateKeyModal as any).displayName = "RegenerateKeyModal";
return { RegenerateKeyModal };
});
-vi.mock("../object_permissions_view", async () => {
- const React = await import("react");
+vi.mock("../object_permissions_view", () => {
function ObjectPermissionsView() {
return null;
}
(ObjectPermissionsView as any).displayName = "ObjectPermissionsView";
return { __esModule: true, default: ObjectPermissionsView };
});
-vi.mock("../logging_settings_view", async () => {
- const React = await import("react");
+vi.mock("../logging_settings_view", () => {
function LoggingSettingsView() {
return null;
}
(LoggingSettingsView as any).displayName = "LoggingSettingsView";
return { __esModule: true, default: LoggingSettingsView };
});
-vi.mock("../common_components/AutoRotationView", async () => {
- const React = await import("react");
+vi.mock("../common_components/AutoRotationView", () => {
function AutoRotationView() {
return null;
}
diff --git a/ui/litellm-dashboard/src/components/templates/key_edit_view.test.tsx b/ui/litellm-dashboard/src/components/templates/key_edit_view.test.tsx
index 756481487e7..e93b28f4ea3 100644
--- a/ui/litellm-dashboard/src/components/templates/key_edit_view.test.tsx
+++ b/ui/litellm-dashboard/src/components/templates/key_edit_view.test.tsx
@@ -1007,7 +1007,7 @@ describe("KeyEditView", () => {
});
it("should disable the organization dropdown for non-admin users", async () => {
- const { container } = renderWithProviders(
+ renderWithProviders(
{}}
@@ -1029,7 +1029,7 @@ describe("KeyEditView", () => {
});
it("should not disable the organization dropdown for admin users", async () => {
- const { container } = renderWithProviders(
+ renderWithProviders(
{}}
diff --git a/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx b/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx
index 4b6c266eb8f..e7af6b2923f 100644
--- a/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx
+++ b/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx
@@ -50,22 +50,6 @@ interface KeyEditViewProps {
}
// Add this helper function
-const getAvailableModelsForKey = (keyData: KeyResponse, teams: any[] | null): string[] => {
- // If no teams data is available, return empty array
- if (!teams || !keyData.team_id) {
- return [];
- }
-
- // Find the team that matches the key's team_id
- const keyTeam = teams.find((team) => team.team_id === keyData.team_id);
-
- // If team found and has models, return those models
- if (keyTeam?.models) {
- return keyTeam.models;
- }
-
- return [];
-};
// Helper function to determine key_type display value from allowed_routes
const getKeyTypeFromRoutes = (allowedRoutes: string[] | null | undefined): string => {
diff --git a/ui/litellm-dashboard/src/components/templates/key_info_view.test.tsx b/ui/litellm-dashboard/src/components/templates/key_info_view.test.tsx
index e46c6fd5c30..98339f6ffc8 100644
--- a/ui/litellm-dashboard/src/components/templates/key_info_view.test.tsx
+++ b/ui/litellm-dashboard/src/components/templates/key_info_view.test.tsx
@@ -1,11 +1,9 @@
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
-import { useProjects } from "@/app/(dashboard)/hooks/projects/useProjects";
import useTeams from "@/app/(dashboard)/hooks/useTeams";
import { renderWithProviders } from "../../../tests/test-utils";
import { screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
-import { useResetKeySpend } from "@/app/(dashboard)/hooks/keys/useResetKeySpend";
import { KeyResponse, Team } from "../key_team_helpers/key_list";
import { keyDeleteCall, keyUpdateCall } from "../networking";
import { QueryClient } from "@tanstack/react-query";
diff --git a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx
index 280abb3dac7..f4803727875 100644
--- a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx
+++ b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx
@@ -6,7 +6,7 @@ import { formatNumberWithCommas } from "@/utils/dataUtils";
import { mapEmptyStringToNull } from "@/utils/keyUpdateUtils";
import { ArrowLeftIcon } from "@heroicons/react/outline";
import { Badge, Button, Card, Grid, Tab, TabGroup, TabList, TabPanel, TabPanels, Text, Title } from "@tremor/react";
-import { Form, Modal, Tag } from "antd";
+import { Modal, Tag } from "antd";
import { KeyInfoHeader } from "./KeyInfoHeader";
import { useEffect, useState } from "react";
import { isProxyAdminRole, isUserTeamAdminForSingleTeam, rolesWithWriteAccess } from "../../utils/roles";
@@ -74,10 +74,8 @@ export default function KeyInfoView({
const { data: uiSettingsData } = useUISettings();
const enableProjectsUI = Boolean(uiSettingsData?.values?.enable_projects_ui);
const [isEditing, setIsEditing] = useState(false);
- const [form] = Form.useForm();
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [deleteLoading, setDeleteLoading] = useState(false);
- const [deleteConfirmInput, setDeleteConfirmInput] = useState("");
const [isRegenerateModalOpen, setIsRegenerateModalOpen] = useState(false);
const [isResetSpendModalOpen, setIsResetSpendModalOpen] = useState(false);
const [isBlockModalOpen, setIsBlockModalOpen] = useState(false);
@@ -340,7 +338,6 @@ export default function KeyInfoView({
} finally {
setDeleteLoading(false);
setIsDeleteModalOpen(false);
- setDeleteConfirmInput("");
}
};
@@ -526,7 +523,6 @@ export default function KeyInfoView({
]}
onCancel={() => {
setIsDeleteModalOpen(false);
- setDeleteConfirmInput("");
}}
onOk={handleDelete}
confirmLoading={deleteLoading}
diff --git a/ui/litellm-dashboard/src/components/user_agent_activity.tsx b/ui/litellm-dashboard/src/components/user_agent_activity.tsx
index 6d262ec2323..29a5336e26f 100644
--- a/ui/litellm-dashboard/src/components/user_agent_activity.tsx
+++ b/ui/litellm-dashboard/src/components/user_agent_activity.tsx
@@ -38,10 +38,6 @@ interface DistinctTagResponse {
tag: string;
}
-interface DistinctTagsResponse {
- results: DistinctTagResponse[];
-}
-
interface UserAgentActivityProps {
accessToken: string | null;
userRole: string | null;
@@ -59,7 +55,7 @@ const UserAgentActivity: React.FC = ({ accessToken, user
const [mauData, setMauData] = useState({ results: [] });
const [summaryData, setSummaryData] = useState({ results: [] });
- const [userAgentFilter, setUserAgentFilter] = useState("");
+ const [userAgentFilter] = useState("");
// Tag filtering state
const [availableTags, setAvailableTags] = useState([]);
diff --git a/ui/litellm-dashboard/src/components/user_dashboard.tsx b/ui/litellm-dashboard/src/components/user_dashboard.tsx
index 1ed4e1d0bba..1de232fadb8 100644
--- a/ui/litellm-dashboard/src/components/user_dashboard.tsx
+++ b/ui/litellm-dashboard/src/components/user_dashboard.tsx
@@ -6,14 +6,7 @@ import React, { useEffect, useState } from "react";
import { fetchTeams } from "./common_components/fetch_teams";
import { KeyResponse, Team } from "./key_team_helpers/key_list";
import { effectiveSessionRole } from "@/utils/roles";
-import {
- getProxyBaseUrl,
- getProxyUISettings,
- keyInfoCall,
- modelAvailableCall,
- Organization,
- userGetInfoV2,
-} from "./networking";
+import { getProxyBaseUrl, keyInfoCall, modelAvailableCall, Organization, userGetInfoV2 } from "./networking";
import CreateKey, { CreateKeyPrefillData } from "./organisms/create_key_button";
import { VirtualKeysTable } from "./VirtualKeysPage/VirtualKeysTable";
@@ -50,12 +43,6 @@ interface UserDashboardProps {
prefillData?: CreateKeyPrefillData;
}
-type TeamInterface = {
- models: any[];
- team_id: null;
- team_alias: string;
-};
-
const UserDashboard: React.FC = ({
userID,
userRole,
@@ -73,15 +60,12 @@ const UserDashboard: React.FC = ({
prefillData,
}) => {
const [userSpendData, setUserSpendData] = useState(null);
- const [currentOrg, setCurrentOrg] = useState(null);
+ const [currentOrg] = useState(null);
const token = getCookie("token");
const [accessToken, setAccessToken] = useState(null);
- const [teamSpend, setTeamSpend] = useState(null);
- const [userModels, setUserModels] = useState([]);
- const [proxySettings, setProxySettings] = useState(null);
- const [selectedTeam, setSelectedTeam] = useState(null);
+ const [selectedTeam] = useState(null);
// Clear session storage on page unload so next load fetches fresh data.
// Note: MCP auth tokens are persistent and should not be cleared on page refresh
@@ -123,14 +107,9 @@ const UserDashboard: React.FC = ({
}
if (userID && accessToken && userRole && !userSpendData) {
const cachedUserModels = sessionStorage.getItem("userModels" + userID);
- if (cachedUserModels) {
- setUserModels(JSON.parse(cachedUserModels));
- } else {
+ if (!cachedUserModels) {
const fetchData = async () => {
try {
- const proxy_settings: ProxySettings = await getProxyUISettings(accessToken);
- setProxySettings(proxy_settings);
-
const response = await userGetInfoV2(accessToken, userID);
setUserSpendData(response);
@@ -140,7 +119,6 @@ const UserDashboard: React.FC = ({
const model_available = await modelAvailableCall(accessToken, userID, userRole);
// loop through model_info["data"] and create an array of element.model_name
let available_model_names = model_available["data"].map((element: { id: string }) => element.id);
- setUserModels(available_model_names);
sessionStorage.setItem("userModels" + userID, JSON.stringify(available_model_names));
} catch (error: any) {
@@ -162,7 +140,7 @@ const UserDashboard: React.FC = ({
if (accessToken) {
const fetchKeyInfo = async () => {
try {
- const keyInfo = await keyInfoCall(accessToken, [accessToken]);
+ await keyInfoCall(accessToken, [accessToken]);
} catch (error: any) {
if (error.message.includes("Invalid proxy server token passed")) {
gotoLogin();
@@ -179,26 +157,6 @@ const UserDashboard: React.FC = ({
}
}, [currentOrg]);
- useEffect(() => {
- // This code will run every time selectedTeam changes
- if (keys !== null && selectedTeam !== null && selectedTeam !== undefined && selectedTeam.team_id !== null) {
- let sum = 0;
- for (const key of keys) {
- if (selectedTeam.hasOwnProperty("team_id") && key.team_id !== null && key.team_id === selectedTeam.team_id) {
- sum += key.spend;
- }
- }
- setTeamSpend(sum);
- } else if (keys !== null) {
- // sum the keys which don't have team-id set (default team)
- let sum = 0;
- for (const key of keys) {
- sum += key.spend;
- }
- setTeamSpend(sum);
- }
- }, [selectedTeam]);
-
function gotoLogin() {
// Clear token cookies using the utility function
clearTokenCookies();
diff --git a/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx b/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx
index 0a375fa130c..4625f500c6d 100644
--- a/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx
@@ -235,18 +235,6 @@ const DownloadIcon = () => (
);
-const ExternalLinkIcon = () => (
-
-
-
-);
-
// ── Sub-components ──────────────────────────────────────────────────────────
const MatchDetailsTable = ({ matchDetails }: { matchDetails: MatchDetail[] }) => {
@@ -647,10 +635,6 @@ const GuardrailViewer = ({ data, accessToken, logEntry }: GuardrailViewerProps)
return Math.round(guardrailEntries.reduce((sum, e) => sum + (e.duration ?? 0), 0) * 1000);
}, [guardrailEntries]);
- const policyTemplates = useMemo(() => {
- return Array.from(new Set(guardrailEntries.map((e) => e.policy_template).filter(Boolean)));
- }, [guardrailEntries]);
-
if (guardrailEntries.length === 0) {
return null;
}
diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/RealtimePrettyView.test.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/RealtimePrettyView.test.tsx
index 286b55ca6d6..4f8f662aa16 100644
--- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/RealtimePrettyView.test.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/RealtimePrettyView.test.tsx
@@ -1,5 +1,5 @@
import React from "react";
-import { render, screen, fireEvent, act, waitFor } from "@testing-library/react";
+import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { RealtimePrettyView, isRealtimeResponse } from "./RealtimePrettyView";
diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/SimpleToolCallBlock.test.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/SimpleToolCallBlock.test.tsx
index 107b911e386..0eb61686b73 100644
--- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/SimpleToolCallBlock.test.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/SimpleToolCallBlock.test.tsx
@@ -31,7 +31,7 @@ describe("SimpleToolCallBlock", () => {
});
it("should not render arguments section when arguments are empty", () => {
- const { container } = render( );
+ render( );
// The tool name and "function" badge should be there, but no key: value pairs
expect(screen.getByText("get_weather")).toBeInTheDocument();
expect(screen.queryByText(/:$/)).not.toBeInTheDocument();
diff --git a/ui/litellm-dashboard/src/components/view_logs/table.tsx b/ui/litellm-dashboard/src/components/view_logs/table.tsx
index c96f34f9b93..d522478e370 100644
--- a/ui/litellm-dashboard/src/components/view_logs/table.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/table.tsx
@@ -14,6 +14,7 @@ import {
import { Table, TableHeader, TableHead, TableBody, TableRow, TableCell } from "@/components/ui/table";
declare module "@tanstack/react-table" {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- declaration merging requires the type parameters to match the upstream ColumnMeta signature exactly (TS2428)
interface ColumnMeta {
numeric?: boolean;
}
diff --git a/ui/litellm-dashboard/src/components/view_user_spend.tsx b/ui/litellm-dashboard/src/components/view_user_spend.tsx
index b5a4769e797..b5d54ae3bcb 100644
--- a/ui/litellm-dashboard/src/components/view_user_spend.tsx
+++ b/ui/litellm-dashboard/src/components/view_user_spend.tsx
@@ -5,11 +5,6 @@ import { formatNumberWithCommas } from "@/utils/dataUtils";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
// Define the props type
-interface UserSpendData {
- spend: number; // Adjust the type accordingly based on your data
- max_budget?: number | null; // Optional property with a default of null
- // Add other properties if needed
-}
interface ViewUserSpendProps {
userSpend: number | null;
userMaxBudget: number | null;
diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts
index 65434407f74..752572f9863 100644
--- a/ui/litellm-dashboard/src/lib/http/schema.d.ts
+++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts
@@ -26716,6 +26716,8 @@ export interface components {
auto_router_max_input_chars?: number | null;
/** Aws Access Key Id */
aws_access_key_id?: string | null;
+ /** Aws Batch Role Arn */
+ aws_batch_role_arn?: string | null;
/** Aws Bedrock Project Id */
aws_bedrock_project_id?: string | null;
/** Aws Bedrock Runtime Endpoint */
@@ -26895,6 +26897,10 @@ export interface components {
output_cost_per_pixel?: number | null;
/** Output Cost Per Reasoning Token */
output_cost_per_reasoning_token?: number | null;
+ /** Output Cost Per Reasoning Token Flex */
+ output_cost_per_reasoning_token_flex?: number | null;
+ /** Output Cost Per Reasoning Token Priority */
+ output_cost_per_reasoning_token_priority?: number | null;
/** Output Cost Per Second */
output_cost_per_second?: number | null;
/** Output Cost Per Second 1080P */
@@ -26945,6 +26951,8 @@ export interface components {
s3_bucket_name?: string | null;
/** S3 Encryption Key Id */
s3_encryption_key_id?: string | null;
+ /** S3 Region Name */
+ s3_region_name?: string | null;
/** Search Context Cost Per Query */
search_context_cost_per_query?: {
[key: string]: unknown;
@@ -31560,6 +31568,26 @@ export interface components {
/** Review Notes */
review_notes?: string | null;
};
+ /**
+ * ReminderMarkerPair
+ * @description One open/close delimiter pair a harness wraps injected context in.
+ *
+ * Normalizing here rather than at the scan is what makes matching case-insensitive: markers reach
+ * the scan already lowered, so it lowercases only the haystack and never the needles. Stripping
+ * keeps YAML indentation whitespace from becoming part of the delimiter.
+ */
+ ReminderMarkerPair: {
+ /**
+ * Close
+ * @description Closing delimiter, e.g. ''
+ */
+ close: string;
+ /**
+ * Open
+ * @description Opening delimiter, e.g. ''
+ */
+ open: string;
+ };
/**
* RequestComplexityRouterConfig
* @description The part of a complexity-router config a request can carry.
@@ -31672,12 +31700,9 @@ export interface components {
reasoning_keywords?: string[] | null;
/**
* Reminder Markers
- * @description Override the (open, close) marker pair used to recognize and strip harness-injected reminder blocks before classification. Defaults to Claude Code's convention, ('', ' '), when unset. Matching is case-insensitive.
+ * @description Override the delimiter pairs used to recognize and strip harness-injected reminder blocks before classification. A harness that wraps injected context differently per agent type (main, subagent, cron) lists every pair it emits. Replaces, rather than adds to, the built-in default of ('', ' '), so a harness that also emits that pair lists it too. Matching is case-insensitive.
*/
- reminder_markers?: [
- string,
- string
- ] | null;
+ reminder_markers?: components["schemas"]["ReminderMarkerPair"][] | null;
/**
* Return Raw Model Name
* @description Return the resolved raw model name in the response model field instead of the client-requested complexity-router alias
@@ -35295,6 +35320,8 @@ export interface components {
auto_router_max_input_chars?: number | null;
/** Aws Access Key Id */
aws_access_key_id?: string | null;
+ /** Aws Batch Role Arn */
+ aws_batch_role_arn?: string | null;
/** Aws Bedrock Project Id */
aws_bedrock_project_id?: string | null;
/** Aws Bedrock Runtime Endpoint */
@@ -35474,6 +35501,10 @@ export interface components {
output_cost_per_pixel?: number | null;
/** Output Cost Per Reasoning Token */
output_cost_per_reasoning_token?: number | null;
+ /** Output Cost Per Reasoning Token Flex */
+ output_cost_per_reasoning_token_flex?: number | null;
+ /** Output Cost Per Reasoning Token Priority */
+ output_cost_per_reasoning_token_priority?: number | null;
/** Output Cost Per Second */
output_cost_per_second?: number | null;
/** Output Cost Per Second 1080P */
@@ -35524,6 +35555,8 @@ export interface components {
s3_bucket_name?: string | null;
/** S3 Encryption Key Id */
s3_encryption_key_id?: string | null;
+ /** S3 Region Name */
+ s3_region_name?: string | null;
/** Search Context Cost Per Query */
search_context_cost_per_query?: {
[key: string]: unknown;
@@ -53858,6 +53891,8 @@ export interface operations {
page_size?: number;
/** @description Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention. */
timezone?: number | null;
+ /** @description When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended. */
+ include_current_utc_day?: boolean;
};
header?: never;
path?: never;
diff --git a/ui/litellm-dashboard/src/utils/entityLinks.ts b/ui/litellm-dashboard/src/utils/entityLinks.ts
index 2659a307866..85052448866 100644
--- a/ui/litellm-dashboard/src/utils/entityLinks.ts
+++ b/ui/litellm-dashboard/src/utils/entityLinks.ts
@@ -3,3 +3,7 @@ import { migratedHref } from "@/utils/migratedPages";
export function teamDetailHref(teamId: string): string {
return `${migratedHref("teams")}?team=${encodeURIComponent(teamId)}`;
}
+
+export function keyDetailHref(keyToken: string): string {
+ return `${migratedHref("api-keys")}?key=${encodeURIComponent(keyToken)}`;
+}