mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-11 22:51:28 +00:00
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:
commit
a39df6fb38
6 changed files with 216 additions and 67 deletions
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
>
|
||||
← 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 →
|
||||
</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>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue