diff --git a/litellm/proxy/_experimental/out/onboarding.html b/litellm/proxy/_experimental/out/onboarding.html
deleted file mode 100644
index 51290ebef106..000000000000
--- a/litellm/proxy/_experimental/out/onboarding.html
+++ /dev/null
@@ -1 +0,0 @@
-
LiteLLM Dashboard
\ No newline at end of file
diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py
index c98e54061284..00ea8fb7f495 100644
--- a/litellm/proxy/_types.py
+++ b/litellm/proxy/_types.py
@@ -268,10 +268,11 @@ class LiteLLMRoutes(enum.Enum):
"/v2/key/info",
"/model_group/info",
"/health",
+ "/key/list",
]
# NOTE: ROUTES ONLY FOR MASTER KEY - only the Master Key should be able to Reset Spend
- master_key_only_routes = ["/global/spend/reset", "/key/list"]
+ master_key_only_routes = ["/global/spend/reset"]
management_routes = [ # key
"/key/generate",
@@ -280,6 +281,7 @@ class LiteLLMRoutes(enum.Enum):
"/key/delete",
"/key/info",
"/key/health",
+ "/key/list",
# user
"/user/new",
"/user/update",
@@ -1348,7 +1350,7 @@ class LiteLLM_VerificationToken(LiteLLMPydanticObjectBase):
key_alias: Optional[str] = None
spend: float = 0.0
max_budget: Optional[float] = None
- expires: Optional[str] = None
+ expires: Optional[Union[str, datetime]] = None
models: List = []
aliases: Dict = {}
config: Dict = {}
diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py
index f0b8c84683c4..fff4c16b2dcd 100644
--- a/litellm/proxy/management_endpoints/key_management_endpoints.py
+++ b/litellm/proxy/management_endpoints/key_management_endpoints.py
@@ -1676,6 +1676,52 @@ async def regenerate_key_fn(
raise handle_exception_on_proxy(e)
+def validate_key_list_check(
+ complete_user_info: LiteLLM_UserTable,
+ user_id: Optional[str],
+ team_id: Optional[str],
+ organization_id: Optional[str],
+ key_alias: Optional[str],
+):
+ if complete_user_info.user_role == LitellmUserRoles.PROXY_ADMIN.value:
+ return # proxy admin can see all keys
+
+ # internal user can only see their own keys
+ if user_id:
+ if complete_user_info.user_id != user_id:
+ raise ProxyException(
+ message="You are not authorized to check another user's keys",
+ type=ProxyErrorTypes.bad_request_error,
+ param="user_id",
+ code=status.HTTP_403_FORBIDDEN,
+ )
+
+ if team_id:
+ if team_id not in complete_user_info.teams:
+ raise ProxyException(
+ message="You are not authorized to check this team's keys",
+ type=ProxyErrorTypes.bad_request_error,
+ param="team_id",
+ code=status.HTTP_403_FORBIDDEN,
+ )
+
+ if organization_id:
+ if (
+ complete_user_info.organization_memberships is None
+ or organization_id
+ not in [
+ membership.organization_id
+ for membership in complete_user_info.organization_memberships
+ ]
+ ):
+ raise ProxyException(
+ message="You are not authorized to check this organization's keys",
+ type=ProxyErrorTypes.bad_request_error,
+ param="organization_id",
+ code=status.HTTP_403_FORBIDDEN,
+ )
+
+
@router.get(
"/key/list",
tags=["key management"],
@@ -1689,14 +1735,18 @@ async def list_keys(
size: int = Query(10, description="Page size", ge=1, le=100),
user_id: Optional[str] = Query(None, description="Filter keys by user ID"),
team_id: Optional[str] = Query(None, description="Filter keys by team ID"),
+ organization_id: Optional[str] = Query(
+ None, description="Filter keys by organization ID"
+ ),
key_alias: Optional[str] = Query(None, description="Filter keys by key alias"),
+ return_full_object: bool = Query(False, description="Return full key object"),
) -> KeyListResponseObject:
"""
- List all keys for a given user or team.
+ List all keys for a given user / team / organization.
Returns:
{
- "keys": List[str],
+ "keys": List[str] or List[UserAPIKeyAuth],
"total_count": int,
"current_page": int,
"total_pages": int,
@@ -1706,7 +1756,14 @@ async def list_keys(
from litellm.proxy.proxy_server import prisma_client
# Check for unsupported parameters
- supported_params = {"page", "size", "user_id", "team_id", "key_alias"}
+ supported_params = {
+ "page",
+ "size",
+ "user_id",
+ "team_id",
+ "key_alias",
+ "return_full_object",
+ }
unsupported_params = set(request.query_params.keys()) - supported_params
if unsupported_params:
raise ProxyException(
@@ -1722,6 +1779,47 @@ async def list_keys(
verbose_proxy_logger.error("Database not connected")
raise Exception("Database not connected")
+ if not user_api_key_dict.user_id:
+ raise ProxyException(
+ message="You are not authorized to access this endpoint. No 'user_id' is associated with your API key.",
+ type=ProxyErrorTypes.bad_request_error,
+ param="user_id",
+ code=status.HTTP_403_FORBIDDEN,
+ )
+
+ complete_user_info: Optional[BaseModel] = (
+ await prisma_client.db.litellm_usertable.find_unique(
+ where={"user_id": user_api_key_dict.user_id},
+ include={"organization_memberships": True},
+ )
+ )
+
+ if complete_user_info is None:
+ raise ProxyException(
+ message="You are not authorized to access this endpoint. No 'user_id' is associated with your API key.",
+ type=ProxyErrorTypes.bad_request_error,
+ param="user_id",
+ code=status.HTTP_403_FORBIDDEN,
+ )
+
+ complete_user_info_pydantic_obj = LiteLLM_UserTable(
+ **complete_user_info.model_dump()
+ )
+
+ validate_key_list_check(
+ complete_user_info=complete_user_info_pydantic_obj,
+ user_id=user_id,
+ team_id=team_id,
+ organization_id=organization_id,
+ key_alias=key_alias,
+ )
+
+ if user_id is None and complete_user_info_pydantic_obj.user_role != [
+ LitellmUserRoles.PROXY_ADMIN.value,
+ LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY.value,
+ ]:
+ user_id = user_api_key_dict.user_id
+
response = await _list_key_helper(
prisma_client=prisma_client,
page=page,
@@ -1729,6 +1827,7 @@ async def list_keys(
user_id=user_id,
team_id=team_id,
key_alias=key_alias,
+ return_full_object=return_full_object,
)
verbose_proxy_logger.debug("Successfully prepared response")
@@ -1736,6 +1835,7 @@ async def list_keys(
return response
except Exception as e:
+ verbose_proxy_logger.exception(f"Error in list_keys: {e}")
if isinstance(e, HTTPException):
raise ProxyException(
message=getattr(e, "detail", f"error({str(e)})"),
diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx
index 3a089bf8067f..9fca59315d7e 100644
--- a/ui/litellm-dashboard/src/app/page.tsx
+++ b/ui/litellm-dashboard/src/app/page.tsx
@@ -4,7 +4,7 @@ import React, { Suspense, useEffect, useState } from "react";
import { useSearchParams } from "next/navigation";
import { jwtDecode } from "jwt-decode";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
-
+import { defaultOrg } from "@/components/common_components/default_org";
import Navbar from "@/components/navbar";
import UserDashboard from "@/components/user_dashboard";
import ModelDashboard from "@/components/model_dashboard";
@@ -78,7 +78,7 @@ export default function CreateKeyPage() {
const [userEmail, setUserEmail] = useState(null);
const [teams, setTeams] = useState(null);
const [keys, setKeys] = useState(null);
- const [currentOrg, setCurrentOrg] = useState(null);
+ const [currentOrg, setCurrentOrg] = useState(defaultOrg);
const [organizations, setOrganizations] = useState([]);
const [proxySettings, setProxySettings] = useState({
PROXY_BASE_URL: "",
diff --git a/ui/litellm-dashboard/src/components/common_components/default_org.tsx b/ui/litellm-dashboard/src/components/common_components/default_org.tsx
new file mode 100644
index 000000000000..a1c06a24395d
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/common_components/default_org.tsx
@@ -0,0 +1,6 @@
+import { Organization } from "../networking";
+
+export const defaultOrg = {
+ organization_id: null,
+ organization_alias: "Default Organization"
+} as Organization
\ No newline at end of file
diff --git a/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx b/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx
new file mode 100644
index 000000000000..8bb7cf98b36a
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx
@@ -0,0 +1,144 @@
+import { useState, useEffect } from 'react';
+import { keyListCall, Organization } from '../networking';
+interface Team {
+team_id: string;
+team_alias: string;
+}
+
+export interface KeyResponse {
+token: string;
+key_name: string;
+key_alias: string;
+spend: number;
+max_budget: number;
+expires: string;
+models: string[];
+aliases: Record;
+config: Record;
+user_id: string;
+team_id: string | null;
+max_parallel_requests: number;
+metadata: Record;
+tpm_limit: number;
+rpm_limit: number;
+budget_duration: string;
+budget_reset_at: string;
+allowed_cache_controls: string[];
+permissions: Record;
+model_spend: Record;
+model_max_budget: Record;
+soft_budget_cooldown: boolean;
+blocked: boolean;
+litellm_budget_table: Record;
+org_id: string | null;
+created_at: string;
+updated_at: string;
+team_spend: number;
+team_alias: string;
+team_tpm_limit: number;
+team_rpm_limit: number;
+team_max_budget: number;
+team_models: string[];
+team_blocked: boolean;
+soft_budget: number;
+team_model_aliases: Record;
+team_member_spend: number;
+team_member?: {
+ user_id: string;
+ user_email: string;
+ role: 'admin' | 'user';
+};
+team_metadata: Record;
+end_user_id: string;
+end_user_tpm_limit: number;
+end_user_rpm_limit: number;
+end_user_max_budget: number;
+last_refreshed_at: number;
+api_key: string;
+user_role: 'proxy_admin' | 'user';
+allowed_model_region?: 'eu' | 'us' | string;
+parent_otel_span?: string;
+rpm_limit_per_model: Record;
+tpm_limit_per_model: Record;
+user_tpm_limit: number;
+user_rpm_limit: number;
+user_email: string;
+}
+
+interface KeyListResponse {
+keys: KeyResponse[];
+total_count: number;
+current_page: number;
+total_pages: number;
+}
+
+interface UseKeyListProps {
+selectedTeam?: Team;
+currentOrg: Organization | null;
+accessToken: string;
+}
+
+interface PaginationData {
+currentPage: number;
+totalPages: number;
+totalCount: number;
+}
+
+interface UseKeyListReturn {
+keys: KeyResponse[];
+isLoading: boolean;
+error: Error | null;
+pagination: PaginationData;
+refresh: (params?: Record) => Promise;
+}
+
+const useKeyList = ({ selectedTeam, currentOrg, accessToken }: UseKeyListProps): UseKeyListReturn => {
+ const [keyData, setKeyData] = useState({
+ keys: [],
+ total_count: 0,
+ current_page: 1,
+ total_pages: 0
+ });
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ const fetchKeys = async (params: Record = {}): Promise => {
+ try {
+ console.log("calling fetchKeys");
+ if (!currentOrg || !selectedTeam || !accessToken) {
+ console.log("currentOrg", currentOrg);
+ console.log("selectedTeam", selectedTeam);
+ console.log("accessToken", accessToken);
+ return
+ }
+ setIsLoading(true);
+
+ const data = await keyListCall(accessToken, currentOrg.organization_id, selectedTeam.team_id);
+ console.log("data", data);
+ setKeyData(data);
+ setError(null);
+ } catch (err) {
+ setError(err instanceof Error ? err : new Error('An error occurred'));
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchKeys();
+ }, [selectedTeam, currentOrg]);
+
+ return {
+ keys: keyData.keys,
+ isLoading,
+ error,
+ pagination: {
+ currentPage: keyData.current_page,
+ totalPages: keyData.total_pages,
+ totalCount: keyData.total_count
+ },
+ refresh: fetchKeys
+ };
+};
+
+export default useKeyList;
\ No newline at end of file
diff --git a/ui/litellm-dashboard/src/components/navbar.tsx b/ui/litellm-dashboard/src/components/navbar.tsx
index e304f931a34b..e02706ec4378 100644
--- a/ui/litellm-dashboard/src/components/navbar.tsx
+++ b/ui/litellm-dashboard/src/components/navbar.tsx
@@ -2,9 +2,8 @@ import Link from "next/link";
import React from "react";
import type { MenuProps } from "antd";
import { Dropdown } from "antd";
-import { CogIcon } from "@heroicons/react/outline";
import { Organization } from "@/components/networking";
-
+import { defaultOrg } from "@/components/common_components/default_org";
interface NavbarProps {
userID: string | null;
userRole: string | null;
@@ -75,10 +74,7 @@ const Navbar: React.FC = ({
Default Organization
),
- onClick: () => onOrgChange({
- organization_id: null,
- organization_alias: "Default Organization"
- } as Organization)
+ onClick: () => onOrgChange(defaultOrg)
},
...organizations.filter(org => org.organization_id !== null).map(org => ({
key: org.organization_id ?? "default",
diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx
index a59cf146007c..f385423979af 100644
--- a/ui/litellm-dashboard/src/components/networking.tsx
+++ b/ui/litellm-dashboard/src/components/networking.tsx
@@ -2075,6 +2075,58 @@ export const keyInfoCall = async (accessToken: String, keys: String[]) => {
}
};
+export const keyListCall = async (
+ accessToken: String,
+ organizationID: string | null,
+ teamID: string | null,
+) => {
+ /**
+ * Get all available teams on proxy
+ */
+ try {
+ let url = proxyBaseUrl ? `${proxyBaseUrl}/key/list` : `/key/list`;
+ console.log("in keyListCall");
+ const queryParams = new URLSearchParams();
+
+ if (teamID) {
+ queryParams.append('team_id', teamID.toString());
+ }
+
+ if (organizationID) {
+ queryParams.append('organization_id', organizationID.toString());
+ }
+
+ queryParams.append('return_full_object', 'true');
+
+ const queryString = queryParams.toString();
+ if (queryString) {
+ url += `?${queryString}`;
+ }
+
+ const response = await fetch(url, {
+ method: "GET",
+ headers: {
+ [globalLitellmHeaderName]: `Bearer ${accessToken}`,
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (!response.ok) {
+ const errorData = await response.text();
+ handleError(errorData);
+ throw new Error("Network response was not ok");
+ }
+
+ const data = await response.json();
+ console.log("/team/list API Response:", 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 spendUsersCall = async (accessToken: String, userID: String) => {
try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/spend/users` : `/spend/users`;
diff --git a/ui/litellm-dashboard/src/components/view_key_table.tsx b/ui/litellm-dashboard/src/components/view_key_table.tsx
index ad90932cac1e..012782b8bf7f 100644
--- a/ui/litellm-dashboard/src/components/view_key_table.tsx
+++ b/ui/litellm-dashboard/src/components/view_key_table.tsx
@@ -69,8 +69,10 @@ import {
import { CopyToClipboard } from "react-copy-to-clipboard";
import TextArea from "antd/es/input/TextArea";
-
+import useKeyList from "./key_team_helpers/key_list";
+import { KeyResponse } from "./key_team_helpers/key_list";
const { Option } = Select;
+
const isLocal = process.env.NODE_ENV === "development";
const proxyBaseUrl = isLocal ? "http://localhost:4000" : null;
if (isLocal != true) {
@@ -87,7 +89,7 @@ interface EditKeyModalProps {
interface ModelLimitModalProps {
visible: boolean;
onCancel: () => void;
- token: ItemData;
+ token: KeyResponse;
onSubmit: (updatedMetadata: any) => void;
accessToken: string;
}
@@ -125,6 +127,19 @@ interface ItemData {
// Add any other properties that exist in the item data
}
+interface ModelLimits {
+ [key: string]: number; // Index signature allowing string keys
+}
+
+interface CombinedLimit {
+ tpm: number;
+ rpm: number;
+}
+
+interface CombinedLimits {
+ [key: string]: CombinedLimit; // Index signature allowing string keys
+}
+
const ViewKeyTable: React.FC = ({
userID,
userRole,
@@ -139,15 +154,22 @@ const ViewKeyTable: React.FC = ({
const [isButtonClicked, setIsButtonClicked] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [keyToDelete, setKeyToDelete] = useState(null);
- const [selectedItem, setSelectedItem] = useState(null);
+ const [selectedItem, setSelectedItem] = useState(null);
const [spendData, setSpendData] = useState<
{ day: string; spend: number }[] | null
>(null);
- const [predictedSpendString, setPredictedSpendString] = useState("");
+
+ const { keys, isLoading, error, refresh } = useKeyList({
+ selectedTeam,
+ currentOrg,
+ accessToken
+ });
+
+ console.log("keys", keys);
const [editModalVisible, setEditModalVisible] = useState(false);
const [infoDialogVisible, setInfoDialogVisible] = useState(false);
- const [selectedToken, setSelectedToken] = useState(null);
+ const [selectedToken, setSelectedToken] = useState(null);
const [userModels, setUserModels] = useState([]);
const initialKnownTeamIDs: Set = new Set();
const [modelLimitModalVisible, setModelLimitModalVisible] = useState(false);
@@ -160,75 +182,6 @@ const ViewKeyTable: React.FC = ({
const [knownTeamIDs, setKnownTeamIDs] = useState(initialKnownTeamIDs);
const [guardrailsList, setGuardrailsList] = useState([]);
- // Function to check if user is admin of a team
- const isUserTeamAdmin = (team: any) => {
- if (!team.members_with_roles) return false;
- return team.members_with_roles.some(
- (member: any) => member.role === "admin" && member.user_id === userID
- );
- };
-
- // Combine all keys that user should have access to
- const all_keys_to_display = React.useMemo(() => {
- if (!data) return [];
-
- // Helper function for default team org check
- const matchesDefaultTeamOrg = (key: any) => {
- console.log(`Checking if key matches default team org: ${JSON.stringify(key)}, currentOrg: ${JSON.stringify(currentOrg)}`)
- if (!currentOrg || currentOrg.organization_id === null) {
- return !('organization_id' in key) || key.organization_id === null;
- }
- return key.organization_id === currentOrg.organization_id;
- };
-
- let allKeys: any[] = [];
-
- // Handle no team selected or Default Team case
- if (!selectedTeam || selectedTeam.team_alias === "Default Team") {
-
- console.log(`inside personal keys`)
- // Get personal keys (with org check)
- const personalKeys = data.filter(key =>
- key.team_id == null &&
- matchesDefaultTeamOrg(key)
- );
-
- console.log(`personalKeys: ${JSON.stringify(personalKeys)}`)
-
- // Get admin team keys (no org check)
- const adminTeamKeys = data.filter(key => {
- const keyTeam = teams?.find(team => team.team_id === key.team_id);
- return keyTeam && isUserTeamAdmin(keyTeam) && key.team_id !== "default-team";
- });
-
- console.log(`adminTeamKeys: ${JSON.stringify(adminTeamKeys)}`)
-
- allKeys = [...personalKeys, ...adminTeamKeys];
- }
- // Handle specific team selected
- else {
- const selectedTeamData = teams?.find(t => t.team_id === selectedTeam.team_id);
- if (selectedTeamData) {
- const teamKeys = data.filter(key => {
- if (selectedTeamData.team_id === "default-team") {
- return key.team_id == null && matchesDefaultTeamOrg(key);
- }
- return key.team_id === selectedTeamData.team_id;
- });
- allKeys = teamKeys;
- }
- }
-
- // Final filtering and deduplication
- return Array.from(
- new Map(
- allKeys
- .filter(key => key.team_id !== "litellm-dashboard")
- .map(key => [key.token, key])
- ).values()
- );
- }, [data, teams, selectedTeam, currentOrg]);
-
useEffect(() => {
const calculateNewExpiryTime = (duration: string | undefined) => {
if (!duration) {
@@ -298,7 +251,7 @@ const ViewKeyTable: React.FC = ({
fetchUserModels();
}, [accessToken, userID, userRole]);
- const handleModelLimitClick = (token: ItemData) => {
+ const handleModelLimitClick = (token: KeyResponse) => {
setSelectedToken(token);
setModelLimitModalVisible(true);
};
@@ -668,13 +621,12 @@ const ViewKeyTable: React.FC = ({
if (token.metadata) {
const tpmLimits = token.metadata.model_tpm_limit || {};
const rpmLimits = token.metadata.model_rpm_limit || {};
- const combinedLimits: { [key: string]: { tpm: number; rpm: number } } =
- {};
+ const combinedLimits: CombinedLimits = {};
Object.keys({ ...tpmLimits, ...rpmLimits }).forEach((model) => {
combinedLimits[model] = {
- tpm: tpmLimits[model] || 0,
- rpm: rpmLimits[model] || 0,
+ tpm: (tpmLimits as ModelLimits)[model] || 0,
+ rpm: (rpmLimits as ModelLimits)[model] || 0,
};
});
@@ -1061,10 +1013,6 @@ const ViewKeyTable: React.FC = ({
}
};
- if (data == null) {
- return;
- }
- console.log("RERENDER TRIGGERED");
return (
@@ -1084,8 +1032,8 @@ const ViewKeyTable: React.FC = ({
- {all_keys_to_display &&
- all_keys_to_display.map((item) => {
+ {keys &&
+ keys.map((item) => {
console.log(item);
// skip item if item.team_id == "litellm-dashboard"
if (item.team_id === "litellm-dashboard") {
@@ -1095,9 +1043,7 @@ const ViewKeyTable: React.FC = ({
/**
* if selected team id is null -> show the keys with no team id or team id's that don't exist in db
*/
- console.log(
- `item team id: ${item.team_id}, knownTeamIDs.has(item.team_id): ${knownTeamIDs.has(item.team_id)}, selectedTeam id: ${selectedTeam.team_id}`
- );
+
if (
selectedTeam.team_id == null &&
item.team_id !== null &&
@@ -1147,7 +1093,7 @@ const ViewKeyTable: React.FC = ({
{(() => {
try {
- return parseFloat(item.spend).toFixed(4);
+ return item.spend.toFixed(4);
} catch (error) {
return item.spend;
}
@@ -1324,9 +1270,7 @@ const ViewKeyTable: React.FC = ({
{(() => {
try {
- return parseFloat(
- selectedToken.spend
- ).toFixed(4);
+ return selectedToken.spend.toFixed(4);
} catch (error) {
return selectedToken.spend;
}
@@ -1334,7 +1278,7 @@ const ViewKeyTable: React.FC = ({
-
+
Budget
@@ -1359,7 +1303,7 @@ const ViewKeyTable: React.FC = ({
-
+
Expires