mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-26 19:24:27 +00:00
Litellm UI stable version 02 12 2025 (#8497)
* fix(key_management_endpoints.py): fix `/key/list` to include `return_full_object` as a top-level query param Allows user to specify they want the keys as a list of objects * refactor(key_list.tsx): initial refactor of key table in user dashboard offloads key filtering logic to backend api prevents common error of user not being able to see their keys * fix(key_management_endpoints.py): allow internal user to query `/key/list` to see their keys * fix(key_management_endpoints.py): add validation checks and filtering to `/key/list` endpoint allow internal user to see their keys. not anybody else's * fix(view_key_table.tsx): fix issue where internal user could not see default team keys * fix: fix linting error * fix: fix linting error * fix: fix linting error * fix: fix linting error * fix: fix linting error * fix: fix linting error * fix: fix linting error
This commit is contained in:
parent
40e3af0428
commit
1195fe2a44
9 changed files with 351 additions and 108 deletions
File diff suppressed because one or more lines are too long
|
@ -268,10 +268,11 @@ class LiteLLMRoutes(enum.Enum):
|
||||||
"/v2/key/info",
|
"/v2/key/info",
|
||||||
"/model_group/info",
|
"/model_group/info",
|
||||||
"/health",
|
"/health",
|
||||||
|
"/key/list",
|
||||||
]
|
]
|
||||||
|
|
||||||
# NOTE: ROUTES ONLY FOR MASTER KEY - only the Master Key should be able to Reset Spend
|
# 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
|
management_routes = [ # key
|
||||||
"/key/generate",
|
"/key/generate",
|
||||||
|
@ -280,6 +281,7 @@ class LiteLLMRoutes(enum.Enum):
|
||||||
"/key/delete",
|
"/key/delete",
|
||||||
"/key/info",
|
"/key/info",
|
||||||
"/key/health",
|
"/key/health",
|
||||||
|
"/key/list",
|
||||||
# user
|
# user
|
||||||
"/user/new",
|
"/user/new",
|
||||||
"/user/update",
|
"/user/update",
|
||||||
|
@ -1348,7 +1350,7 @@ class LiteLLM_VerificationToken(LiteLLMPydanticObjectBase):
|
||||||
key_alias: Optional[str] = None
|
key_alias: Optional[str] = None
|
||||||
spend: float = 0.0
|
spend: float = 0.0
|
||||||
max_budget: Optional[float] = None
|
max_budget: Optional[float] = None
|
||||||
expires: Optional[str] = None
|
expires: Optional[Union[str, datetime]] = None
|
||||||
models: List = []
|
models: List = []
|
||||||
aliases: Dict = {}
|
aliases: Dict = {}
|
||||||
config: Dict = {}
|
config: Dict = {}
|
||||||
|
|
|
@ -1676,6 +1676,52 @@ async def regenerate_key_fn(
|
||||||
raise handle_exception_on_proxy(e)
|
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(
|
@router.get(
|
||||||
"/key/list",
|
"/key/list",
|
||||||
tags=["key management"],
|
tags=["key management"],
|
||||||
|
@ -1689,14 +1735,18 @@ async def list_keys(
|
||||||
size: int = Query(10, description="Page size", ge=1, le=100),
|
size: int = Query(10, description="Page size", ge=1, le=100),
|
||||||
user_id: Optional[str] = Query(None, description="Filter keys by user ID"),
|
user_id: Optional[str] = Query(None, description="Filter keys by user ID"),
|
||||||
team_id: Optional[str] = Query(None, description="Filter keys by team 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"),
|
key_alias: Optional[str] = Query(None, description="Filter keys by key alias"),
|
||||||
|
return_full_object: bool = Query(False, description="Return full key object"),
|
||||||
) -> KeyListResponseObject:
|
) -> KeyListResponseObject:
|
||||||
"""
|
"""
|
||||||
List all keys for a given user or team.
|
List all keys for a given user / team / organization.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
{
|
{
|
||||||
"keys": List[str],
|
"keys": List[str] or List[UserAPIKeyAuth],
|
||||||
"total_count": int,
|
"total_count": int,
|
||||||
"current_page": int,
|
"current_page": int,
|
||||||
"total_pages": int,
|
"total_pages": int,
|
||||||
|
@ -1706,7 +1756,14 @@ async def list_keys(
|
||||||
from litellm.proxy.proxy_server import prisma_client
|
from litellm.proxy.proxy_server import prisma_client
|
||||||
|
|
||||||
# Check for unsupported parameters
|
# 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
|
unsupported_params = set(request.query_params.keys()) - supported_params
|
||||||
if unsupported_params:
|
if unsupported_params:
|
||||||
raise ProxyException(
|
raise ProxyException(
|
||||||
|
@ -1722,6 +1779,47 @@ async def list_keys(
|
||||||
verbose_proxy_logger.error("Database not connected")
|
verbose_proxy_logger.error("Database not connected")
|
||||||
raise Exception("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(
|
response = await _list_key_helper(
|
||||||
prisma_client=prisma_client,
|
prisma_client=prisma_client,
|
||||||
page=page,
|
page=page,
|
||||||
|
@ -1729,6 +1827,7 @@ async def list_keys(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
team_id=team_id,
|
team_id=team_id,
|
||||||
key_alias=key_alias,
|
key_alias=key_alias,
|
||||||
|
return_full_object=return_full_object,
|
||||||
)
|
)
|
||||||
|
|
||||||
verbose_proxy_logger.debug("Successfully prepared response")
|
verbose_proxy_logger.debug("Successfully prepared response")
|
||||||
|
@ -1736,6 +1835,7 @@ async def list_keys(
|
||||||
return response
|
return response
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
verbose_proxy_logger.exception(f"Error in list_keys: {e}")
|
||||||
if isinstance(e, HTTPException):
|
if isinstance(e, HTTPException):
|
||||||
raise ProxyException(
|
raise ProxyException(
|
||||||
message=getattr(e, "detail", f"error({str(e)})"),
|
message=getattr(e, "detail", f"error({str(e)})"),
|
||||||
|
|
|
@ -4,7 +4,7 @@ import React, { Suspense, useEffect, useState } from "react";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import { jwtDecode } from "jwt-decode";
|
import { jwtDecode } from "jwt-decode";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import { defaultOrg } from "@/components/common_components/default_org";
|
||||||
import Navbar from "@/components/navbar";
|
import Navbar from "@/components/navbar";
|
||||||
import UserDashboard from "@/components/user_dashboard";
|
import UserDashboard from "@/components/user_dashboard";
|
||||||
import ModelDashboard from "@/components/model_dashboard";
|
import ModelDashboard from "@/components/model_dashboard";
|
||||||
|
@ -78,7 +78,7 @@ export default function CreateKeyPage() {
|
||||||
const [userEmail, setUserEmail] = useState<null | string>(null);
|
const [userEmail, setUserEmail] = useState<null | string>(null);
|
||||||
const [teams, setTeams] = useState<null | any[]>(null);
|
const [teams, setTeams] = useState<null | any[]>(null);
|
||||||
const [keys, setKeys] = 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 [organizations, setOrganizations] = useState<Organization[]>([]);
|
||||||
const [proxySettings, setProxySettings] = useState<ProxySettings>({
|
const [proxySettings, setProxySettings] = useState<ProxySettings>({
|
||||||
PROXY_BASE_URL: "",
|
PROXY_BASE_URL: "",
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { Organization } from "../networking";
|
||||||
|
|
||||||
|
export const defaultOrg = {
|
||||||
|
organization_id: null,
|
||||||
|
organization_alias: "Default Organization"
|
||||||
|
} as Organization
|
|
@ -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;
|
|
@ -2,9 +2,8 @@ import Link from "next/link";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import type { MenuProps } from "antd";
|
import type { MenuProps } from "antd";
|
||||||
import { Dropdown } from "antd";
|
import { Dropdown } from "antd";
|
||||||
import { CogIcon } from "@heroicons/react/outline";
|
|
||||||
import { Organization } from "@/components/networking";
|
import { Organization } from "@/components/networking";
|
||||||
|
import { defaultOrg } from "@/components/common_components/default_org";
|
||||||
interface NavbarProps {
|
interface NavbarProps {
|
||||||
userID: string | null;
|
userID: string | null;
|
||||||
userRole: string | null;
|
userRole: string | null;
|
||||||
|
@ -75,10 +74,7 @@ const Navbar: React.FC<NavbarProps> = ({
|
||||||
<span className="text-sm">Default Organization</span>
|
<span className="text-sm">Default Organization</span>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
onClick: () => onOrgChange({
|
onClick: () => onOrgChange(defaultOrg)
|
||||||
organization_id: null,
|
|
||||||
organization_alias: "Default Organization"
|
|
||||||
} as Organization)
|
|
||||||
},
|
},
|
||||||
...organizations.filter(org => org.organization_id !== null).map(org => ({
|
...organizations.filter(org => org.organization_id !== null).map(org => ({
|
||||||
key: org.organization_id ?? "default",
|
key: org.organization_id ?? "default",
|
||||||
|
|
|
@ -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) => {
|
export const spendUsersCall = async (accessToken: String, userID: String) => {
|
||||||
try {
|
try {
|
||||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/spend/users` : `/spend/users`;
|
const url = proxyBaseUrl ? `${proxyBaseUrl}/spend/users` : `/spend/users`;
|
||||||
|
|
|
@ -69,8 +69,10 @@ import {
|
||||||
|
|
||||||
import { CopyToClipboard } from "react-copy-to-clipboard";
|
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||||
import TextArea from "antd/es/input/TextArea";
|
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 { Option } = Select;
|
||||||
|
|
||||||
const isLocal = process.env.NODE_ENV === "development";
|
const isLocal = process.env.NODE_ENV === "development";
|
||||||
const proxyBaseUrl = isLocal ? "http://localhost:4000" : null;
|
const proxyBaseUrl = isLocal ? "http://localhost:4000" : null;
|
||||||
if (isLocal != true) {
|
if (isLocal != true) {
|
||||||
|
@ -87,7 +89,7 @@ interface EditKeyModalProps {
|
||||||
interface ModelLimitModalProps {
|
interface ModelLimitModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
token: ItemData;
|
token: KeyResponse;
|
||||||
onSubmit: (updatedMetadata: any) => void;
|
onSubmit: (updatedMetadata: any) => void;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
}
|
}
|
||||||
|
@ -125,6 +127,19 @@ interface ItemData {
|
||||||
// Add any other properties that exist in the item data
|
// 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<ViewKeyTableProps> = ({
|
const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||||
userID,
|
userID,
|
||||||
userRole,
|
userRole,
|
||||||
|
@ -139,15 +154,22 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||||
const [isButtonClicked, setIsButtonClicked] = useState(false);
|
const [isButtonClicked, setIsButtonClicked] = useState(false);
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
const [keyToDelete, setKeyToDelete] = useState<string | null>(null);
|
const [keyToDelete, setKeyToDelete] = useState<string | null>(null);
|
||||||
const [selectedItem, setSelectedItem] = useState<ItemData | null>(null);
|
const [selectedItem, setSelectedItem] = useState<KeyResponse | null>(null);
|
||||||
const [spendData, setSpendData] = useState<
|
const [spendData, setSpendData] = useState<
|
||||||
{ day: string; spend: number }[] | null
|
{ day: string; spend: number }[] | null
|
||||||
>(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 [editModalVisible, setEditModalVisible] = useState(false);
|
||||||
const [infoDialogVisible, setInfoDialogVisible] = useState(false);
|
const [infoDialogVisible, setInfoDialogVisible] = useState(false);
|
||||||
const [selectedToken, setSelectedToken] = useState<ItemData | null>(null);
|
const [selectedToken, setSelectedToken] = useState<KeyResponse | null>(null);
|
||||||
const [userModels, setUserModels] = useState<string[]>([]);
|
const [userModels, setUserModels] = useState<string[]>([]);
|
||||||
const initialKnownTeamIDs: Set<string> = new Set();
|
const initialKnownTeamIDs: Set<string> = new Set();
|
||||||
const [modelLimitModalVisible, setModelLimitModalVisible] = useState(false);
|
const [modelLimitModalVisible, setModelLimitModalVisible] = useState(false);
|
||||||
|
@ -160,75 +182,6 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||||
const [knownTeamIDs, setKnownTeamIDs] = useState(initialKnownTeamIDs);
|
const [knownTeamIDs, setKnownTeamIDs] = useState(initialKnownTeamIDs);
|
||||||
const [guardrailsList, setGuardrailsList] = useState<string[]>([]);
|
const [guardrailsList, setGuardrailsList] = useState<string[]>([]);
|
||||||
|
|
||||||
// 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(() => {
|
useEffect(() => {
|
||||||
const calculateNewExpiryTime = (duration: string | undefined) => {
|
const calculateNewExpiryTime = (duration: string | undefined) => {
|
||||||
if (!duration) {
|
if (!duration) {
|
||||||
|
@ -298,7 +251,7 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||||
fetchUserModels();
|
fetchUserModels();
|
||||||
}, [accessToken, userID, userRole]);
|
}, [accessToken, userID, userRole]);
|
||||||
|
|
||||||
const handleModelLimitClick = (token: ItemData) => {
|
const handleModelLimitClick = (token: KeyResponse) => {
|
||||||
setSelectedToken(token);
|
setSelectedToken(token);
|
||||||
setModelLimitModalVisible(true);
|
setModelLimitModalVisible(true);
|
||||||
};
|
};
|
||||||
|
@ -668,13 +621,12 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||||
if (token.metadata) {
|
if (token.metadata) {
|
||||||
const tpmLimits = token.metadata.model_tpm_limit || {};
|
const tpmLimits = token.metadata.model_tpm_limit || {};
|
||||||
const rpmLimits = token.metadata.model_rpm_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) => {
|
Object.keys({ ...tpmLimits, ...rpmLimits }).forEach((model) => {
|
||||||
combinedLimits[model] = {
|
combinedLimits[model] = {
|
||||||
tpm: tpmLimits[model] || 0,
|
tpm: (tpmLimits as ModelLimits)[model] || 0,
|
||||||
rpm: rpmLimits[model] || 0,
|
rpm: (rpmLimits as ModelLimits)[model] || 0,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1061,10 +1013,6 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (data == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log("RERENDER TRIGGERED");
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[50vh] mb-4 mt-2">
|
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[50vh] mb-4 mt-2">
|
||||||
|
@ -1084,8 +1032,8 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{all_keys_to_display &&
|
{keys &&
|
||||||
all_keys_to_display.map((item) => {
|
keys.map((item) => {
|
||||||
console.log(item);
|
console.log(item);
|
||||||
// skip item if item.team_id == "litellm-dashboard"
|
// skip item if item.team_id == "litellm-dashboard"
|
||||||
if (item.team_id === "litellm-dashboard") {
|
if (item.team_id === "litellm-dashboard") {
|
||||||
|
@ -1095,9 +1043,7 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||||
/**
|
/**
|
||||||
* if selected team id is null -> show the keys with no team id or team id's that don't exist in db
|
* 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 (
|
if (
|
||||||
selectedTeam.team_id == null &&
|
selectedTeam.team_id == null &&
|
||||||
item.team_id !== null &&
|
item.team_id !== null &&
|
||||||
|
@ -1147,7 +1093,7 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||||
<Text>
|
<Text>
|
||||||
{(() => {
|
{(() => {
|
||||||
try {
|
try {
|
||||||
return parseFloat(item.spend).toFixed(4);
|
return item.spend.toFixed(4);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return item.spend;
|
return item.spend;
|
||||||
}
|
}
|
||||||
|
@ -1324,9 +1270,7 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||||
<p className="text-tremor font-semibold text-tremor-content-strong dark:text-dark-tremor-content-strong">
|
<p className="text-tremor font-semibold text-tremor-content-strong dark:text-dark-tremor-content-strong">
|
||||||
{(() => {
|
{(() => {
|
||||||
try {
|
try {
|
||||||
return parseFloat(
|
return selectedToken.spend.toFixed(4);
|
||||||
selectedToken.spend
|
|
||||||
).toFixed(4);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return selectedToken.spend;
|
return selectedToken.spend;
|
||||||
}
|
}
|
||||||
|
@ -1334,7 +1278,7 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<Card key={item.name}>
|
<Card key={item.key_name}>
|
||||||
<p className="text-tremor-default font-medium text-tremor-content dark:text-dark-tremor-content">
|
<p className="text-tremor-default font-medium text-tremor-content dark:text-dark-tremor-content">
|
||||||
Budget
|
Budget
|
||||||
</p>
|
</p>
|
||||||
|
@ -1359,7 +1303,7 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<Card key={item.name}>
|
<Card key={item.key_name}>
|
||||||
<p className="text-tremor-default font-medium text-tremor-content dark:text-dark-tremor-content">
|
<p className="text-tremor-default font-medium text-tremor-content dark:text-dark-tremor-content">
|
||||||
Expires
|
Expires
|
||||||
</p>
|
</p>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue