Merge pull request #2267 from BerriAI/litellm_end_user_spend_tracking

feat(proxy/utils.py): enable end_user + team id tracking in spend logs
This commit is contained in:
Krish Dholakia 2024-02-29 19:31:24 -08:00 committed by GitHub
commit a39df6fb38
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 216 additions and 67 deletions

View file

@ -1152,9 +1152,9 @@ async def update_database(
payload["spend"] = response_cost
if prisma_client is not None:
await prisma_client.insert_data(data=payload, table_name="spend")
elif custom_db_client is not None:
await custom_db_client.insert_data(payload, table_name="spend")
except Exception as e:
verbose_proxy_logger.info(f"Update Spend Logs DB failed to execute")
@ -4140,6 +4140,28 @@ async def global_spend_keys(
return response
@router.get(
"/global/spend/end_users",
tags=["Budget & Spend Tracking"],
dependencies=[Depends(user_api_key_auth)],
)
async def global_spend_end_users():
"""
[BETA] This is a beta endpoint. It will change.
Use this to get the top 'n' keys with the highest spend, ordered by spend.
"""
global prisma_client
if prisma_client is None:
raise HTTPException(status_code=500, detail={"error": "No db connected"})
sql_query = f"""SELECT * FROM "Last30dTopEndUsersSpend";"""
response = await prisma_client.db.query_raw(query=sql_query)
return response
@router.get(
"/global/spend/models",
tags=["Budget & Spend Tracking"],

View file

@ -100,7 +100,6 @@ model LiteLLM_SpendLogs {
team_id String?
end_user String?
}
// Beta - allow team members to request access to a model
model LiteLLM_UserNotifications {
request_id String @unique

View file

@ -616,6 +616,26 @@ class PrismaClient:
print("MonthlyGlobalSpendPerKey Created!") # noqa
try:
await self.db.query_raw(
"""SELECT 1 FROM "Last30dTopEndUsersSpend" LIMIT 1"""
)
print("Last30dTopEndUsersSpend Exists!") # noqa
except Exception as e:
sql_query = """
CREATE VIEW "Last30dTopEndUsersSpend" AS
SELECT end_user, COUNT(*) AS total_events, SUM(spend) AS total_spend
FROM "LiteLLM_SpendLogs"
WHERE end_user <> '' AND end_user <> user
AND "startTime" >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY end_user
ORDER BY total_spend DESC
LIMIT 100;
"""
await self.db.execute_raw(query=sql_query)
print("Last30dTopEndUsersSpend Created!") # noqa
return
@backoff.on_exception(
@ -1551,7 +1571,12 @@ def get_logging_payload(kwargs, response_obj, start_time, end_time):
"startTime": start_time,
"endTime": end_time,
"model": kwargs.get("model", ""),
"user": kwargs.get("user", ""),
"user": kwargs.get("litellm_params", {})
.get("metadata", {})
.get("user_api_key_user_id", ""),
"team_id": kwargs.get("litellm_params", {})
.get("metadata", {})
.get("user_api_key_team_id", ""),
"metadata": metadata,
"cache_key": cache_key,
"spend": kwargs.get("response_cost", 0),
@ -1559,6 +1584,7 @@ def get_logging_payload(kwargs, response_obj, start_time, end_time):
"prompt_tokens": usage.get("prompt_tokens", 0),
"completion_tokens": usage.get("completion_tokens", 0),
"request_tags": metadata.get("tags", []),
"end_user": kwargs.get("user", ""),
}
verbose_proxy_logger.debug(f"SpendTable: created payload - payload: {payload}\n\n")

View file

@ -138,10 +138,13 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
console.log(`admins: ${admins?.length}`);
return (
<div className="w-full m-2">
<Title level={4}>Proxy Admins</Title>
<Title level={4}>Restricted Access</Title>
<Paragraph>
Add other people to just view global spend. They cannot create teams or
grant users access to new models.
Add other people to just view spend. They cannot create keys, teams or
grant users access to new models.{" "}
<a href="https://docs.litellm.ai/docs/proxy/ui#restrict-ui-access">
Requires SSO Setup
</a>
</Paragraph>
<Grid numItems={1} className="gap-2 p-0 w-full">
<Col numColSpan={1}>

View file

@ -404,13 +404,13 @@ export const adminTopKeysCall = async (accessToken: String) => {
}
};
export const adminTopModelsCall = async (accessToken: String) => {
export const adminTopEndUsersCall = async (accessToken: String) => {
try {
let url = proxyBaseUrl
? `${proxyBaseUrl}/global/spend/models?limit=5`
: `/global/spend/models?limit=5`;
? `${proxyBaseUrl}/global/spend/end_users`
: `/global/spend/end_users`;
message.info("Making spend models request");
message.info("Making top end users request");
const response = await fetch(url, {
method: "GET",
headers: {
@ -426,7 +426,37 @@ export const adminTopModelsCall = async (accessToken: String) => {
const data = await response.json();
console.log(data);
message.success("Spend Logs received");
message.success("Top End users received");
return data;
} catch (error) {
console.error("Failed to create key:", error);
throw error;
}
};
export const adminTopModelsCall = async (accessToken: String) => {
try {
let url = proxyBaseUrl
? `${proxyBaseUrl}/global/spend/models?limit=5`
: `/global/spend/models?limit=5`;
message.info("Making top models request");
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
const errorData = await response.text();
message.error(errorData);
throw new Error("Network response was not ok");
}
const data = await response.json();
console.log(data);
message.success("Top Models received");
return data;
} catch (error) {
console.error("Failed to create key:", error);

View file

@ -5,13 +5,19 @@ import {
Subtitle,
Table,
TableHead,
TableHeaderCell,
TableRow,
TableCell,
TableBody,
Tab,
TabGroup,
TabList,
TabPanels,
Metric,
Grid,
TabPanel,
} from "@tremor/react";
import { userInfoCall } from "./networking";
import { userInfoCall, adminTopEndUsersCall } from "./networking";
import { Badge, BadgeDelta, Button } from "@tremor/react";
import RequestAccess from "./request_model_access";
import CreateUser from "./create_user_button";
@ -29,8 +35,10 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
userRole,
userID,
}) => {
const [userData, setuserData] = useState<null | any[]>(null);
const [pendingRequests, setPendingRequests] = useState<any[]>([]);
const [userData, setUserData] = useState<null | any[]>(null);
const [endUsers, setEndUsers] = useState<null | any[]>(null);
const [currentPage, setCurrentPage] = useState(1);
const defaultPageSize = 25;
useEffect(() => {
if (!accessToken || !token || !userRole || !userID) {
@ -46,15 +54,32 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
true
);
console.log("user data response:", userDataResponse);
setuserData(userDataResponse);
setUserData(userDataResponse);
} catch (error) {
console.error("There was an error fetching the model data", error);
}
};
if (accessToken && token && userRole && userID) {
if (accessToken && token && userRole && userID && !userData) {
fetchData();
}
const fetchEndUserSpend = async () => {
try {
const topEndUsers = await adminTopEndUsersCall(accessToken);
console.log("user data response:", topEndUsers);
setEndUsers(topEndUsers);
} catch (error) {
console.error("There was an error fetching the model data", error);
}
};
if (
userRole &&
(userRole == "Admin" || userRole == "Admin Viewer") &&
!endUsers
) {
fetchEndUserSpend();
}
}, [accessToken, token, userRole, userID]);
if (!userData) {
@ -65,62 +90,106 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
return <div>Loading...</div>;
}
function renderPagination() {
if (!userData) return null;
const totalPages = Math.ceil(userData.length / defaultPageSize);
const startItem = (currentPage - 1) * defaultPageSize + 1;
const endItem = Math.min(currentPage * defaultPageSize, userData.length);
return (
<div className="flex justify-between items-center">
<div>
Showing {startItem} {endItem} of {userData.length}
</div>
<div className="flex">
<button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-l focus:outline-none"
disabled={currentPage === 1}
onClick={() => setCurrentPage(currentPage - 1)}
>
&larr; Prev
</button>
<button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-r focus:outline-none"
disabled={currentPage === totalPages}
onClick={() => setCurrentPage(currentPage + 1)}
>
Next &rarr;
</button>
</div>
</div>
);
}
return (
<div style={{ width: "100%" }}>
<Grid className="gap-2 p-10 h-[75vh] w-full">
<CreateUser userID={userID} accessToken={accessToken} />
<Card>
<Table className="mt-5">
<TableHead>
<TableRow>
<TableCell>
<Title>User ID </Title>
</TableCell>
<TableCell>
<Title>User Role</Title>
</TableCell>
<TableCell>
<Title>User Models</Title>
</TableCell>
<TableCell>
<Title>User Spend ($ USD)</Title>
</TableCell>
<TableCell>
<Title>User Max Budget ($ USD)</Title>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{userData.map((user: any) => (
<TableRow key={user.user_id}>
<TableCell>
<Title>{user.user_id}</Title>
</TableCell>
<TableCell>
<Title>
{user.user_role ? user.user_role : "app_user"}
</Title>
</TableCell>
<TableCell>
<Title>
{user.models && user.models.length > 0
? user.models
: "All Models"}
</Title>
</TableCell>
<TableCell>
<Title>{user.spend ? user.spend : 0}</Title>
</TableCell>
<TableCell>
<Title>
{user.max_budget ? user.max_budget : "Unlimited"}
</Title>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[50vh] mb-4">
<TabGroup>
<TabList variant="line" defaultValue="1">
<Tab value="1">Key Owners</Tab>
<Tab value="2">End-Users</Tab>
</TabList>
<TabPanels>
<TabPanel>
<Table className="mt-5">
<TableHead>
<TableRow>
<TableHeaderCell>User ID</TableHeaderCell>
<TableHeaderCell>User Role</TableHeaderCell>
<TableHeaderCell>User Models</TableHeaderCell>
<TableHeaderCell>User Spend ($ USD)</TableHeaderCell>
<TableHeaderCell>User Max Budget ($ USD)</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{userData.map((user: any) => (
<TableRow key={user.user_id}>
<TableCell>{user.user_id}</TableCell>
<TableCell>
{user.user_role ? user.user_role : "app_owner"}
</TableCell>
<TableCell>
{user.models && user.models.length > 0
? user.models
: "All Models"}
</TableCell>
<TableCell>{user.spend ? user.spend : 0}</TableCell>
<TableCell>
{user.max_budget ? user.max_budget : "Unlimited"}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TabPanel>
<TabPanel>
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>End User</TableHeaderCell>
<TableHeaderCell>Spend</TableHeaderCell>
<TableHeaderCell>Total Events</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{endUsers?.map((user: any, index: number) => (
<TableRow key={index}>
<TableCell>{user.end_user}</TableCell>
<TableCell>{user.total_spend}</TableCell>
<TableCell>{user.total_events}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TabPanel>
</TabPanels>
</TabGroup>
</Card>
{renderPagination()}
</Grid>
</div>
);