Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Litellm UI stable version 02 12 2025 #8497

Merged
merged 12 commits into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion litellm/proxy/_experimental/out/onboarding.html

This file was deleted.

6 changes: 4 additions & 2 deletions litellm/proxy/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -280,6 +281,7 @@ class LiteLLMRoutes(enum.Enum):
"/key/delete",
"/key/info",
"/key/health",
"/key/list",
# user
"/user/new",
"/user/update",
Expand Down Expand Up @@ -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 = {}
Expand Down
106 changes: 103 additions & 3 deletions litellm/proxy/management_endpoints/key_management_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand All @@ -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,
Expand All @@ -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(
Expand All @@ -1722,20 +1779,63 @@ 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,
size=size,
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")

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)})"),
Expand Down
4 changes: 2 additions & 2 deletions ui/litellm-dashboard/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -78,7 +78,7 @@ export default function CreateKeyPage() {
const [userEmail, setUserEmail] = useState<null | string>(null);
const [teams, setTeams] = useState<null | any[]>(null);
const [keys, setKeys] = useState<null | any[]>(null);
const [currentOrg, setCurrentOrg] = useState<Organization | null>(null);
const [currentOrg, setCurrentOrg] = useState<Organization>(defaultOrg);
const [organizations, setOrganizations] = useState<Organization[]>([]);
const [proxySettings, setProxySettings] = useState<ProxySettings>({
PROXY_BASE_URL: "",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Organization } from "../networking";

export const defaultOrg = {
organization_id: null,
organization_alias: "Default Organization"
} as Organization
144 changes: 144 additions & 0 deletions ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx
Original file line number Diff line number Diff line change
@@ -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<string, unknown>;
config: Record<string, unknown>;
user_id: string;
team_id: string | null;
max_parallel_requests: number;
metadata: Record<string, unknown>;
tpm_limit: number;
rpm_limit: number;
budget_duration: string;
budget_reset_at: string;
allowed_cache_controls: string[];
permissions: Record<string, unknown>;
model_spend: Record<string, number>;
model_max_budget: Record<string, number>;
soft_budget_cooldown: boolean;
blocked: boolean;
litellm_budget_table: Record<string, unknown>;
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<string, string>;
team_member_spend: number;
team_member?: {
user_id: string;
user_email: string;
role: 'admin' | 'user';
};
team_metadata: Record<string, unknown>;
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<string, number>;
tpm_limit_per_model: Record<string, number>;
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<string, unknown>) => Promise<void>;
}

const useKeyList = ({ selectedTeam, currentOrg, accessToken }: UseKeyListProps): UseKeyListReturn => {
const [keyData, setKeyData] = useState<KeyListResponse>({
keys: [],
total_count: 0,
current_page: 1,
total_pages: 0
});
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<Error | null>(null);

const fetchKeys = async (params: Record<string, unknown> = {}): Promise<void> => {
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;
8 changes: 2 additions & 6 deletions ui/litellm-dashboard/src/components/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -75,10 +74,7 @@ const Navbar: React.FC<NavbarProps> = ({
<span className="text-sm">Default Organization</span>
</div>
),
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",
Expand Down
Loading