diff --git a/docs/my-website/docs/troubleshoot/max_callbacks.md b/docs/my-website/docs/troubleshoot/max_callbacks.md new file mode 100644 index 00000000000..4b0f3e24b73 --- /dev/null +++ b/docs/my-website/docs/troubleshoot/max_callbacks.md @@ -0,0 +1,68 @@ +# MAX_CALLBACKS Limit + +## Error Message + +``` +Cannot add callback - would exceed MAX_CALLBACKS limit of 30. Current callbacks: 30 +``` + +## What This Means + +LiteLLM limits the number of callbacks that can be registered to prevent performance degradation. Each callback runs on every LLM request, so having too many callbacks can cause exponential CPU usage and slow down your proxy. + +The default limit is **30 callbacks**. + +## When You Might Hit This Limit + +- **Large enterprise deployments** with many teams, each having their own guardrails +- **Multiple logging integrations** combined with custom callbacks +- **Per-team callback configurations** that add up across your organization + +## How to Override + +Set the `LITELLM_MAX_CALLBACKS` environment variable to increase the limit: + +```bash +# Docker +docker run -e LITELLM_MAX_CALLBACKS=100 ... + +# Docker Compose +environment: + - LITELLM_MAX_CALLBACKS=100 + +# Kubernetes +env: + - name: LITELLM_MAX_CALLBACKS + value: "100" + +# Direct +export LITELLM_MAX_CALLBACKS=100 +litellm --config config.yaml +``` + +## Recommendations + +1. **Start conservative** - Only increase as much as you need. If you have 60 teams with guardrails, try `LITELLM_MAX_CALLBACKS=75` to leave headroom. + +2. **Monitor performance** - More callbacks means more processing per request. Watch your CPU usage and response latency after increasing the limit. + +3. **Consolidate where possible** - If multiple teams use identical guardrails, consider using shared callback configurations rather than per-team duplicates. + +## Example: Large Enterprise Setup + +For an organization with 60+ teams, each with a guardrail callback: + +```yaml +# config.yaml +litellm_settings: + callbacks: ["prometheus", "langfuse"] # 2 global callbacks + +# Each team adds 1 guardrail callback = 60+ callbacks +# Total: 62+ callbacks needed +``` + +Set the environment variable: + +```bash +export LITELLM_MAX_CALLBACKS=100 +``` diff --git a/docs/my-website/release_notes/v1.81.3-stable/index.md b/docs/my-website/release_notes/v1.81.3-stable/index.md index 22b6f43deef..c4b9013590c 100644 --- a/docs/my-website/release_notes/v1.81.3-stable/index.md +++ b/docs/my-website/release_notes/v1.81.3-stable/index.md @@ -27,7 +27,7 @@ import TabItem from '@theme/TabItem'; docker run \ -e STORE_MODEL_IN_DB=True \ -p 4000:4000 \ -docker.litellm.ai/berriai/litellm:v1.81.3.rc.2 +docker.litellm.ai/berriai/litellm:v1.81.3-stable ``` diff --git a/docs/my-website/sidebars.js b/docs/my-website/sidebars.js index 37392ff5c18..0d95bf7e545 100644 --- a/docs/my-website/sidebars.js +++ b/docs/my-website/sidebars.js @@ -1081,6 +1081,7 @@ const sidebars = { "troubleshoot/cpu_issues", "troubleshoot/memory_issues", "troubleshoot/spend_queue_warnings", + "troubleshoot/max_callbacks", ], }, ], diff --git a/litellm/constants.py b/litellm/constants.py index 3c618723d64..f5f2c7a49e4 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -2,6 +2,8 @@ import os import sys from typing import List, Literal +from litellm.litellm_core_utils.env_utils import get_env_int + DEFAULT_HEALTH_CHECK_PROMPT = str( os.getenv("DEFAULT_HEALTH_CHECK_PROMPT", "test from litellm") ) @@ -99,6 +101,11 @@ DEFAULT_REASONING_EFFORT_MINIMAL_THINKING_BUDGET_GEMINI_2_5_FLASH_LITE = int( ) ) +# Maximum number of callbacks that can be registered +# This prevents callbacks from exponentially growing and consuming CPU resources +# Override with LITELLM_MAX_CALLBACKS env var for large deployments (e.g., many teams with guardrails) +MAX_CALLBACKS = get_env_int("LITELLM_MAX_CALLBACKS", 30) + # Generic fallback for unknown models DEFAULT_REASONING_EFFORT_MINIMAL_THINKING_BUDGET = int( os.getenv("DEFAULT_REASONING_EFFORT_MINIMAL_THINKING_BUDGET", 128) diff --git a/litellm/litellm_core_utils/env_utils.py b/litellm/litellm_core_utils/env_utils.py new file mode 100644 index 00000000000..34c65275331 --- /dev/null +++ b/litellm/litellm_core_utils/env_utils.py @@ -0,0 +1,21 @@ +""" +Utility helpers for reading and parsing environment variables. +""" + +import os + + +def get_env_int(env_var: str, default: int) -> int: + """Parse an environment variable as an integer, falling back to default on invalid values. + + Handles empty strings, whitespace, and non-numeric values gracefully + so that misconfiguration doesn't crash the process at import time. + """ + raw = os.getenv(env_var) + if raw is None: + return default + raw = raw.strip() + try: + return int(raw) + except (ValueError, TypeError): + return default diff --git a/litellm/litellm_core_utils/logging_callback_manager.py b/litellm/litellm_core_utils/logging_callback_manager.py index 435ae078a65..34d25817378 100644 --- a/litellm/litellm_core_utils/logging_callback_manager.py +++ b/litellm/litellm_core_utils/logging_callback_manager.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Set, Type, Uni import litellm from litellm._logging import verbose_logger +from litellm.constants import MAX_CALLBACKS from litellm.integrations.additional_logging_utils import AdditionalLoggingUtils from litellm.integrations.custom_logger import CustomLogger from litellm.integrations.generic_api.generic_api_callback import GenericAPILogger @@ -24,9 +25,6 @@ class LoggingCallbackManager: - Keep a reasonable MAX_CALLBACKS limit (this ensures callbacks don't exponentially grow and consume CPU Resources) """ - # healthy maximum number of callbacks - unlikely someone needs more than 20 - MAX_CALLBACKS = 30 - def add_litellm_input_callback(self, callback: Union[CustomLogger, str]): """ Add a input callback to litellm.input_callback @@ -155,9 +153,9 @@ class LoggingCallbackManager: Check if adding another callback would exceed MAX_CALLBACKS Returns True if safe to add, False if would exceed limit """ - if len(parent_list) >= self.MAX_CALLBACKS: + if len(parent_list) >= MAX_CALLBACKS: verbose_logger.warning( - f"Cannot add callback - would exceed MAX_CALLBACKS limit of {self.MAX_CALLBACKS}. Current callbacks: {len(parent_list)}" + f"Cannot add callback - would exceed MAX_CALLBACKS limit of {MAX_CALLBACKS}. Current callbacks: {len(parent_list)}" ) return False return True diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/mcpServers/useMCPAccessGroups.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/mcpServers/useMCPAccessGroups.test.ts new file mode 100644 index 00000000000..9c555ff1234 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/mcpServers/useMCPAccessGroups.test.ts @@ -0,0 +1,124 @@ +/* @vitest-environment jsdom */ +import React from "react"; +import { renderHook, waitFor } from "@testing-library/react"; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { useMCPAccessGroups } from "./useMCPAccessGroups"; +import * as networking from "@/components/networking"; + +vi.mock("@/components/networking", () => ({ + fetchMCPAccessGroups: vi.fn(), +})); + +vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ + default: vi.fn(() => ({ + accessToken: "test-token-456", + })), +})); + +const createQueryClient = () => + new QueryClient({ + defaultOptions: { + queries: { + retry: false, + gcTime: 0, + }, + }, + }); + +const wrapper = ({ children }: { children: React.ReactNode }) => { + const queryClient = createQueryClient(); + return React.createElement(QueryClientProvider, { client: queryClient }, children); +}; + +const mockAccessToken = "test-token-456"; +const mockAccessGroups = ["group-1", "group-2", "group-3"]; + +describe("useMCPAccessGroups", () => { + beforeEach(async () => { + vi.clearAllMocks(); + const useAuthorizedModule = await import("@/app/(dashboard)/hooks/useAuthorized"); + vi.mocked(useAuthorizedModule.default).mockReturnValue({ + accessToken: mockAccessToken, + } as any); + }); + + it("should return hook result without errors", () => { + vi.mocked(networking.fetchMCPAccessGroups).mockResolvedValue([]); + + const { result } = renderHook(() => useMCPAccessGroups(), { wrapper }); + + expect(result.current).toBeDefined(); + expect(result.current).toHaveProperty("data"); + expect(result.current).toHaveProperty("isSuccess"); + expect(result.current).toHaveProperty("isError"); + expect(result.current).toHaveProperty("status"); + }); + + it("should return MCP access groups when access token is present", async () => { + vi.mocked(networking.fetchMCPAccessGroups).mockResolvedValue(mockAccessGroups); + + const { result } = renderHook(() => useMCPAccessGroups(), { wrapper }); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + expect(networking.fetchMCPAccessGroups).toHaveBeenCalledWith(mockAccessToken); + expect(result.current.data).toEqual(mockAccessGroups); + }); + + it("should not fetch when access token is null", async () => { + const useAuthorizedModule = await import("@/app/(dashboard)/hooks/useAuthorized"); + vi.mocked(useAuthorizedModule.default).mockReturnValue({ + accessToken: null, + } as any); + + const { result } = renderHook(() => useMCPAccessGroups(), { wrapper }); + + expect(result.current.isFetching).toBe(false); + expect(result.current.isLoading).toBe(false); + expect(result.current.data).toBeUndefined(); + expect(networking.fetchMCPAccessGroups).not.toHaveBeenCalled(); + }); + + it("should not fetch when access token is empty string", async () => { + const useAuthorizedModule = await import("@/app/(dashboard)/hooks/useAuthorized"); + vi.mocked(useAuthorizedModule.default).mockReturnValue({ + accessToken: "", + } as any); + + const { result } = renderHook(() => useMCPAccessGroups(), { wrapper }); + + expect(result.current.isFetching).toBe(false); + expect(result.current.isLoading).toBe(false); + expect(result.current.data).toBeUndefined(); + expect(networking.fetchMCPAccessGroups).not.toHaveBeenCalled(); + }); + + it("should expose error state when fetch fails", async () => { + const mockError = new Error("Failed to fetch MCP access groups"); + vi.mocked(networking.fetchMCPAccessGroups).mockRejectedValue(mockError); + + const { result } = renderHook(() => useMCPAccessGroups(), { wrapper }); + + await waitFor(() => { + expect(result.current.isError).toBe(true); + }); + + expect(result.current.error).toEqual(mockError); + expect(result.current.data).toBeUndefined(); + }); + + it("should return empty array when API returns no groups", async () => { + vi.mocked(networking.fetchMCPAccessGroups).mockResolvedValue([]); + + const { result } = renderHook(() => useMCPAccessGroups(), { wrapper }); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + expect(result.current.data).toEqual([]); + }); +}); \ No newline at end of file diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/mcpServers/useMCPServers.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/mcpServers/useMCPServers.test.ts new file mode 100644 index 00000000000..3681ffc7475 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/mcpServers/useMCPServers.test.ts @@ -0,0 +1,134 @@ +/* @vitest-environment jsdom */ +import React from "react"; +import { renderHook, waitFor } from "@testing-library/react"; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { useMCPServers } from "./useMCPServers"; +import * as networking from "@/components/networking"; + +vi.mock("@/components/networking", () => ({ + fetchMCPServers: vi.fn(), +})); + +vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ + default: vi.fn(() => ({ + accessToken: "test-token-123", + })), +})); + +const createQueryClient = () => + new QueryClient({ + defaultOptions: { + queries: { + retry: false, + gcTime: 0, + }, + }, + }); + +const wrapper = ({ children }: { children: React.ReactNode }) => { + const queryClient = createQueryClient(); + return React.createElement(QueryClientProvider, { client: queryClient }, children); +}; + +const mockAccessToken = "test-token-123"; +const mockServers = [ + { + server_id: "server-1", + server_name: "Server One", + url: "http://localhost:4000", + created_at: "2025-01-01T00:00:00Z", + created_by: "user-1", + updated_at: "2025-01-01T00:00:00Z", + updated_by: "user-1", + }, +]; + +describe("useMCPServers", () => { + beforeEach(async () => { + vi.clearAllMocks(); + const useAuthorizedModule = await import("@/app/(dashboard)/hooks/useAuthorized"); + vi.mocked(useAuthorizedModule.default).mockReturnValue({ + accessToken: mockAccessToken, + } as any); + }); + + it("should return hook result without errors", () => { + vi.mocked(networking.fetchMCPServers).mockResolvedValue([]); + + const { result } = renderHook(() => useMCPServers(), { wrapper }); + + expect(result.current).toBeDefined(); + expect(result.current).toHaveProperty("data"); + expect(result.current).toHaveProperty("isSuccess"); + expect(result.current).toHaveProperty("isError"); + expect(result.current).toHaveProperty("status"); + }); + + it("should return MCP servers when access token is present", async () => { + vi.mocked(networking.fetchMCPServers).mockResolvedValue(mockServers); + + const { result } = renderHook(() => useMCPServers(), { wrapper }); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + expect(networking.fetchMCPServers).toHaveBeenCalledWith(mockAccessToken); + expect(result.current.data).toEqual(mockServers); + }); + + it("should not fetch when access token is null", async () => { + const useAuthorizedModule = await import("@/app/(dashboard)/hooks/useAuthorized"); + vi.mocked(useAuthorizedModule.default).mockReturnValue({ + accessToken: null, + } as any); + + const { result } = renderHook(() => useMCPServers(), { wrapper }); + + expect(result.current.isFetching).toBe(false); + expect(result.current.isLoading).toBe(false); + expect(result.current.data).toBeUndefined(); + expect(networking.fetchMCPServers).not.toHaveBeenCalled(); + }); + + it("should not fetch when access token is empty string", async () => { + const useAuthorizedModule = await import("@/app/(dashboard)/hooks/useAuthorized"); + vi.mocked(useAuthorizedModule.default).mockReturnValue({ + accessToken: "", + } as any); + + const { result } = renderHook(() => useMCPServers(), { wrapper }); + + expect(result.current.isFetching).toBe(false); + expect(result.current.isLoading).toBe(false); + expect(result.current.data).toBeUndefined(); + expect(networking.fetchMCPServers).not.toHaveBeenCalled(); + }); + + it("should expose error state when fetch fails", async () => { + const mockError = new Error("Failed to fetch MCP servers"); + vi.mocked(networking.fetchMCPServers).mockRejectedValue(mockError); + + const { result } = renderHook(() => useMCPServers(), { wrapper }); + + await waitFor(() => { + expect(result.current.isError).toBe(true); + }); + + expect(result.current.error).toEqual(mockError); + expect(result.current.data).toBeUndefined(); + }); + + it("should return empty array when API returns empty list", async () => { + vi.mocked(networking.fetchMCPServers).mockResolvedValue([]); + + const { result } = renderHook(() => useMCPServers(), { wrapper }); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + expect(result.current.data).toEqual([]); + }); +}); \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/dashboard_default_team.tsx b/ui/litellm-dashboard/src/components/dashboard_default_team.tsx deleted file mode 100644 index 6506e1a60a5..00000000000 --- a/ui/litellm-dashboard/src/components/dashboard_default_team.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import React, { useState, useEffect } from "react"; -import { Select, SelectItem, Text, Title } from "@tremor/react"; -import { ProxySettings, UserInfo } from "./user_dashboard"; -import { getProxyUISettings } from "./networking"; - -interface DashboardTeamProps { - teams: object[] | null; - setSelectedTeam: React.Dispatch>; - userRole: string | null; - proxySettings: ProxySettings | null; - setProxySettings: React.Dispatch>; - userInfo: UserInfo | null; - accessToken: string | null; - setKeys: React.Dispatch>; -} - -type TeamInterface = { - models: any[]; - team_id: null; - team_alias: string; - max_budget: number | null; -}; - -const DashboardTeam: React.FC = ({ - teams, - setSelectedTeam, - userRole, - proxySettings, - setProxySettings, - userInfo, - accessToken, - setKeys, -}) => { - console.log(`userInfo: ${JSON.stringify(userInfo)}`); - const defaultTeam: TeamInterface = { - models: userInfo?.models || [], - team_id: null, - team_alias: "Default Team", - max_budget: userInfo?.max_budget || null, - }; - - const getProxySettings = async () => { - if (proxySettings === null && accessToken) { - const proxy_settings: ProxySettings = await getProxyUISettings(accessToken); - setProxySettings(proxy_settings); - } - }; - - useEffect(() => { - getProxySettings(); - }, [proxySettings]); - - const [value, setValue] = useState(defaultTeam); - - let updatedTeams; - console.log(`userRole: ${userRole}`); - console.log(`proxySettings: ${JSON.stringify(proxySettings)}`); - if (userRole === "App User") { - // Non-Admin SSO users should only see their own team - they should not see "Default Team" - updatedTeams = teams; - } else if (proxySettings && proxySettings.DEFAULT_TEAM_DISABLED === true) { - updatedTeams = teams ? [...teams] : [defaultTeam]; - } else { - updatedTeams = teams ? [...teams, defaultTeam] : [defaultTeam]; - } - - return ( -
- Select Team - - - If you belong to multiple teams, this setting controls which team is used by default when creating new Virtual - Keys. - - - Default Team: If no team_id is set for a key, it will be grouped under here. - - - {updatedTeams && updatedTeams.length > 0 ? ( - - ) : ( - - No team created. Defaulting to personal account. - - )} -
- ); -}; - -export default DashboardTeam; diff --git a/ui/litellm-dashboard/src/components/enter_proxy_url.tsx b/ui/litellm-dashboard/src/components/enter_proxy_url.tsx deleted file mode 100644 index 2fad2fef061..00000000000 --- a/ui/litellm-dashboard/src/components/enter_proxy_url.tsx +++ /dev/null @@ -1,69 +0,0 @@ -"use client"; - -import React, { useState, ChangeEvent } from "react"; -import { Button, Col, Grid, TextInput } from "@tremor/react"; -import { Card, Text } from "@tremor/react"; - -const EnterProxyUrl: React.FC = () => { - const [proxyUrl, setProxyUrl] = useState(""); - const [isUrlSaved, setIsUrlSaved] = useState(false); - - const handleUrlChange = (event: ChangeEvent) => { - setProxyUrl(event.target.value); - // Reset the saved status when the URL changes - setIsUrlSaved(false); - }; - - const handleSaveClick = () => { - // You can perform any additional validation or actions here - // For now, let's just display the message - setIsUrlSaved(true); - }; - - // Construct the URL for clicking - const clickableUrl = `${window.location.href}?proxyBaseUrl=${proxyUrl}`; - - return ( -
- - Admin Configuration - - - - {/* Display message if the URL is saved */} - {isUrlSaved && ( -
- - -

Proxy Admin UI (Save this URL): {clickableUrl}

- - -

- Get Started with Proxy Admin UI 👉 - - {clickableUrl} - -

- -
-
- )} -
-
- ); -}; - -export default EnterProxyUrl; diff --git a/ui/litellm-dashboard/src/components/key_team_helpers/team_search_fn.tsx b/ui/litellm-dashboard/src/components/key_team_helpers/team_search_fn.tsx deleted file mode 100644 index 7ff4417ea3f..00000000000 --- a/ui/litellm-dashboard/src/components/key_team_helpers/team_search_fn.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Team } from "./key_list"; - -export const createTeamSearchFunction = (teams: Team[] | null) => { - return async (searchText: string): Promise> => { - // Return empty array if teams is null or searchText is empty - if (!teams || !searchText.trim()) { - return []; - } - - // Filter teams where team_alias contains the search text (case insensitive) - const filteredTeams = teams.filter((team) => team.team_alias.toLowerCase().includes(searchText.toLowerCase())); - - // Map filtered teams to the required format - return filteredTeams.map((team) => ({ - label: `${team.team_alias} (${team.team_id.substring(0, 8)}...)`, - value: team.team_id, - })); - }; -}; diff --git a/ui/litellm-dashboard/src/components/mcp_connection_test.tsx b/ui/litellm-dashboard/src/components/mcp_connection_test.tsx deleted file mode 100644 index 35f8bd7aa09..00000000000 --- a/ui/litellm-dashboard/src/components/mcp_connection_test.tsx +++ /dev/null @@ -1,279 +0,0 @@ -import React from "react"; -import { Typography, Space, Button, Divider } from "antd"; -import { WarningOutlined, InfoCircleOutlined, CopyOutlined } from "@ant-design/icons"; -import { testMCPConnectionRequest } from "./networking"; -import NotificationsManager from "./molecules/notifications_manager"; - -const { Text } = Typography; - -interface MCPConnectionTestProps { - formValues: Record; - accessToken: string; - serverName?: string; - onClose?: () => void; - onTestComplete?: () => void; -} - -const MCPConnectionTest: React.FC = ({ - formValues, - accessToken, - serverName = "this MCP server", - onClose, - onTestComplete, -}) => { - const [connectionError, setConnectionError] = React.useState(null); - const [rawRequest, setRawRequest] = React.useState(null); - const [rawResponse, setRawResponse] = React.useState(null); - const [isLoading, setIsLoading] = React.useState(true); - const [connectionSuccess, setConnectionSuccess] = React.useState(false); - const [showDetails, setShowDetails] = React.useState(false); - - const testMCPConnection = async () => { - setIsLoading(true); - setShowDetails(false); - setConnectionError(null); - setRawRequest(null); - setRawResponse(null); - setConnectionSuccess(false); - - // Add a small delay to ensure form values are fully populated - await new Promise((resolve) => setTimeout(resolve, 100)); - - try { - console.log("Testing MCP connection with form values:", formValues); - - // Prepare the MCP server config from form values - const mcpServerConfig = { - server_id: formValues.server_id || "", - alias: formValues.alias || "", - url: formValues.url, - transport: formValues.transport, - auth_type: formValues.auth_type, - mcp_info: formValues.mcp_info, - }; - - setRawRequest(mcpServerConfig); - - // Test connection - const connectionResponse = await testMCPConnectionRequest(accessToken, mcpServerConfig); - console.log("Connection test response:", connectionResponse); - - if (connectionResponse.status === "ok") { - setConnectionError(null); - setConnectionSuccess(true); - } else { - const errorMessage = connectionResponse.message || "Unknown connection error"; - setConnectionError(errorMessage); - setRawResponse(connectionResponse); - } - } catch (error) { - console.error("MCP connection test error:", error); - setConnectionError(error instanceof Error ? error.message : String(error)); - } finally { - setIsLoading(false); - if (onTestComplete) onTestComplete(); - } - }; - - React.useEffect(() => { - // Run the test once when component mounts - // Add a small timeout to ensure form values are ready - const timer = setTimeout(() => { - testMCPConnection(); - }, 200); - - return () => clearTimeout(timer); - }, []); // Empty dependency array means this runs once on mount - - const getCleanErrorMessage = (errorMsg: string) => { - if (!errorMsg) return "Unknown error"; - - const mainError = errorMsg.split("stack trace:")[0].trim(); - - const cleanedError = mainError.replace(/^(.*?)Error: /, ""); - - return cleanedError; - }; - - const connectionErrorMessage = - typeof connectionError === "string" - ? getCleanErrorMessage(connectionError) - : connectionError?.message - ? getCleanErrorMessage(connectionError.message) - : "Unknown error"; - - const formatMCPRequest = (mcpConfig: Record) => { - return JSON.stringify(mcpConfig, null, 2); - }; - - const isOverallSuccess = connectionSuccess && !connectionError; - - return ( -
- {isLoading ? ( -
-
- {/* Simple CSS spinner */} -
-
- Testing connection to {serverName}... - -
- ) : isOverallSuccess ? ( -
-
-
- -
- - Connection to {serverName} successful! - -
-
- ) : ( - <> -
-
- - - Connection to {serverName} failed - -
- -
- - Error:{" "} - - - {connectionErrorMessage} - - - {connectionError && ( -
- -
- )} -
- - {showDetails && ( -
- - Troubleshooting Details - -
-                  {typeof connectionError === "string" ? connectionError : JSON.stringify(connectionError, null, 2)}
-                
-
- )} - -
- - MCP Server Configuration - -
-                {formatMCPRequest(rawRequest || {})}
-              
- -
-
- - )} - -
- - - - - -
-
- ); -}; - -export default MCPConnectionTest; diff --git a/ui/litellm-dashboard/src/components/model_add/CredentialDeleteModal.tsx b/ui/litellm-dashboard/src/components/model_add/CredentialDeleteModal.tsx deleted file mode 100644 index 0ef889b9da4..00000000000 --- a/ui/litellm-dashboard/src/components/model_add/CredentialDeleteModal.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React, { useState } from "react"; -import { Modal } from "antd"; -import { Button as TremorButton } from "@tremor/react"; -import { ExclamationIcon } from "@heroicons/react/outline"; - -interface CredentialDeleteModalProps { - isVisible: boolean; - onCancel: () => void; - onConfirm: () => void; - credentialName: string; -} - -const CredentialDeleteModal: React.FC = ({ - isVisible, - onCancel, - onConfirm, - credentialName, -}) => { - const [deleteConfirmInput, setDeleteConfirmInput] = useState(""); - const isValid = deleteConfirmInput === credentialName; - - const handleCancel = () => { - setDeleteConfirmInput(""); - onCancel(); - }; - - const handleConfirm = () => { - if (isValid) { - setDeleteConfirmInput(""); - onConfirm(); - } - }; - - return ( - - - Delete Credential -
- } - open={isVisible} - footer={null} - onCancel={handleCancel} - closable={true} - destroyOnHidden={true} - maskClosable={false} - > -
-
-
- -
-
-

- This action cannot be undone and may break existing integrations. -

-
-
- -
- - setDeleteConfirmInput(e.target.value)} - placeholder="Enter credential name exactly" - className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-base" - autoFocus - /> -
- -
- - Cancel - - - Delete Credential - -
-
- - ); -}; - -export default CredentialDeleteModal; diff --git a/ui/litellm-dashboard/src/components/molecules/filter.test.tsx b/ui/litellm-dashboard/src/components/molecules/filter.test.tsx index bd06d110e95..1a90c4a069f 100644 --- a/ui/litellm-dashboard/src/components/molecules/filter.test.tsx +++ b/ui/litellm-dashboard/src/components/molecules/filter.test.tsx @@ -1,4 +1,4 @@ -import { screen, waitFor } from "@testing-library/react"; +import { screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "../../../tests/test-utils"; @@ -58,8 +58,34 @@ describe("FilterComponent", () => { expect(screen.getByRole("button", { name: "Custom Filters" })).toBeInTheDocument(); }); + it("should toggle filters visibility when filter button is clicked", async () => { + const user = userEvent.setup({ delay: null }); + renderWithProviders( + , + ); + + const filterButton = screen.getByRole("button", { name: "Filters" }); + expect(screen.queryByPlaceholderText("Enter User ID...")).not.toBeInTheDocument(); + + await user.click(filterButton); + + await waitFor(() => { + expect(screen.getByPlaceholderText("Enter User ID...")).toBeInTheDocument(); + }); + + await user.click(filterButton); + + await waitFor(() => { + expect(screen.queryByPlaceholderText("Enter User ID...")).not.toBeInTheDocument(); + }); + }); + it("should call onResetFilters when reset button is clicked", async () => { - const user = userEvent.setup(); + const user = userEvent.setup({ delay: null }); renderWithProviders( { }); it("should render filters in correct order", async () => { - const user = userEvent.setup(); + const user = userEvent.setup({ delay: null }); const options: FilterOption[] = [ { name: "model", label: "Model" }, { name: "teamId", label: "Team ID" }, @@ -105,7 +131,7 @@ describe("FilterComponent", () => { }); it("should handle input filter changes", async () => { - const user = userEvent.setup(); + const user = userEvent.setup({ delay: null }); renderWithProviders( { expect(mockOnApplyFilters).toHaveBeenCalledWith({ userId: "user123" }); }); }); + + it("should display initial values in filters", async () => { + const user = userEvent.setup({ delay: null }); + renderWithProviders( + , + ); + + const filterButton = screen.getByRole("button", { name: "Filters" }); + await user.click(filterButton); + + await waitFor(() => { + const userIdInput = screen.getByPlaceholderText("Enter User ID...") as HTMLInputElement; + expect(userIdInput.value).toBe("user123"); + }); + }); + + it("should handle select dropdown filter changes", async () => { + const user = userEvent.setup({ delay: null }); + renderWithProviders( + , + ); + + const filterButton = screen.getByRole("button", { name: "Filters" }); + await user.click(filterButton); + + const teamIdLabel = screen.getByText("Team ID"); + const teamIdSection = teamIdLabel.closest("div"); + const teamIdSelect = within(teamIdSection!).getByRole("combobox"); + + await user.click(teamIdSelect); + + await waitFor(() => { + expect(screen.getByText("Team 1")).toBeInTheDocument(); + }); + + await user.click(screen.getByText("Team 1")); + + await waitFor(() => { + expect(mockOnApplyFilters).toHaveBeenCalledWith({ teamId: "team1" }); + }); + }); + + it("should handle searchable filter with search function", async () => { + const user = userEvent.setup({ delay: null }); + const mockSearchFn = vi.fn().mockResolvedValue([ + { label: "Result 1", value: "result1" }, + { label: "Result 2", value: "result2" }, + ]); + + const options: FilterOption[] = [ + { + name: "model", + label: "Model", + isSearchable: true, + searchFn: mockSearchFn, + }, + ]; + + renderWithProviders( + , + ); + + const filterButton = screen.getByRole("button", { name: "Filters" }); + await user.click(filterButton); + + await waitFor(() => { + expect(mockSearchFn).toHaveBeenCalledWith(""); + }); + + const modelLabel = screen.getByText("Model"); + const modelSection = modelLabel.closest("div"); + const modelSelect = within(modelSection!).getByRole("combobox"); + await user.click(modelSelect); + + await waitFor(() => { + expect(screen.getByText("Result 1")).toBeInTheDocument(); + expect(screen.getByText("Result 2")).toBeInTheDocument(); + }); + }); + + it("should debounce search input for searchable filters", async () => { + const user = userEvent.setup({ delay: null }); + const mockSearchFn = vi.fn().mockResolvedValue([ + { label: "Result", value: "result" }, + ]); + + const options: FilterOption[] = [ + { + name: "model", + label: "Model", + isSearchable: true, + searchFn: mockSearchFn, + }, + ]; + + renderWithProviders( + , + ); + + const filterButton = screen.getByRole("button", { name: "Filters" }); + await user.click(filterButton); + + await waitFor(() => { + expect(mockSearchFn).toHaveBeenCalledWith(""); + }); + + vi.clearAllMocks(); + + const modelLabel = screen.getByText("Model"); + const modelSection = modelLabel.closest("div"); + const modelSelect = within(modelSection!).getByRole("combobox"); + await user.click(modelSelect); + await user.type(modelSelect, "test"); + + expect(mockSearchFn).not.toHaveBeenCalled(); + + await waitFor( + () => { + expect(mockSearchFn).toHaveBeenCalledWith("test"); + }, + { timeout: 500 }, + ); + }); + + it("should show loading state when searching", async () => { + const user = userEvent.setup({ delay: null }); + let resolveSearch: (value: Array<{ label: string; value: string }>) => void; + const mockSearchFn = vi.fn().mockImplementation( + () => + new Promise>((resolve) => { + resolveSearch = resolve; + }), + ); + + const options: FilterOption[] = [ + { + name: "model", + label: "Model", + isSearchable: true, + searchFn: mockSearchFn, + }, + ]; + + renderWithProviders( + , + ); + + const filterButton = screen.getByRole("button", { name: "Filters" }); + await user.click(filterButton); + + await waitFor(() => { + expect(mockSearchFn).toHaveBeenCalledWith(""); + }); + + const modelLabel = screen.getByText("Model"); + const modelSection = modelLabel.closest("div"); + const modelSelect = within(modelSection!).getByRole("combobox"); + await user.click(modelSelect); + await user.type(modelSelect, "test"); + + await waitFor( + () => { + expect(screen.getByText("Loading...")).toBeInTheDocument(); + }, + { timeout: 500 }, + ); + + resolveSearch!([{ label: "Result", value: "result" }]); + + await waitFor(() => { + expect(screen.queryByText("Loading...")).not.toBeInTheDocument(); + }); + }); + + it("should handle search errors gracefully", async () => { + const user = userEvent.setup({ delay: null }); + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + const mockSearchFn = vi.fn().mockRejectedValue(new Error("Search failed")); + + const options: FilterOption[] = [ + { + name: "model", + label: "Model", + isSearchable: true, + searchFn: mockSearchFn, + }, + ]; + + renderWithProviders( + , + ); + + const filterButton = screen.getByRole("button", { name: "Filters" }); + await user.click(filterButton); + + await waitFor(() => { + expect(mockSearchFn).toHaveBeenCalledWith(""); + }); + + const modelLabel = screen.getByText("Model"); + const modelSection = modelLabel.closest("div"); + const modelSelect = within(modelSection!).getByRole("combobox"); + await user.click(modelSelect); + await user.type(modelSelect, "test"); + + await waitFor( + () => { + expect(consoleErrorSpy).toHaveBeenCalledWith("Error searching:", expect.any(Error)); + expect(screen.getByText("No results found")).toBeInTheDocument(); + }, + { timeout: 500 }, + ); + + consoleErrorSpy.mockRestore(); + }); + + it("should load initial options when dropdown opens for searchable filter", async () => { + const user = userEvent.setup({ delay: null }); + const mockSearchFn = vi.fn().mockResolvedValue([ + { label: "Initial Result", value: "initial" }, + ]); + + const options: FilterOption[] = [ + { + name: "model", + label: "Model", + isSearchable: true, + searchFn: mockSearchFn, + }, + ]; + + renderWithProviders( + , + ); + + const filterButton = screen.getByRole("button", { name: "Filters" }); + await user.click(filterButton); + + await waitFor(() => { + expect(mockSearchFn).toHaveBeenCalledWith(""); + }); + + vi.clearAllMocks(); + + const modelLabel = screen.getByText("Model"); + const modelSection = modelLabel.closest("div"); + const modelSelect = within(modelSection!).getByRole("combobox"); + await user.click(modelSelect); + + await waitFor(() => { + expect(screen.getByText("Initial Result")).toBeInTheDocument(); + }); + }); + + it("should not render filters that are not in orderedFilters list", async () => { + const user = userEvent.setup({ delay: null }); + const options: FilterOption[] = [ + { + name: "unknownFilter", + label: "Unknown Filter", + }, + ]; + + renderWithProviders( + , + ); + + const filterButton = screen.getByRole("button", { name: "Filters" }); + await user.click(filterButton); + + await waitFor(() => { + expect(screen.queryByText("Unknown Filter")).not.toBeInTheDocument(); + }); + }); + + it("should call onApplyFilters with updated values when multiple filters change", async () => { + const user = userEvent.setup({ delay: null }); + renderWithProviders( + , + ); + + const filterButton = screen.getByRole("button", { name: "Filters" }); + await user.click(filterButton); + + const userIdInput = screen.getByPlaceholderText("Enter User ID..."); + await user.type(userIdInput, "user123"); + + await waitFor(() => { + expect(mockOnApplyFilters).toHaveBeenCalledWith({ userId: "user123" }); + }); + + const teamIdLabel = screen.getByText("Team ID"); + const teamIdSection = teamIdLabel.closest("div"); + const teamIdSelect = within(teamIdSection!).getByRole("combobox"); + await user.click(teamIdSelect); + + await waitFor(() => { + expect(screen.getByText("Team 1")).toBeInTheDocument(); + }); + + await user.click(screen.getByText("Team 1")); + + await waitFor(() => { + expect(mockOnApplyFilters).toHaveBeenCalledWith({ + userId: "user123", + teamId: "team1", + }); + }); + }); + + it("should reset all filter values when reset button is clicked", async () => { + const user = userEvent.setup({ delay: null }); + renderWithProviders( + , + ); + + const filterButton = screen.getByRole("button", { name: "Filters" }); + await user.click(filterButton); + + await waitFor(() => { + const userIdInput = screen.getByPlaceholderText("Enter User ID...") as HTMLInputElement; + expect(userIdInput.value).toBe("user123"); + }); + + const resetButton = screen.getByRole("button", { name: "Reset Filters" }); + await user.click(resetButton); + + await waitFor(() => { + const userIdInput = screen.getByPlaceholderText("Enter User ID...") as HTMLInputElement; + expect(userIdInput.value).toBe(""); + }); + }); }); diff --git a/ui/litellm-dashboard/src/components/molecules/filter.tsx b/ui/litellm-dashboard/src/components/molecules/filter.tsx index 6c46392410d..a3a12fdf759 100644 --- a/ui/litellm-dashboard/src/components/molecules/filter.tsx +++ b/ui/litellm-dashboard/src/components/molecules/filter.tsx @@ -164,7 +164,7 @@ const FilterComponent: React.FC = ({ placeholder={`Search ${option.label || option.name}...`} value={tempValues[option.name] || undefined} onChange={(value) => handleFilterChange(option.name, value)} - onDropdownVisibleChange={(open) => handleDropdownVisibleChange(open, option)} + onOpenChange={(open) => handleDropdownVisibleChange(open, option)} onSearch={(value) => { setSearchInputValueMap((prev) => ({ ...prev, diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 0d543952efc..023c88c5e83 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -83,7 +83,7 @@ const defaultServerRootPath = "/"; export let serverRootPath = defaultServerRootPath; export let proxyBaseUrl = defaultProxyBaseUrl; if (isLocal != true) { - console.log = function () {}; + console.log = function () { }; } const getWindowLocation = () => { @@ -136,8 +136,6 @@ const HTTP_REQUEST = { DELETE: "DELETE", }; -export const DEFAULT_ORGANIZATION = "default_organization"; - export interface Model { model_name: string; litellm_params: object; @@ -533,38 +531,6 @@ export const modelCreateCall = async (accessToken: string, formValues: Model) => } }; -export const modelSettingsCall = async (accessToken: string) => { - /** - * Get all configurable params for setting a model - */ - try { - let url = proxyBaseUrl ? `${proxyBaseUrl}/model/settings` : `/model/settings`; - - //NotificationsManager.info("Requesting model data"); - const response = await fetch(url, { - method: "GET", - headers: { - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - const errorData = await response.json(); - const errorMessage = deriveErrorMessage(errorData); - handleError(errorMessage); - throw new Error(errorMessage); - } - - const data = await response.json(); - //NotificationsManager.info("Received model data"); - return data; - // Handle success - you might want to update some state or UI based on the created key - } catch (error: any) { - console.error("Failed to get model settings:", error); - } -}; - export const modelDeleteCall = async (accessToken: string, model_id: string) => { console.log(`model_id in model delete call: ${model_id}`); try { @@ -2301,178 +2267,6 @@ export const deleteAllowedIP = async (accessToken: string, ip: string) => { } }; -export const modelMetricsCall = async ( - accessToken: string, - userID: string, - userRole: string, - modelGroup: string | null, - startTime: string | undefined, - endTime: string | undefined, - apiKey: string | null, - customer: string | null, -) => { - /** - * Get all models on proxy - */ - try { - let url = proxyBaseUrl ? `${proxyBaseUrl}/model/metrics` : `/model/metrics`; - if (modelGroup) { - url = `${url}?_selected_model_group=${modelGroup}&startTime=${startTime}&endTime=${endTime}&api_key=${apiKey}&customer=${customer}`; - } - // NotificationsManager.info("Requesting model data"); - const response = await fetch(url, { - method: "GET", - headers: { - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - const errorData = await response.json(); - const errorMessage = deriveErrorMessage(errorData); - handleError(errorMessage); - throw new Error(errorMessage); - } - - const data = await response.json(); - // NotificationsManager.info("Received model data"); - return data; - // Handle success - you might want to update some state or UI based on the created key - } catch (error) { - console.error("Failed to create key:", error); - throw error; - } -}; -export const streamingModelMetricsCall = async ( - accessToken: string, - modelGroup: string | null, - startTime: string | undefined, - endTime: string | undefined, -) => { - /** - * Get all models on proxy - */ - try { - let url = proxyBaseUrl ? `${proxyBaseUrl}/model/streaming_metrics` : `/model/streaming_metrics`; - if (modelGroup) { - url = `${url}?_selected_model_group=${modelGroup}&startTime=${startTime}&endTime=${endTime}`; - } - // NotificationsManager.info("Requesting model data"); - const response = await fetch(url, { - method: "GET", - headers: { - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - const errorData = await response.json(); - const errorMessage = deriveErrorMessage(errorData); - handleError(errorMessage); - throw new Error(errorMessage); - } - - const data = await response.json(); - // NotificationsManager.info("Received model data"); - return data; - // Handle success - you might want to update some state or UI based on the created key - } catch (error) { - console.error("Failed to create key:", error); - throw error; - } -}; - -export const modelMetricsSlowResponsesCall = async ( - accessToken: string, - userID: string, - userRole: string, - modelGroup: string | null, - startTime: string | undefined, - endTime: string | undefined, - apiKey: string | null, - customer: string | null, -) => { - /** - * Get all models on proxy - */ - try { - let url = proxyBaseUrl ? `${proxyBaseUrl}/model/metrics/slow_responses` : `/model/metrics/slow_responses`; - if (modelGroup) { - url = `${url}?_selected_model_group=${modelGroup}&startTime=${startTime}&endTime=${endTime}&api_key=${apiKey}&customer=${customer}`; - } - - // NotificationsManager.info("Requesting model data"); - const response = await fetch(url, { - method: "GET", - headers: { - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - const errorData = await response.json(); - const errorMessage = deriveErrorMessage(errorData); - handleError(errorMessage); - throw new Error(errorMessage); - } - - const data = await response.json(); - // NotificationsManager.info("Received model data"); - return data; - // Handle success - you might want to update some state or UI based on the created key - } catch (error) { - console.error("Failed to create key:", error); - throw error; - } -}; - -export const modelExceptionsCall = async ( - accessToken: string, - userID: string, - userRole: string, - modelGroup: string | null, - startTime: string | undefined, - endTime: string | undefined, - apiKey: string | null, - customer: string | null, -) => { - /** - * Get all models on proxy - */ - try { - let url = proxyBaseUrl ? `${proxyBaseUrl}/model/metrics/exceptions` : `/model/metrics/exceptions`; - - if (modelGroup) { - url = `${url}?_selected_model_group=${modelGroup}&startTime=${startTime}&endTime=${endTime}&api_key=${apiKey}&customer=${customer}`; - } - const response = await fetch(url, { - method: "GET", - headers: { - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - const errorData = await response.json(); - const errorMessage = deriveErrorMessage(errorData); - handleError(errorMessage); - throw new Error(errorMessage); - } - - const data = await response.json(); - // NotificationsManager.info("Received model data"); - return data; - // Handle success - you might want to update some state or UI based on the created key - } catch (error) { - console.error("Failed to create key:", error); - throw error; - } -}; - export const updateUsefulLinksCall = async ( accessToken: string, useful_links: Record, diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/types.ts b/ui/litellm-dashboard/src/components/playground/chat_ui/types.ts index 4e43e6ec77d..07855827886 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/types.ts +++ b/ui/litellm-dashboard/src/components/playground/chat_ui/types.ts @@ -1,61 +1,3 @@ -export interface Delta { - content?: string; - reasoning_content?: string; - role?: string; - function_call?: any; - tool_calls?: any; - audio?: any; - refusal?: any; - provider_specific_fields?: any; - image?: { - url: string; - detail: string; - }; -} - -export interface CompletionTokensDetails { - accepted_prediction_tokens?: number; - audio_tokens?: number; - reasoning_tokens?: number; - rejected_prediction_tokens?: number; - text_tokens?: number | null; -} - -export interface PromptTokensDetails { - audio_tokens?: number; - cached_tokens?: number; - text_tokens?: number; - image_tokens?: number; -} - -export interface Usage { - completion_tokens: number; - prompt_tokens: number; - total_tokens: number; - completion_tokens_details?: CompletionTokensDetails; - prompt_tokens_details?: PromptTokensDetails; -} - -export interface StreamingChoices { - finish_reason?: string | null; - index: number; - delta: Delta; - logprobs?: any; -} - -export interface StreamingResponse { - id: string; - created: number; - model: string; - object: string; - system_fingerprint?: string; - choices: StreamingChoices[]; - provider_specific_fields?: any; - stream_options?: any; - citations?: any; - usage?: Usage; -} - export interface VectorStoreSearchResult { score: number; content: Array<{ text: string; type: string }>; diff --git a/ui/litellm-dashboard/src/components/templates/model_dashboard.tsx b/ui/litellm-dashboard/src/components/templates/model_dashboard.tsx deleted file mode 100644 index 6f3ce27567c..00000000000 --- a/ui/litellm-dashboard/src/components/templates/model_dashboard.tsx +++ /dev/null @@ -1,1753 +0,0 @@ -import { - Card, - Col, - Grid, - Subtitle, - Table, - TableBody, - TableCell, - TableHead, - TableHeaderCell, - TableRow, - Text, - Title, -} from "@tremor/react"; -import React, { useEffect, useMemo, useRef, useState } from "react"; -import { CredentialItem, credentialListCall, CredentialsResponse } from "../networking"; - -import { handleAddModelSubmit } from "../add_model/handle_add_model_submit"; - -import CredentialsPanel from "@/components/model_add/credentials"; -import { InfoCircleOutlined } from "@ant-design/icons"; -import { FilterIcon, RefreshIcon } from "@heroicons/react/outline"; -import { - AreaChart, - BarChart, - Button, - DateRangePickerValue, - Icon, - Select, - SelectItem, - Tab, - TabGroup, - TabList, - TabPanel, - TabPanels, -} from "@tremor/react"; -import type { UploadProps } from "antd"; -import { Form, InputNumber, Popover, Typography } from "antd"; -import AddModelTab from "../add_model/add_model_tab"; -import { Team } from "../key_team_helpers/key_list"; -import ModelInfoView from "../model_info_view"; -import TimeToFirstToken from "../model_metrics/time_to_first_token"; -import { - adminGlobalActivityExceptions, - adminGlobalActivityExceptionsPerDeployment, - allEndUsersCall, - getCallbacksCall, - healthCheckCall, - modelCostMap, - modelExceptionsCall, - modelInfoCall, - modelMetricsCall, - modelMetricsSlowResponsesCall, - modelSettingsCall, - setCallbacksCall, - streamingModelMetricsCall, -} from "../networking"; -import { getPlaceholder, getProviderModels, provider_map, Providers } from "../provider_info_helpers"; -import UsageDatePicker from "../shared/usage_date_picker"; -import TeamInfoView from "../team/team_info"; -import { getDisplayModelName } from "../view_model/model_name_display"; - -import { all_admin_roles } from "@/utils/roles"; -import { PaginationState } from "@tanstack/react-table"; -import HealthCheckComponent from "../model_dashboard/HealthCheckComponent"; -import { ModelDataTable } from "../model_dashboard/table"; -import ModelGroupAliasSettings from "../model_group_alias_settings"; -import { columns } from "../molecules/models/columns"; -import NotificationsManager from "../molecules/notifications_manager"; -import PassThroughSettings from "../pass_through_settings"; -import PriceDataReload from "../price_data_reload"; - -interface ModelDashboardProps { - accessToken: string | null; - token: string | null; - userRole: string | null; - userID: string | null; - modelData: any; - keys: any[] | null; - setModelData: any; - premiumUser: boolean; - teams: Team[] | null; -} - -interface RetryPolicyObject { - [key: string]: { [retryPolicyKey: string]: number } | undefined; -} - -interface GlobalRetryPolicyObject { - [retryPolicyKey: string]: number; -} - -interface GlobalExceptionActivityData { - sum_num_rate_limit_exceptions: number; - daily_data: { date: string; num_rate_limit_exceptions: number }[]; -} - -//["OpenAI", "Azure OpenAI", "Anthropic", "Gemini (Google AI Studio)", "Amazon Bedrock", "OpenAI-Compatible Endpoints (Groq, Together AI, Mistral AI, etc.)"] - -interface ProviderFields { - field_name: string; - field_type: string; - field_description: string; - field_value: string; -} - -interface ProviderSettings { - name: string; - fields: ProviderFields[]; -} - -const retry_policy_map: Record = { - "BadRequestError (400)": "BadRequestErrorRetries", - "AuthenticationError (401)": "AuthenticationErrorRetries", - "TimeoutError (408)": "TimeoutErrorRetries", - "RateLimitError (429)": "RateLimitErrorRetries", - "ContentPolicyViolationError (400)": "ContentPolicyViolationErrorRetries", - "InternalServerError (500)": "InternalServerErrorRetries", -}; - -const OldModelDashboard: React.FC = ({ - accessToken, - token, - userRole, - userID, - modelData = { data: [] }, - keys, - setModelData, - premiumUser, - teams, -}) => { - const [addModelForm] = Form.useForm(); - const [autoRouterForm] = Form.useForm(); - const [modelMap, setModelMap] = useState(null); - const [lastRefreshed, setLastRefreshed] = useState(""); - - const [providerModels, setProviderModels] = useState>([]); // Explicitly typing providerModels as a string array - - const [providerSettings, setProviderSettings] = useState([]); - const [selectedProvider, setSelectedProvider] = useState(Providers.OpenAI); - const [healthCheckResponse, setHealthCheckResponse] = useState(null); - const [isHealthCheckLoading, setIsHealthCheckLoading] = useState(false); - const [editModalVisible, setEditModalVisible] = useState(false); - - const [selectedModel, setSelectedModel] = useState(null); - const [availableModelGroups, setAvailableModelGroups] = useState>([]); - const [availableModelAccessGroups, setAvailableModelAccessGroups] = useState>([]); - const [selectedModelGroup, setSelectedModelGroup] = useState(null); - const [modelMetrics, setModelMetrics] = useState([]); - const [modelMetricsCategories, setModelMetricsCategories] = useState([]); - const [streamingModelMetrics, setStreamingModelMetrics] = useState([]); - const [streamingModelMetricsCategories, setStreamingModelMetricsCategories] = useState([]); - const [modelExceptions, setModelExceptions] = useState([]); - const [allExceptions, setAllExceptions] = useState([]); - const [slowResponsesData, setSlowResponsesData] = useState([]); - const [dateValue, setDateValue] = useState({ - from: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), - to: new Date(), - }); - - const [modelGroupRetryPolicy, setModelGroupRetryPolicy] = useState(null); - const [globalRetryPolicy, setGlobalRetryPolicy] = useState(null); - const [defaultRetry, setDefaultRetry] = useState(0); - - const [globalExceptionData, setGlobalExceptionData] = useState( - {} as GlobalExceptionActivityData, - ); - const [globalExceptionPerDeployment, setGlobalExceptionPerDeployment] = useState([]); - - const [showAdvancedFilters, setShowAdvancedFilters] = useState(false); - const [selectedAPIKey, setSelectedAPIKey] = useState(null); - const [selectedCustomer, setSelectedCustomer] = useState(null); - - const [allEndUsers, setAllEndUsers] = useState([]); - - const [credentialsList, setCredentialsList] = useState([]); - - // Model Group Alias state - const [modelGroupAlias, setModelGroupAlias] = useState<{ [key: string]: string }>({}); - - // Add state for advanced settings visibility - const [showAdvancedSettings, setShowAdvancedSettings] = useState(false); - - // Add these state variables - const [selectedModelId, setSelectedModelId] = useState(null); - const [editModel, setEditModel] = useState(false); - - const [selectedTeamId, setSelectedTeamId] = useState(null); - const [selectedTeam, setSelectedTeam] = useState(null); - - const [selectedTeamFilter, setSelectedTeamFilter] = useState(null); - const [selectedModelAccessGroupFilter, setSelectedModelAccessGroupFilter] = useState(null); - - const [modelNameSearch, setModelNameSearch] = useState(""); - - // Add new state for current team and model view mode - const [currentTeam, setCurrentTeam] = useState("personal"); // 'personal' or team_id - const [modelViewMode, setModelViewMode] = useState<"current_team" | "all">("current_team"); - - // Add state for showing/hiding filters - const [showFilters, setShowFilters] = useState(false); - - const [showColumnDropdown, setShowColumnDropdown] = useState(false); - - const [isDropdownOpen, setIsDropdownOpen] = useState(false); - const [expandedRows, setExpandedRows] = useState>(new Set()); - const dropdownRef = useRef(null); - - // Pagination state - const [pagination, setPagination] = useState({ - pageIndex: 0, - pageSize: 50, - }); - const [selectedTabIndex, setSelectedTabIndex] = useState(0); - - const handleCreateNewModelClick = () => { - if (selectedModelId) { - setSelectedModelId(null); - } - setSelectedTabIndex(1); - }; - - const resetFilters = () => { - setModelNameSearch(""); - setSelectedModelGroup("all"); - setSelectedModelAccessGroupFilter(null); - setCurrentTeam("personal"); - setModelViewMode("current_team"); - setPagination({ pageIndex: 0, pageSize: 50 }); - }; - - // Memoize filtered data to prevent unnecessary re-calculations - const filteredData = useMemo(() => { - if (!modelData || !modelData.data || modelData.data.length === 0) { - return []; - } - - return modelData.data.filter((model: any) => { - const searchMatch = - modelNameSearch === "" || model.model_name.toLowerCase().includes(modelNameSearch.toLowerCase()); - - const modelNameMatch = - selectedModelGroup === "all" || - model.model_name === selectedModelGroup || - !selectedModelGroup || - (selectedModelGroup === "wildcard" && model.model_name?.includes("*")); - - const accessGroupMatch = - selectedModelAccessGroupFilter === "all" || - model.model_info["access_groups"]?.includes(selectedModelAccessGroupFilter) || - !selectedModelAccessGroupFilter; - - let teamAccessMatch = true; - if (modelViewMode === "current_team") { - if (currentTeam === "personal") { - teamAccessMatch = model.model_info?.direct_access === true; - } else { - teamAccessMatch = model.model_info?.access_via_team_ids?.includes(currentTeam) === true; - } - } - - return searchMatch && modelNameMatch && accessGroupMatch && teamAccessMatch; - }); - }, [modelData, modelNameSearch, selectedModelGroup, selectedModelAccessGroupFilter, currentTeam, modelViewMode]); - - // Memoize paginated data - const paginatedData = useMemo(() => { - const startIndex = pagination.pageIndex * pagination.pageSize; - const endIndex = startIndex + pagination.pageSize; - return filteredData.slice(startIndex, endIndex); - }, [filteredData, pagination.pageIndex, pagination.pageSize]); - - // Reset pagination when filters change - useEffect(() => { - setPagination((prev) => ({ ...prev, pageIndex: 0 })); - }, [modelNameSearch, selectedModelGroup, selectedModelAccessGroupFilter, currentTeam, modelViewMode]); - - const setProviderModelsFn = (provider: Providers) => { - const _providerModels = getProviderModels(provider, modelMap); - setProviderModels(_providerModels); - console.log(`providerModels: ${_providerModels}`); - }; - - const updateModelMetrics = async ( - modelGroup: string | null, - startTime: Date | undefined, - endTime: Date | undefined, - ) => { - console.log("Updating model metrics for group:", modelGroup); - if (!accessToken || !userID || !userRole || !startTime || !endTime) { - return; - } - console.log("inside updateModelMetrics - startTime:", startTime, "endTime:", endTime); - setSelectedModelGroup(modelGroup); - - let selected_token = selectedAPIKey?.token; - if (selected_token === undefined) { - selected_token = null; - } - - let selected_customer = selectedCustomer; - if (selected_customer === undefined) { - selected_customer = null; - } - - try { - const modelMetricsResponse = await modelMetricsCall( - accessToken, - userID, - userRole, - modelGroup, - startTime.toISOString(), - endTime.toISOString(), - selected_token, - selected_customer, - ); - console.log("Model metrics response:", modelMetricsResponse); - - // Assuming modelMetricsResponse now contains the metric data for the specified model group - setModelMetrics(modelMetricsResponse.data); - setModelMetricsCategories(modelMetricsResponse.all_api_bases); - - const streamingModelMetricsResponse = await streamingModelMetricsCall( - accessToken, - modelGroup, - startTime.toISOString(), - endTime.toISOString(), - ); - - // Assuming modelMetricsResponse now contains the metric data for the specified model group - setStreamingModelMetrics(streamingModelMetricsResponse.data); - setStreamingModelMetricsCategories(streamingModelMetricsResponse.all_api_bases); - - const modelExceptionsResponse = await modelExceptionsCall( - accessToken, - userID, - userRole, - modelGroup, - startTime.toISOString(), - endTime.toISOString(), - selected_token, - selected_customer, - ); - console.log("Model exceptions response:", modelExceptionsResponse); - setModelExceptions(modelExceptionsResponse.data); - setAllExceptions(modelExceptionsResponse.exception_types); - - const slowResponses = await modelMetricsSlowResponsesCall( - accessToken, - userID, - userRole, - modelGroup, - startTime.toISOString(), - endTime.toISOString(), - selected_token, - selected_customer, - ); - - console.log("slowResponses:", slowResponses); - - setSlowResponsesData(slowResponses); - - if (modelGroup) { - const dailyExceptions = await adminGlobalActivityExceptions( - accessToken, - startTime?.toISOString().split("T")[0], - endTime?.toISOString().split("T")[0], - modelGroup, - ); - - setGlobalExceptionData(dailyExceptions); - - const dailyExceptionsPerDeplyment = await adminGlobalActivityExceptionsPerDeployment( - accessToken, - startTime?.toISOString().split("T")[0], - endTime?.toISOString().split("T")[0], - modelGroup, - ); - - setGlobalExceptionPerDeployment(dailyExceptionsPerDeplyment); - } - } catch (error) { - console.error("Failed to fetch model metrics", error); - } - }; - - const fetchCredentials = async (accessToken: string) => { - try { - const response: CredentialsResponse = await credentialListCall(accessToken); - console.log(`credentials: ${JSON.stringify(response)}`); - setCredentialsList(response.credentials); - } catch (error) { - console.error("Error fetching credentials:", error); - } - }; - - useEffect(() => { - updateModelMetrics(selectedModelGroup, dateValue.from, dateValue.to); - }, [selectedAPIKey, selectedCustomer, selectedTeam]); - - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { - setIsDropdownOpen(false); - } - }; - - document.addEventListener("mousedown", handleClickOutside); - return () => document.removeEventListener("mousedown", handleClickOutside); - }, []); - - function formatCreatedAt(createdAt: string | null) { - if (createdAt) { - const date = new Date(createdAt); - const options = { month: "long", day: "numeric", year: "numeric" }; - return date.toLocaleDateString("en-US"); - } - return null; - } - - const handleEditClick = (model: any) => { - setSelectedModel(model); - setEditModalVisible(true); - }; - - const handleEditCancel = () => { - setEditModalVisible(false); - setSelectedModel(null); - }; - - const uploadProps: UploadProps = { - name: "file", - accept: ".json", - beforeUpload: (file) => { - if (file.type === "application/json") { - const reader = new FileReader(); - reader.onload = (e) => { - if (e.target) { - const jsonStr = e.target.result as string; - console.log(`Resetting vertex_credentials to JSON; jsonStr: ${jsonStr}`); - addModelForm.setFieldsValue({ vertex_credentials: jsonStr }); - console.log("Form values right after setting:", addModelForm.getFieldsValue()); - } - }; - reader.readAsText(file); - } - // Prevent upload - return false; - }, - onChange(info) { - console.log("Upload onChange triggered with values:", info); - console.log("Current form values:", addModelForm.getFieldsValue()); - - if (info.file.status !== "uploading") { - console.log(info.file, info.fileList); - } - if (info.file.status === "done") { - NotificationsManager.success(`${info.file.name} file uploaded successfully`); - } else if (info.file.status === "error") { - NotificationsManager.fromBackend(`${info.file.name} file upload failed.`); - } - }, - }; - - const handleRefreshClick = () => { - // Update the 'lastRefreshed' state to the current date and time - const currentDate = new Date(); - setLastRefreshed(currentDate.toLocaleString()); - }; - - const handleSaveRetrySettings = async () => { - if (!accessToken) { - console.error("Access token is missing"); - return; - } - - try { - const payload: any = { - router_settings: {}, - }; - - if (selectedModelGroup === "global") { - // Only update global retry policy - console.log("Saving global retry policy:", globalRetryPolicy); - if (globalRetryPolicy) { - payload.router_settings.retry_policy = globalRetryPolicy; - } - NotificationsManager.success("Global retry settings saved successfully"); - } else { - // Only update model group retry policy - console.log("Saving model group retry policy for", selectedModelGroup, ":", modelGroupRetryPolicy); - if (modelGroupRetryPolicy) { - payload.router_settings.model_group_retry_policy = modelGroupRetryPolicy; - } - NotificationsManager.success(`Retry settings saved successfully for ${selectedModelGroup}`); - } - - await setCallbacksCall(accessToken, payload); - } catch (error) { - console.error("Failed to save retry settings:", error); - NotificationsManager.fromBackend("Failed to save retry settings"); - } - }; - - useEffect(() => { - if (!accessToken || !token || !userRole || !userID) { - return; - } - const fetchData = async () => { - try { - // Replace with your actual API call for model data - const modelDataResponse = await modelInfoCall(accessToken, userID, userRole); - console.log("Model data response:", modelDataResponse.data); - setModelData(modelDataResponse); - const _providerSettings = await modelSettingsCall(accessToken); - if (_providerSettings) { - setProviderSettings(_providerSettings); - } - - // loop through modelDataResponse and get all`model_name` values - let all_model_groups: Set = new Set(); - for (let i = 0; i < modelDataResponse.data.length; i++) { - const model = modelDataResponse.data[i]; - all_model_groups.add(model.model_name); - } - console.log("all_model_groups:", all_model_groups); - let _array_model_groups = Array.from(all_model_groups); - // sort _array_model_groups alphabetically - _array_model_groups = _array_model_groups.sort(); - - setAvailableModelGroups(_array_model_groups); - - let all_model_access_groups: Set = new Set(); - for (let i = 0; i < modelDataResponse.data.length; i++) { - const model = modelDataResponse.data[i]; - let model_info: any | null = model.model_info; - if (model_info) { - let access_groups = model_info.access_groups; - if (access_groups) { - for (let j = 0; j < access_groups.length; j++) { - all_model_access_groups.add(access_groups[j]); - } - } - } - } - - setAvailableModelAccessGroups(Array.from(all_model_access_groups)); - - console.log("array_model_groups:", _array_model_groups); - let _initial_model_group = "all"; - if (_array_model_groups.length > 0) { - // set selectedModelGroup to the last model group - _initial_model_group = _array_model_groups[_array_model_groups.length - 1]; - console.log("_initial_model_group:", _initial_model_group); - //setSelectedModelGroup(_initial_model_group); - } - - console.log("selectedModelGroup:", selectedModelGroup); - - const modelMetricsResponse = await modelMetricsCall( - accessToken, - userID, - userRole, - _initial_model_group, - dateValue.from?.toISOString(), - dateValue.to?.toISOString(), - selectedAPIKey?.token, - selectedCustomer, - ); - - console.log("Model metrics response:", modelMetricsResponse); - // Sort by latency (avg_latency_per_token) - - setModelMetrics(modelMetricsResponse.data); - setModelMetricsCategories(modelMetricsResponse.all_api_bases); - - const streamingModelMetricsResponse = await streamingModelMetricsCall( - accessToken, - _initial_model_group, - dateValue.from?.toISOString(), - dateValue.to?.toISOString(), - ); - - // Assuming modelMetricsResponse now contains the metric data for the specified model group - setStreamingModelMetrics(streamingModelMetricsResponse.data); - setStreamingModelMetricsCategories(streamingModelMetricsResponse.all_api_bases); - - const modelExceptionsResponse = await modelExceptionsCall( - accessToken, - userID, - userRole, - _initial_model_group, - dateValue.from?.toISOString(), - dateValue.to?.toISOString(), - selectedAPIKey?.token, - selectedCustomer, - ); - console.log("Model exceptions response:", modelExceptionsResponse); - setModelExceptions(modelExceptionsResponse.data); - setAllExceptions(modelExceptionsResponse.exception_types); - - const slowResponses = await modelMetricsSlowResponsesCall( - accessToken, - userID, - userRole, - _initial_model_group, - dateValue.from?.toISOString(), - dateValue.to?.toISOString(), - selectedAPIKey?.token, - selectedCustomer, - ); - - const dailyExceptions = await adminGlobalActivityExceptions( - accessToken, - dateValue.from?.toISOString().split("T")[0], - dateValue.to?.toISOString().split("T")[0], - _initial_model_group, - ); - - setGlobalExceptionData(dailyExceptions); - - const dailyExceptionsPerDeplyment = await adminGlobalActivityExceptionsPerDeployment( - accessToken, - dateValue.from?.toISOString().split("T")[0], - dateValue.to?.toISOString().split("T")[0], - _initial_model_group, - ); - - setGlobalExceptionPerDeployment(dailyExceptionsPerDeplyment); - - console.log("dailyExceptions:", dailyExceptions); - - console.log("dailyExceptionsPerDeplyment:", dailyExceptionsPerDeplyment); - - console.log("slowResponses:", slowResponses); - - setSlowResponsesData(slowResponses); - - let all_end_users_data = await allEndUsersCall(accessToken); - - setAllEndUsers(all_end_users_data?.map((u: any) => u.user_id)); - - const routerSettingsInfo = await getCallbacksCall(accessToken, userID, userRole); - - let router_settings = routerSettingsInfo.router_settings; - - console.log("routerSettingsInfo:", router_settings); - - let model_group_retry_policy = router_settings.model_group_retry_policy; - let default_retries = router_settings.num_retries; - - console.log("model_group_retry_policy:", model_group_retry_policy); - console.log("default_retries:", default_retries); - setModelGroupRetryPolicy(model_group_retry_policy); - setGlobalRetryPolicy(router_settings.retry_policy); - setDefaultRetry(default_retries); - - // Set model group alias - const model_group_alias = router_settings.model_group_alias || {}; - setModelGroupAlias(model_group_alias); - } catch (error) { - console.error("There was an error fetching the model data", error); - } - }; - - if (accessToken && token && userRole && userID) { - fetchData(); - } - - const fetchModelMap = async () => { - const data = await modelCostMap(); - console.log(`received model cost map data: ${Object.keys(data)}`); - setModelMap(data); - }; - if (modelMap == null) { - fetchModelMap(); - } - - handleRefreshClick(); - }, [accessToken, token, userRole, userID, modelMap, lastRefreshed, selectedTeam]); - - if (!modelData) { - return
Loading...
; - } - - if (!accessToken || !token || !userRole || !userID) { - return
Loading...
; - } - let all_models_on_proxy: any[] = []; - let all_providers: string[] = []; - - // loop through model data and edit each row - for (let i = 0; i < modelData.data.length; i++) { - let curr_model = modelData.data[i]; - let litellm_model_name = curr_model?.litellm_params?.model; - let custom_llm_provider = curr_model?.litellm_params?.custom_llm_provider; - let model_info = curr_model?.model_info; - - let defaultProvider = "openai"; - let provider = ""; - let input_cost = "Undefined"; - let output_cost = "Undefined"; - let max_tokens = "Undefined"; - let max_input_tokens = "Undefined"; - let cleanedLitellmParams = {}; - - const getProviderFromModel = (model: string) => { - /** - * Use model map - * - check if model in model map - * - return it's litellm_provider, if so - */ - console.log(`GET PROVIDER CALLED! - ${modelMap}`); - if (modelMap !== null && modelMap !== undefined) { - if (typeof modelMap == "object" && model in modelMap) { - return modelMap[model]["litellm_provider"]; - } - } - return "openai"; - }; - - // Check if litellm_model_name is null or undefined - if (litellm_model_name) { - // Split litellm_model_name based on "/" - let splitModel = litellm_model_name.split("/"); - - // Get the first element in the split - let firstElement = splitModel[0]; - - // If there is only one element, default provider to openai - provider = custom_llm_provider; - if (!provider) { - provider = splitModel.length === 1 ? getProviderFromModel(litellm_model_name) : firstElement; - } - } else { - // litellm_model_name is null or undefined, default provider to openai - provider = "-"; - } - - if (model_info) { - input_cost = model_info?.input_cost_per_token; - output_cost = model_info?.output_cost_per_token; - max_tokens = model_info?.max_tokens; - max_input_tokens = model_info?.max_input_tokens; - } - - if (curr_model?.litellm_params) { - cleanedLitellmParams = Object.fromEntries( - Object.entries(curr_model?.litellm_params).filter(([key]) => key !== "model" && key !== "api_base"), - ); - } - - modelData.data[i].provider = provider; - modelData.data[i].input_cost = input_cost; - modelData.data[i].output_cost = output_cost; - modelData.data[i].litellm_model_name = litellm_model_name; - all_providers.push(provider); - - // Convert Cost in terms of Cost per 1M tokens - if (modelData.data[i].input_cost) { - modelData.data[i].input_cost = (Number(modelData.data[i].input_cost) * 1000000).toFixed(2); - } - - if (modelData.data[i].output_cost) { - modelData.data[i].output_cost = (Number(modelData.data[i].output_cost) * 1000000).toFixed(2); - } - - modelData.data[i].max_tokens = max_tokens; - modelData.data[i].max_input_tokens = max_input_tokens; - modelData.data[i].api_base = curr_model?.litellm_params?.api_base; - modelData.data[i].cleanedLitellmParams = cleanedLitellmParams; - - all_models_on_proxy.push(curr_model.model_name); - - console.log(modelData.data[i]); - } - // when users click request access show pop up to allow them to request access - - if (userRole && userRole == "Admin Viewer") { - const { Title, Paragraph } = Typography; - return ( -
- Access Denied - Ask your proxy admin for access to view all models -
- ); - } - - const runHealthCheck = async () => { - try { - NotificationsManager.info("Running health check..."); - setIsHealthCheckLoading(true); - setHealthCheckResponse(null); - const response = await healthCheckCall(accessToken); - setHealthCheckResponse(response); - } catch (error) { - console.error("Error running health check:", error); - setHealthCheckResponse("Error running health check"); - } finally { - setIsHealthCheckLoading(false); - } - }; - - const FilterByContent = ( -
- Select API Key Name - - {premiumUser ? ( -
- - - Select Customer Name - - - - Select Team - - -
- ) : ( -
- {/* ... existing non-premium user content ... */} - Select Team - - -
- )} -
- ); - - const customTooltip = (props: any) => { - const { payload, active } = props; - if (!active || !payload) return null; - - // Extract the date from the first item in the payload array - const date = payload[0]?.payload?.date; - - // Sort the payload array by category.value in descending order - let sortedPayload = payload.sort((a: any, b: any) => b.value - a.value); - - // Only show the top 5, the 6th one should be called "X other categories" depending on how many categories were not shown - if (sortedPayload.length > 5) { - let remainingItems = sortedPayload.length - 5; - sortedPayload = sortedPayload.slice(0, 5); - sortedPayload.push({ - dataKey: `${remainingItems} other deployments`, - value: payload.slice(5).reduce((acc: number, curr: any) => acc + curr.value, 0), - color: "gray", - }); - } - - return ( -
- {date &&

Date: {date}

} - {sortedPayload.map((category: any, idx: number) => { - const roundedValue = parseFloat(category.value.toFixed(5)); - const displayValue = roundedValue === 0 && category.value > 0 ? "<0.00001" : roundedValue.toFixed(5); - return ( -
-
-
-

{category.dataKey}

-
-

{displayValue}

-
- ); - })} -
- ); - }; - - const handleOk = async () => { - console.log("🚀 handleOk called from model dashboard!"); - console.log("Current form values:", addModelForm.getFieldsValue()); - - addModelForm - .validateFields() - .then((values: any) => { - console.log("✅ Validation passed, submitting:", values); - handleAddModelSubmit(values, accessToken, addModelForm, handleRefreshClick); - }) - .catch((error: any) => { - console.error("❌ Validation failed:", error); - console.error("Form errors:", error.errorFields); - const errorMessages = - error.errorFields - ?.map((field: any) => { - return `${field.name.join(".")}: ${field.errors.join(", ")}`; - }) - .join(" | ") || "Unknown validation error"; - NotificationsManager.fromBackend(`Please fill in the following required fields: ${errorMessages}`); - }); - }; - - console.log(`selectedProvider: ${selectedProvider}`); - console.log(`providerModels.length: ${providerModels.length}`); - - const providerKey = Object.keys(Providers).find( - (key) => (Providers as { [index: string]: any })[key] === selectedProvider, - ); - - let dynamicProviderForm: ProviderSettings | undefined = undefined; - if (providerKey && providerSettings) { - dynamicProviderForm = providerSettings.find((provider) => provider.name === provider_map[providerKey]); - } - - // If a team is selected, render TeamInfoView in full page layout - if (selectedTeamId) { - return ( -
- setSelectedTeamId(null)} - accessToken={accessToken} - is_team_admin={userRole === "Admin"} - is_proxy_admin={userRole === "Proxy Admin"} - userModels={all_models_on_proxy} - editTeam={false} - onUpdate={handleRefreshClick} - /> -
- ); - } - - return ( -
- - - {/* Model Management Header */} -
-
-

Model Management

- {!all_admin_roles.includes(userRole) ? ( -

Add models for teams you are an admin for.

- ) : ( -

Add and manage models for the proxy

- )} -
-
- {selectedModelId ? ( - { - setSelectedModelId(null); - setEditModel(false); - }} - accessToken={accessToken} - userID={userID} - userRole={userRole} - onModelUpdate={(updatedModel) => { - // Update the model in the modelData.data array - const updatedModelData = { - ...modelData, - data: modelData.data.map((model: any) => - model.model_info.id === updatedModel.model_info.id ? updatedModel : model, - ), - }; - setModelData(updatedModelData); - // Trigger a refresh to update UI - handleRefreshClick(); - }} - modelAccessGroups={availableModelAccessGroups} - /> - ) : ( - - -
- {all_admin_roles.includes(userRole) ? All Models : Your Models} - Add Model - {all_admin_roles.includes(userRole) && LLM Credentials} - {all_admin_roles.includes(userRole) && Pass-Through Endpoints} - {all_admin_roles.includes(userRole) && Health Status} - {all_admin_roles.includes(userRole) && Model Analytics} - {all_admin_roles.includes(userRole) && Model Retry Settings} - {all_admin_roles.includes(userRole) && Model Group Alias} - {all_admin_roles.includes(userRole) && Price Data Reload} -
- -
- {lastRefreshed && Last Refreshed: {lastRefreshed}} - -
-
- - - -
-
- {/* Current Team and View Mode Selector - Prominent Section */} -
-
-
- Current Team: - -
- -
- View: - -
-
- - {modelViewMode === "current_team" && ( -
- -
- {currentTeam === "personal" ? ( - - To access these models: Create a Virtual Key without selecting a team on the{" "} - - Virtual Keys page - - - ) : ( - - To access these models: Create a Virtual Key and select Team as " - {currentTeam}" on the{" "} - - Virtual Keys page - - - )} -
-
- )} -
- - {/* Search and Filter Controls */} -
-
- {/* Search and Filter Controls */} -
- {/* Model Name Search */} -
- setModelNameSearch(e.target.value)} - /> - - - -
- - {/* Filter Button */} - - - {/* Reset Filters Button */} - -
- - {/* Additional Filters */} - {showFilters && ( -
- {/* Model Name Filter */} -
- -
- - {/* Model Access Group Filter */} -
- -
-
- )} - - {/* Results Count and Pagination Controls */} -
- - {filteredData.length > 0 - ? `Showing ${pagination.pageIndex * pagination.pageSize + 1} - ${Math.min( - (pagination.pageIndex + 1) * pagination.pageSize, - filteredData.length, - )} of ${filteredData.length} results` - : "Showing 0 results"} - - - {/* Pagination Controls */} - {filteredData.length > pagination.pageSize && ( -
- - - -
- )} -
-
-
- - -
-
-
-
- - - - - - - - - - - - - - - - { - setDateValue(value); - updateModelMetrics(selectedModelGroup, value.from, value.to); - }} - /> - - - Select Model Group - - - - - - - - - - - - - - - Avg. Latency per Token - Time to first token - - - -

(seconds/token)

- - average Latency for successfull requests divided by the total tokens - - {modelMetrics && modelMetricsCategories && ( - - )} -
- - - -
-
-
- - - - - - - Deployment - Success Responses - - Slow Responses

Success Responses taking 600+s

-
-
-
- - {slowResponsesData.map((metric, idx) => ( - - {metric.api_base} - {metric.total_count} - {metric.slow_count} - - ))} - -
-
- -
- - - All Exceptions for {selectedModelGroup} - - - - - - - - All Up Rate Limit Errors (429) for {selectedModelGroup} - - - - Num Rate Limit Errors {globalExceptionData.sum_num_rate_limit_exceptions} - - console.log(v)} - /> - - - - - - {premiumUser ? ( - <> - {globalExceptionPerDeployment.map((globalActivity, index) => ( - - {globalActivity.api_base ? globalActivity.api_base : "Unknown API Base"} - - - - Num Rate Limit Errors (429) {globalActivity.sum_num_rate_limit_exceptions} - - console.log(v)} - /> - - - - ))} - - ) : ( - <> - {globalExceptionPerDeployment && - globalExceptionPerDeployment.length > 0 && - globalExceptionPerDeployment.slice(0, 1).map((globalActivity, index) => ( - - ✨ Rate Limit Errors by Deployment -

- Upgrade to see exceptions for all deployments -

- - - {globalActivity.api_base} - - - - Num Rate Limit Errors {globalActivity.sum_num_rate_limit_exceptions} - - console.log(v)} - /> - - - -
- ))} - - )} -
-
- -
-
- Retry Policy Scope: - -
-
- - {selectedModelGroup === "global" ? ( - <> - Global Retry Policy - Default retry settings applied to all model groups unless overridden - - ) : ( - <> - Retry Policy for {selectedModelGroup} - - Model-specific retry settings. Falls back to global defaults if not set. - - - )} - {retry_policy_map && ( - - - {Object.entries(retry_policy_map).map(([exceptionType, retryPolicyKey], idx) => { - let retryCount: number; - - if (selectedModelGroup === "global") { - // Show global policy values - retryCount = globalRetryPolicy?.[retryPolicyKey] ?? defaultRetry; - } else { - // Show model-group specific values with fallback to global - const modelSpecificCount = modelGroupRetryPolicy?.[selectedModelGroup!]?.[retryPolicyKey]; - if (modelSpecificCount != null) { - retryCount = modelSpecificCount; - } else { - // Fall back to global policy, then default - retryCount = globalRetryPolicy?.[retryPolicyKey] ?? defaultRetry; - } - } - - return ( - - - - - ); - })} - -
- {exceptionType} - {selectedModelGroup !== "global" && ( - - (Global: {globalRetryPolicy?.[retryPolicyKey] ?? defaultRetry}) - - )} - - { - if (selectedModelGroup === "global") { - // Update global policy - setGlobalRetryPolicy((prevGlobalRetryPolicy) => { - if (value == null) return prevGlobalRetryPolicy; - return { - ...(prevGlobalRetryPolicy ?? {}), - [retryPolicyKey]: value, - }; - }); - } else { - // Update model-group specific policy - setModelGroupRetryPolicy((prevModelGroupRetryPolicy) => { - const prevRetryPolicy = prevModelGroupRetryPolicy?.[selectedModelGroup!] ?? {}; - return { - ...(prevModelGroupRetryPolicy ?? {}), - [selectedModelGroup!]: { - ...prevRetryPolicy, - [retryPolicyKey!]: value, - }, - } as RetryPolicyObject; - }); - } - }} - /> -
- )} - -
- - - - -
-
- Price Data Management - - Manage model pricing data and configure automatic reload schedules - -
- { - // Refresh the model map after successful reload - const fetchModelMap = async () => { - const data = await modelCostMap(); - setModelMap(data); - }; - fetchModelMap(); - }} - buttonText="Reload Price Data" - size="middle" - type="primary" - className="w-full" - /> -
-
-
-
- )} - -
-
- ); -}; - -export default OldModelDashboard; diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/constants.ts b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/constants.ts index 3186cc27b42..67c274a6d29 100644 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/constants.ts +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/constants.ts @@ -29,7 +29,6 @@ export const FONT_SIZE_HEADER = 16; // Colors export const COLOR_BORDER = "#f0f0f0"; export const COLOR_BACKGROUND = "#fff"; -export const COLOR_SECONDARY = "#8c8c8c"; export const COLOR_BG_LIGHT = "#fafafa"; // Spacing @@ -38,5 +37,3 @@ export const SPACING_MEDIUM = 8; export const SPACING_LARGE = 12; export const SPACING_XLARGE = 16; export const SPACING_XXLARGE = 24; - -// Messages (kept for backwards compatibility if needed elsewhere) diff --git a/ui/litellm-dashboard/src/components/view_logs/constants.ts b/ui/litellm-dashboard/src/components/view_logs/constants.ts new file mode 100644 index 00000000000..84862fa4632 --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/constants.ts @@ -0,0 +1,21 @@ +export const ERROR_CODE_OPTIONS: { label: string; value: string }[] = [ + { label: "400 - Bad Request", value: "400" }, + { label: "401 - Invalid Authentication", value: "401" }, + { label: "403 - Permission Denied", value: "403" }, + { label: "404 - Not Found", value: "404" }, + { label: "408 - Request Timeout", value: "408" }, + { label: "422 - Unprocessable Entity", value: "422" }, + { label: "429 - Rate Limited", value: "429" }, + { label: "500 - Internal Server Error", value: "500" }, + { label: "502 - Bad Gateway", value: "502" }, + { label: "503 - Service Unavailable", value: "503" }, + { label: "529 - Overloaded", value: "529" }, +]; + +export const QUICK_SELECT_OPTIONS: { label: string; value: number; unit: string }[] = [ + { label: "Last 15 Minutes", value: 15, unit: "minutes" }, + { label: "Last Hour", value: 1, unit: "hours" }, + { label: "Last 4 Hours", value: 4, unit: "hours" }, + { label: "Last 24 Hours", value: 24, unit: "hours" }, + { label: "Last 7 Days", value: 7, unit: "days" }, +]; diff --git a/ui/litellm-dashboard/src/components/view_logs/country_cell.tsx b/ui/litellm-dashboard/src/components/view_logs/country_cell.tsx deleted file mode 100644 index b4cae556fd0..00000000000 --- a/ui/litellm-dashboard/src/components/view_logs/country_cell.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import * as React from "react"; -import { getCountryFromIP } from "./ip_lookup"; - -interface CountryCellProps { - ipAddress: string | null; -} - -export const CountryCell: React.FC = ({ ipAddress }) => { - const [country, setCountry] = React.useState("-"); - - React.useEffect(() => { - if (!ipAddress) return; - - let mounted = true; - getCountryFromIP(ipAddress) - .then((result) => { - if (mounted) setCountry(result); - }) - .catch(() => { - if (mounted) setCountry("-"); - }); - - return () => { - mounted = false; - }; - }, [ipAddress]); - - return {country}; -}; diff --git a/ui/litellm-dashboard/src/components/view_logs/index.tsx b/ui/litellm-dashboard/src/components/view_logs/index.tsx index d6d3a2297c8..4bce48bfc28 100644 --- a/ui/litellm-dashboard/src/components/view_logs/index.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/index.tsx @@ -24,6 +24,7 @@ import { CostBreakdownViewer } from "./CostBreakdownViewer"; import { ErrorViewer } from "./ErrorViewer"; import { useLogFilterLogic } from "./log_filter_logic"; import { getTimeRangeDisplay } from "./logs_utils"; +import { ERROR_CODE_OPTIONS, QUICK_SELECT_OPTIONS } from "./constants"; import { RequestResponsePanel } from "./RequestResponsePanel"; import { SessionView } from "./SessionView"; import SpendLogsSettingsModal from "./SpendLogsSettingsModal/SpendLogsSettingsModal"; @@ -354,24 +355,6 @@ export default function SpendLogsTable({ setSelectedLog(log); }; - // Function to extract unique error codes from logs - const extractErrorCodes = (logs: LogEntry[], searchText: string = "") => { - const errorCodes = new Set(); - logs.forEach((log) => { - const metadata = log.metadata || {}; - if (metadata.status === "failure" && metadata.error_information) { - const errorCode = metadata.error_information.error_code; - if (errorCode && (!searchText || errorCode.toLowerCase().includes(searchText.toLowerCase()))) { - errorCodes.add(errorCode); - } - } - }); - return Array.from(errorCodes).map((code) => ({ - label: code, - value: code, - })); - }; - const logFilterOptions: FilterOption[] = [ { name: "Team ID", @@ -437,7 +420,14 @@ export default function SpendLogsTable({ label: "Error Code", isSearchable: true, searchFn: async (searchText: string) => { - return extractErrorCodes(logsData.data, searchText); + if (!searchText) return ERROR_CODE_OPTIONS; + const lower = searchText.toLowerCase(); + const filtered = ERROR_CODE_OPTIONS.filter((opt) => opt.label.toLowerCase().includes(lower)); + const isExactValue = ERROR_CODE_OPTIONS.some((opt) => opt.value === searchText.trim()); + if (!isExactValue && searchText.trim()) { + filtered.push({ label: `Use custom code: ${searchText.trim()}`, value: searchText.trim() }); + } + return filtered; }, }, { @@ -474,15 +464,7 @@ export default function SpendLogsTable({ return unit; }; - const quickSelectOptions = [ - { label: "Last 15 Minutes", value: 15, unit: "minutes" }, - { label: "Last Hour", value: 1, unit: "hours" }, - { label: "Last 4 Hours", value: 4, unit: "hours" }, - { label: "Last 24 Hours", value: 24, unit: "hours" }, - { label: "Last 7 Days", value: 7, unit: "days" }, - ]; - - const selectedOption = quickSelectOptions.find( + const selectedOption = QUICK_SELECT_OPTIONS.find( (option) => option.value === selectedTimeInterval.value && option.unit === selectedTimeInterval.unit, ); @@ -599,7 +581,7 @@ export default function SpendLogsTable({ {quickSelectOpen && (
- {quickSelectOptions.map((option) => ( + {QUICK_SELECT_OPTIONS.map((option) => (