Spaces:
Sleeping
Sleeping
#### SPEND MANAGEMENT ##### | |
import collections | |
import os | |
from datetime import datetime, timedelta, timezone | |
from functools import lru_cache | |
from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional | |
import fastapi | |
from fastapi import APIRouter, Depends, HTTPException, status | |
import litellm | |
from litellm._logging import verbose_proxy_logger | |
from litellm.proxy._types import * | |
from litellm.proxy._types import ProviderBudgetResponse, ProviderBudgetResponseObject | |
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth | |
from litellm.proxy.spend_tracking.spend_tracking_utils import ( | |
get_spend_by_team_and_customer, | |
) | |
from litellm.proxy.utils import handle_exception_on_proxy | |
if TYPE_CHECKING: | |
from litellm.proxy.proxy_server import PrismaClient | |
else: | |
PrismaClient = Any | |
router = APIRouter() | |
async def spend_key_fn(): | |
""" | |
View all keys created, ordered by spend | |
Example Request: | |
``` | |
curl -X GET "http://0.0.0.0:8000/spend/keys" \ | |
-H "Authorization: Bearer sk-1234" | |
``` | |
""" | |
from litellm.proxy.proxy_server import prisma_client | |
try: | |
if prisma_client is None: | |
raise Exception( | |
"Database not connected. Connect a database to your proxy - https://docs.litellm.ai/docs/simple_proxy#managing-auth---virtual-keys" | |
) | |
key_info = await prisma_client.get_data(table_name="key", query_type="find_all") | |
return key_info | |
except Exception as e: | |
raise HTTPException( | |
status_code=status.HTTP_400_BAD_REQUEST, | |
detail={"error": str(e)}, | |
) | |
async def spend_user_fn( | |
user_id: Optional[str] = fastapi.Query( | |
default=None, | |
description="Get User Table row for user_id", | |
), | |
): | |
""" | |
View all users created, ordered by spend | |
Example Request: | |
``` | |
curl -X GET "http://0.0.0.0:8000/spend/users" \ | |
-H "Authorization: Bearer sk-1234" | |
``` | |
View User Table row for user_id | |
``` | |
curl -X GET "http://0.0.0.0:8000/spend/users?user_id=1234" \ | |
-H "Authorization: Bearer sk-1234" | |
``` | |
""" | |
from litellm.proxy.proxy_server import prisma_client | |
try: | |
if prisma_client is None: | |
raise Exception( | |
"Database not connected. Connect a database to your proxy - https://docs.litellm.ai/docs/simple_proxy#managing-auth---virtual-keys" | |
) | |
if user_id is not None: | |
user_info = await prisma_client.get_data( | |
table_name="user", query_type="find_unique", user_id=user_id | |
) | |
return [user_info] | |
else: | |
user_info = await prisma_client.get_data( | |
table_name="user", query_type="find_all" | |
) | |
return user_info | |
except Exception as e: | |
raise HTTPException( | |
status_code=status.HTTP_400_BAD_REQUEST, | |
detail={"error": str(e)}, | |
) | |
async def view_spend_tags( | |
start_date: Optional[str] = fastapi.Query( | |
default=None, | |
description="Time from which to start viewing key spend", | |
), | |
end_date: Optional[str] = fastapi.Query( | |
default=None, | |
description="Time till which to view key spend", | |
), | |
): | |
""" | |
LiteLLM Enterprise - View Spend Per Request Tag | |
Example Request: | |
``` | |
curl -X GET "http://0.0.0.0:8000/spend/tags" \ | |
-H "Authorization: Bearer sk-1234" | |
``` | |
Spend with Start Date and End Date | |
``` | |
curl -X GET "http://0.0.0.0:8000/spend/tags?start_date=2022-01-01&end_date=2022-02-01" \ | |
-H "Authorization: Bearer sk-1234" | |
``` | |
""" | |
from enterprise.utils import get_spend_by_tags | |
from litellm.proxy.proxy_server import prisma_client | |
try: | |
if prisma_client is None: | |
raise Exception( | |
"Database not connected. Connect a database to your proxy - https://docs.litellm.ai/docs/simple_proxy#managing-auth---virtual-keys" | |
) | |
# run the following SQL query on prisma | |
""" | |
SELECT | |
jsonb_array_elements_text(request_tags) AS individual_request_tag, | |
COUNT(*) AS log_count, | |
SUM(spend) AS total_spend | |
FROM "LiteLLM_SpendLogs" | |
GROUP BY individual_request_tag; | |
""" | |
response = await get_spend_by_tags( | |
start_date=start_date, end_date=end_date, prisma_client=prisma_client | |
) | |
return response | |
except Exception as e: | |
if isinstance(e, HTTPException): | |
raise ProxyException( | |
message=getattr(e, "detail", f"/spend/tags Error({str(e)})"), | |
type="internal_error", | |
param=getattr(e, "param", "None"), | |
code=getattr(e, "status_code", status.HTTP_500_INTERNAL_SERVER_ERROR), | |
) | |
elif isinstance(e, ProxyException): | |
raise e | |
raise ProxyException( | |
message="/spend/tags Error" + str(e), | |
type="internal_error", | |
param=getattr(e, "param", "None"), | |
code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
) | |
async def get_global_activity_internal_user( | |
user_api_key_dict: UserAPIKeyAuth, start_date: datetime, end_date: datetime | |
): | |
from litellm.proxy.proxy_server import prisma_client | |
if prisma_client is None: | |
raise HTTPException(status_code=500, detail={"error": "No db connected"}) | |
user_id = user_api_key_dict.user_id | |
if user_id is None: | |
raise HTTPException(status_code=500, detail={"error": "No user_id found"}) | |
sql_query = """ | |
SELECT | |
date_trunc('day', "startTime") AS date, | |
COUNT(*) AS api_requests, | |
SUM(total_tokens) AS total_tokens | |
FROM "LiteLLM_SpendLogs" | |
WHERE "startTime" BETWEEN $1::date AND $2::date + interval '1 day' | |
AND "user" = $3 | |
GROUP BY date_trunc('day', "startTime") | |
""" | |
db_response = await prisma_client.db.query_raw( | |
sql_query, start_date, end_date, user_id | |
) | |
return db_response | |
async def get_global_activity( | |
start_date: Optional[str] = fastapi.Query( | |
default=None, | |
description="Time from which to start viewing spend", | |
), | |
end_date: Optional[str] = fastapi.Query( | |
default=None, | |
description="Time till which to view spend", | |
), | |
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), | |
): | |
""" | |
Get number of API Requests, total tokens through proxy | |
{ | |
"daily_data": [ | |
const chartdata = [ | |
{ | |
date: 'Jan 22', | |
api_requests: 10, | |
total_tokens: 2000 | |
}, | |
{ | |
date: 'Jan 23', | |
api_requests: 10, | |
total_tokens: 12 | |
}, | |
], | |
"sum_api_requests": 20, | |
"sum_total_tokens": 2012 | |
} | |
""" | |
if start_date is None or end_date is None: | |
raise HTTPException( | |
status_code=status.HTTP_400_BAD_REQUEST, | |
detail={"error": "Please provide start_date and end_date"}, | |
) | |
start_date_obj = datetime.strptime(start_date, "%Y-%m-%d") | |
end_date_obj = datetime.strptime(end_date, "%Y-%m-%d") | |
from litellm.proxy.proxy_server import prisma_client | |
try: | |
if prisma_client is None: | |
raise Exception( | |
"Database not connected. Connect a database to your proxy - https://docs.litellm.ai/docs/simple_proxy#managing-auth---virtual-keys" | |
) | |
if ( | |
user_api_key_dict.user_role == LitellmUserRoles.INTERNAL_USER | |
or user_api_key_dict.user_role == LitellmUserRoles.INTERNAL_USER_VIEW_ONLY | |
): | |
db_response = await get_global_activity_internal_user( | |
user_api_key_dict, start_date_obj, end_date_obj | |
) | |
else: | |
sql_query = """ | |
SELECT | |
date_trunc('day', "startTime") AS date, | |
COUNT(*) AS api_requests, | |
SUM(total_tokens) AS total_tokens | |
FROM "LiteLLM_SpendLogs" | |
WHERE "startTime" BETWEEN $1::date AND $2::date + interval '1 day' | |
GROUP BY date_trunc('day', "startTime") | |
""" | |
db_response = await prisma_client.db.query_raw( | |
sql_query, start_date_obj, end_date_obj | |
) | |
if db_response is None: | |
return [] | |
sum_api_requests = 0 | |
sum_total_tokens = 0 | |
daily_data = [] | |
for row in db_response: | |
# cast date to datetime | |
_date_obj = datetime.fromisoformat(row["date"]) | |
row["date"] = _date_obj.strftime("%b %d") | |
daily_data.append(row) | |
sum_api_requests += row.get("api_requests", 0) | |
sum_total_tokens += row.get("total_tokens", 0) | |
# sort daily_data by date | |
daily_data = sorted(daily_data, key=lambda x: x["date"]) | |
data_to_return = { | |
"daily_data": daily_data, | |
"sum_api_requests": sum_api_requests, | |
"sum_total_tokens": sum_total_tokens, | |
} | |
return data_to_return | |
except Exception as e: | |
raise HTTPException( | |
status_code=status.HTTP_400_BAD_REQUEST, | |
detail={"error": str(e)}, | |
) | |
async def get_global_activity_model_internal_user( | |
user_api_key_dict: UserAPIKeyAuth, start_date: datetime, end_date: datetime | |
): | |
from litellm.proxy.proxy_server import prisma_client | |
if prisma_client is None: | |
raise HTTPException(status_code=500, detail={"error": "No db connected"}) | |
user_id = user_api_key_dict.user_id | |
if user_id is None: | |
raise HTTPException(status_code=500, detail={"error": "No user_id found"}) | |
sql_query = """ | |
SELECT | |
model_group, | |
date_trunc('day', "startTime") AS date, | |
COUNT(*) AS api_requests, | |
SUM(total_tokens) AS total_tokens | |
FROM "LiteLLM_SpendLogs" | |
WHERE "startTime" BETWEEN $1::date AND $2::date + interval '1 day' | |
AND "user" = $3 | |
GROUP BY model_group, date_trunc('day', "startTime") | |
""" | |
db_response = await prisma_client.db.query_raw( | |
sql_query, start_date, end_date, user_id | |
) | |
return db_response | |
async def get_global_activity_model( | |
start_date: Optional[str] = fastapi.Query( | |
default=None, | |
description="Time from which to start viewing spend", | |
), | |
end_date: Optional[str] = fastapi.Query( | |
default=None, | |
description="Time till which to view spend", | |
), | |
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), | |
): | |
""" | |
Get number of API Requests, total tokens through proxy - Grouped by MODEL | |
[ | |
{ | |
"model": "gpt-4", | |
"daily_data": [ | |
const chartdata = [ | |
{ | |
date: 'Jan 22', | |
api_requests: 10, | |
total_tokens: 2000 | |
}, | |
{ | |
date: 'Jan 23', | |
api_requests: 10, | |
total_tokens: 12 | |
}, | |
], | |
"sum_api_requests": 20, | |
"sum_total_tokens": 2012 | |
}, | |
{ | |
"model": "azure/gpt-4-turbo", | |
"daily_data": [ | |
const chartdata = [ | |
{ | |
date: 'Jan 22', | |
api_requests: 10, | |
total_tokens: 2000 | |
}, | |
{ | |
date: 'Jan 23', | |
api_requests: 10, | |
total_tokens: 12 | |
}, | |
], | |
"sum_api_requests": 20, | |
"sum_total_tokens": 2012 | |
}, | |
] | |
""" | |
if start_date is None or end_date is None: | |
raise HTTPException( | |
status_code=status.HTTP_400_BAD_REQUEST, | |
detail={"error": "Please provide start_date and end_date"}, | |
) | |
start_date_obj = datetime.strptime(start_date, "%Y-%m-%d") | |
end_date_obj = datetime.strptime(end_date, "%Y-%m-%d") | |
from litellm.proxy.proxy_server import prisma_client | |
try: | |
if prisma_client is None: | |
raise Exception( | |
"Database not connected. Connect a database to your proxy - https://docs.litellm.ai/docs/simple_proxy#managing-auth---virtual-keys" | |
) | |
if ( | |
user_api_key_dict.user_role == LitellmUserRoles.INTERNAL_USER | |
or user_api_key_dict.user_role == LitellmUserRoles.INTERNAL_USER_VIEW_ONLY | |
): | |
db_response = await get_global_activity_model_internal_user( | |
user_api_key_dict, start_date_obj, end_date_obj | |
) | |
else: | |
sql_query = """ | |
SELECT | |
model_group, | |
date_trunc('day', "startTime") AS date, | |
COUNT(*) AS api_requests, | |
SUM(total_tokens) AS total_tokens | |
FROM "LiteLLM_SpendLogs" | |
WHERE "startTime" BETWEEN $1::date AND $2::date + interval '1 day' | |
GROUP BY model_group, date_trunc('day', "startTime") | |
""" | |
db_response = await prisma_client.db.query_raw( | |
sql_query, start_date_obj, end_date_obj | |
) | |
if db_response is None: | |
return [] | |
model_ui_data: dict = ( | |
{} | |
) # {"gpt-4": {"daily_data": [], "sum_api_requests": 0, "sum_total_tokens": 0}} | |
for row in db_response: | |
_model = row["model_group"] | |
if _model not in model_ui_data: | |
model_ui_data[_model] = { | |
"daily_data": [], | |
"sum_api_requests": 0, | |
"sum_total_tokens": 0, | |
} | |
_date_obj = datetime.fromisoformat(row["date"]) | |
row["date"] = _date_obj.strftime("%b %d") | |
model_ui_data[_model]["daily_data"].append(row) | |
model_ui_data[_model]["sum_api_requests"] += row.get("api_requests", 0) | |
model_ui_data[_model]["sum_total_tokens"] += row.get("total_tokens", 0) | |
# sort mode ui data by sum_api_requests -> get top 10 models | |
model_ui_data = dict( | |
sorted( | |
model_ui_data.items(), | |
key=lambda x: x[1]["sum_api_requests"], | |
reverse=True, | |
)[:10] | |
) | |
response = [] | |
for model, data in model_ui_data.items(): | |
_sort_daily_data = sorted(data["daily_data"], key=lambda x: x["date"]) | |
response.append( | |
{ | |
"model": model, | |
"daily_data": _sort_daily_data, | |
"sum_api_requests": data["sum_api_requests"], | |
"sum_total_tokens": data["sum_total_tokens"], | |
} | |
) | |
return response | |
except Exception as e: | |
raise HTTPException( | |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
detail={"error": str(e)}, | |
) | |
async def get_global_activity_exceptions_per_deployment( | |
model_group: str = fastapi.Query( | |
description="Filter by model group", | |
), | |
start_date: Optional[str] = fastapi.Query( | |
default=None, | |
description="Time from which to start viewing spend", | |
), | |
end_date: Optional[str] = fastapi.Query( | |
default=None, | |
description="Time till which to view spend", | |
), | |
): | |
""" | |
Get number of 429 errors - Grouped by deployment | |
[ | |
{ | |
"deployment": "https://azure-us-east-1.openai.azure.com/", | |
"daily_data": [ | |
const chartdata = [ | |
{ | |
date: 'Jan 22', | |
num_rate_limit_exceptions: 10 | |
}, | |
{ | |
date: 'Jan 23', | |
num_rate_limit_exceptions: 12 | |
}, | |
], | |
"sum_num_rate_limit_exceptions": 20, | |
}, | |
{ | |
"deployment": "https://azure-us-east-1.openai.azure.com/", | |
"daily_data": [ | |
const chartdata = [ | |
{ | |
date: 'Jan 22', | |
num_rate_limit_exceptions: 10, | |
}, | |
{ | |
date: 'Jan 23', | |
num_rate_limit_exceptions: 12 | |
}, | |
], | |
"sum_num_rate_limit_exceptions": 20, | |
}, | |
] | |
""" | |
if start_date is None or end_date is None: | |
raise HTTPException( | |
status_code=status.HTTP_400_BAD_REQUEST, | |
detail={"error": "Please provide start_date and end_date"}, | |
) | |
start_date_obj = datetime.strptime(start_date, "%Y-%m-%d") | |
end_date_obj = datetime.strptime(end_date, "%Y-%m-%d") | |
from litellm.proxy.proxy_server import prisma_client | |
try: | |
if prisma_client is None: | |
raise Exception( | |
"Database not connected. Connect a database to your proxy - https://docs.litellm.ai/docs/simple_proxy#managing-auth---virtual-keys" | |
) | |
sql_query = """ | |
SELECT | |
api_base, | |
date_trunc('day', "startTime")::date AS date, | |
COUNT(*) AS num_rate_limit_exceptions | |
FROM | |
"LiteLLM_ErrorLogs" | |
WHERE | |
"startTime" >= $1::date | |
AND "startTime" < ($2::date + INTERVAL '1 day') | |
AND model_group = $3 | |
AND status_code = '429' | |
GROUP BY | |
api_base, | |
date_trunc('day', "startTime") | |
ORDER BY | |
date; | |
""" | |
db_response = await prisma_client.db.query_raw( | |
sql_query, start_date_obj, end_date_obj, model_group | |
) | |
if db_response is None: | |
return [] | |
model_ui_data: dict = ( | |
{} | |
) # {"gpt-4": {"daily_data": [], "sum_api_requests": 0, "sum_total_tokens": 0}} | |
for row in db_response: | |
_model = row["api_base"] | |
if _model not in model_ui_data: | |
model_ui_data[_model] = { | |
"daily_data": [], | |
"sum_num_rate_limit_exceptions": 0, | |
} | |
_date_obj = datetime.fromisoformat(row["date"]) | |
row["date"] = _date_obj.strftime("%b %d") | |
model_ui_data[_model]["daily_data"].append(row) | |
model_ui_data[_model]["sum_num_rate_limit_exceptions"] += row.get( | |
"num_rate_limit_exceptions", 0 | |
) | |
# sort mode ui data by sum_api_requests -> get top 10 models | |
model_ui_data = dict( | |
sorted( | |
model_ui_data.items(), | |
key=lambda x: x[1]["sum_num_rate_limit_exceptions"], | |
reverse=True, | |
)[:10] | |
) | |
response = [] | |
for model, data in model_ui_data.items(): | |
_sort_daily_data = sorted(data["daily_data"], key=lambda x: x["date"]) | |
response.append( | |
{ | |
"api_base": model, | |
"daily_data": _sort_daily_data, | |
"sum_num_rate_limit_exceptions": data[ | |
"sum_num_rate_limit_exceptions" | |
], | |
} | |
) | |
return response | |
except Exception as e: | |
raise HTTPException( | |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
detail={"error": str(e)}, | |
) | |
async def get_global_activity_exceptions( | |
model_group: str = fastapi.Query( | |
description="Filter by model group", | |
), | |
start_date: Optional[str] = fastapi.Query( | |
default=None, | |
description="Time from which to start viewing spend", | |
), | |
end_date: Optional[str] = fastapi.Query( | |
default=None, | |
description="Time till which to view spend", | |
), | |
): | |
""" | |
Get number of API Requests, total tokens through proxy | |
{ | |
"daily_data": [ | |
const chartdata = [ | |
{ | |
date: 'Jan 22', | |
num_rate_limit_exceptions: 10, | |
}, | |
{ | |
date: 'Jan 23', | |
num_rate_limit_exceptions: 10, | |
}, | |
], | |
"sum_api_exceptions": 20, | |
} | |
""" | |
if start_date is None or end_date is None: | |
raise HTTPException( | |
status_code=status.HTTP_400_BAD_REQUEST, | |
detail={"error": "Please provide start_date and end_date"}, | |
) | |
start_date_obj = datetime.strptime(start_date, "%Y-%m-%d") | |
end_date_obj = datetime.strptime(end_date, "%Y-%m-%d") | |
from litellm.proxy.proxy_server import prisma_client | |
try: | |
if prisma_client is None: | |
raise Exception( | |
"Database not connected. Connect a database to your proxy - https://docs.litellm.ai/docs/simple_proxy#managing-auth---virtual-keys" | |
) | |
sql_query = """ | |
SELECT | |
date_trunc('day', "startTime")::date AS date, | |
COUNT(*) AS num_rate_limit_exceptions | |
FROM | |
"LiteLLM_ErrorLogs" | |
WHERE | |
"startTime" >= $1::date | |
AND "startTime" < ($2::date + INTERVAL '1 day') | |
AND model_group = $3 | |
AND status_code = '429' | |
GROUP BY | |
date_trunc('day', "startTime") | |
ORDER BY | |
date; | |
""" | |
db_response = await prisma_client.db.query_raw( | |
sql_query, start_date_obj, end_date_obj, model_group | |
) | |
if db_response is None: | |
return [] | |
sum_num_rate_limit_exceptions = 0 | |
daily_data = [] | |
for row in db_response: | |
# cast date to datetime | |
_date_obj = datetime.fromisoformat(row["date"]) | |
row["date"] = _date_obj.strftime("%b %d") | |
daily_data.append(row) | |
sum_num_rate_limit_exceptions += row.get("num_rate_limit_exceptions", 0) | |
# sort daily_data by date | |
daily_data = sorted(daily_data, key=lambda x: x["date"]) | |
data_to_return = { | |
"daily_data": daily_data, | |
"sum_num_rate_limit_exceptions": sum_num_rate_limit_exceptions, | |
} | |
return data_to_return | |
except Exception as e: | |
raise HTTPException( | |
status_code=status.HTTP_400_BAD_REQUEST, | |
detail={"error": str(e)}, | |
) | |
async def get_global_spend_provider( | |
start_date: Optional[str] = fastapi.Query( | |
default=None, | |
description="Time from which to start viewing spend", | |
), | |
end_date: Optional[str] = fastapi.Query( | |
default=None, | |
description="Time till which to view spend", | |
), | |
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), | |
): | |
""" | |
Get breakdown of spend per provider | |
[ | |
{ | |
"provider": "Azure OpenAI", | |
"spend": 20 | |
}, | |
{ | |
"provider": "OpenAI", | |
"spend": 10 | |
}, | |
{ | |
"provider": "VertexAI", | |
"spend": 30 | |
} | |
] | |
""" | |
from collections import defaultdict | |
if start_date is None or end_date is None: | |
raise HTTPException( | |
status_code=status.HTTP_400_BAD_REQUEST, | |
detail={"error": "Please provide start_date and end_date"}, | |
) | |
start_date_obj = datetime.strptime(start_date, "%Y-%m-%d") | |
end_date_obj = datetime.strptime(end_date, "%Y-%m-%d") | |
from litellm.proxy.proxy_server import llm_router, prisma_client | |
try: | |
if prisma_client is None: | |
raise Exception( | |
"Database not connected. Connect a database to your proxy - https://docs.litellm.ai/docs/simple_proxy#managing-auth---virtual-keys" | |
) | |
if ( | |
user_api_key_dict.user_role == LitellmUserRoles.INTERNAL_USER | |
or user_api_key_dict.user_role == LitellmUserRoles.INTERNAL_USER_VIEW_ONLY | |
): | |
user_id = user_api_key_dict.user_id | |
if user_id is None: | |
raise HTTPException( | |
status_code=400, detail={"error": "No user_id found"} | |
) | |
sql_query = """ | |
SELECT | |
model_id, | |
SUM(spend) AS spend | |
FROM "LiteLLM_SpendLogs" | |
WHERE "startTime" BETWEEN $1::date AND $2::date | |
AND length(model_id) > 0 | |
AND "user" = $3 | |
GROUP BY model_id | |
""" | |
db_response = await prisma_client.db.query_raw( | |
sql_query, start_date_obj, end_date_obj, user_id | |
) | |
else: | |
sql_query = """ | |
SELECT | |
model_id, | |
SUM(spend) AS spend | |
FROM "LiteLLM_SpendLogs" | |
WHERE "startTime" BETWEEN $1::date AND $2::date AND length(model_id) > 0 | |
GROUP BY model_id | |
""" | |
db_response = await prisma_client.db.query_raw( | |
sql_query, start_date_obj, end_date_obj | |
) | |
if db_response is None: | |
return [] | |
################################### | |
# Convert model_id -> to Provider # | |
################################### | |
# we use the in memory router for this | |
ui_response = [] | |
provider_spend_mapping: defaultdict = defaultdict(int) | |
for row in db_response: | |
_model_id = row["model_id"] | |
_provider = "Unknown" | |
if llm_router is not None: | |
_deployment = llm_router.get_deployment(model_id=_model_id) | |
if _deployment is not None: | |
try: | |
_, _provider, _, _ = litellm.get_llm_provider( | |
model=_deployment.litellm_params.model, | |
custom_llm_provider=_deployment.litellm_params.custom_llm_provider, | |
api_base=_deployment.litellm_params.api_base, | |
litellm_params=_deployment.litellm_params, | |
) | |
provider_spend_mapping[_provider] += row["spend"] | |
except Exception: | |
pass | |
for provider, spend in provider_spend_mapping.items(): | |
ui_response.append({"provider": provider, "spend": spend}) | |
return ui_response | |
except Exception as e: | |
raise HTTPException( | |
status_code=status.HTTP_400_BAD_REQUEST, | |
detail={"error": str(e)}, | |
) | |
async def get_global_spend_report( | |
start_date: Optional[str] = fastapi.Query( | |
default=None, | |
description="Time from which to start viewing spend", | |
), | |
end_date: Optional[str] = fastapi.Query( | |
default=None, | |
description="Time till which to view spend", | |
), | |
group_by: Optional[Literal["team", "customer", "api_key"]] = fastapi.Query( | |
default="team", | |
description="Group spend by internal team or customer or api_key", | |
), | |
api_key: Optional[str] = fastapi.Query( | |
default=None, | |
description="View spend for a specific api_key. Example api_key='sk-1234", | |
), | |
internal_user_id: Optional[str] = fastapi.Query( | |
default=None, | |
description="View spend for a specific internal_user_id. Example internal_user_id='1234", | |
), | |
team_id: Optional[str] = fastapi.Query( | |
default=None, | |
description="View spend for a specific team_id. Example team_id='1234", | |
), | |
customer_id: Optional[str] = fastapi.Query( | |
default=None, | |
description="View spend for a specific customer_id. Example customer_id='1234. Can be used in conjunction with team_id as well.", | |
), | |
): | |
""" | |
Get Daily Spend per Team, based on specific startTime and endTime. Per team, view usage by each key, model | |
[ | |
{ | |
"group-by-day": "2024-05-10", | |
"teams": [ | |
{ | |
"team_name": "team-1" | |
"spend": 10, | |
"keys": [ | |
"key": "1213", | |
"usage": { | |
"model-1": { | |
"cost": 12.50, | |
"input_tokens": 1000, | |
"output_tokens": 5000, | |
"requests": 100 | |
}, | |
"audio-modelname1": { | |
"cost": 25.50, | |
"seconds": 25, | |
"requests": 50 | |
}, | |
} | |
} | |
] | |
] | |
} | |
""" | |
if start_date is None or end_date is None: | |
raise HTTPException( | |
status_code=status.HTTP_400_BAD_REQUEST, | |
detail={"error": "Please provide start_date and end_date"}, | |
) | |
start_date_obj = datetime.strptime(start_date, "%Y-%m-%d") | |
end_date_obj = datetime.strptime(end_date, "%Y-%m-%d") | |
from litellm.proxy.proxy_server import premium_user, prisma_client | |
try: | |
if prisma_client is None: | |
raise Exception( | |
"Database not connected. Connect a database to your proxy - https://docs.litellm.ai/docs/simple_proxy#managing-auth---virtual-keys" | |
) | |
if premium_user is not True: | |
verbose_proxy_logger.debug("accessing /spend/report but not a premium user") | |
raise ValueError( | |
"/spend/report endpoint " + CommonProxyErrors.not_premium_user.value | |
) | |
if api_key is not None: | |
verbose_proxy_logger.debug("Getting /spend for api_key: %s", api_key) | |
if api_key.startswith("sk-"): | |
api_key = hash_token(token=api_key) | |
sql_query = """ | |
WITH SpendByModelApiKey AS ( | |
SELECT | |
sl.api_key, | |
sl.model, | |
SUM(sl.spend) AS model_cost, | |
SUM(sl.prompt_tokens) AS model_input_tokens, | |
SUM(sl.completion_tokens) AS model_output_tokens | |
FROM | |
"LiteLLM_SpendLogs" sl | |
WHERE | |
sl."startTime" BETWEEN $1::date AND $2::date AND sl.api_key = $3 | |
GROUP BY | |
sl.api_key, | |
sl.model | |
) | |
SELECT | |
api_key, | |
SUM(model_cost) AS total_cost, | |
SUM(model_input_tokens) AS total_input_tokens, | |
SUM(model_output_tokens) AS total_output_tokens, | |
jsonb_agg(jsonb_build_object( | |
'model', model, | |
'total_cost', model_cost, | |
'total_input_tokens', model_input_tokens, | |
'total_output_tokens', model_output_tokens | |
)) AS model_details | |
FROM | |
SpendByModelApiKey | |
GROUP BY | |
api_key | |
ORDER BY | |
total_cost DESC; | |
""" | |
db_response = await prisma_client.db.query_raw( | |
sql_query, start_date_obj, end_date_obj, api_key | |
) | |
if db_response is None: | |
return [] | |
return db_response | |
elif internal_user_id is not None: | |
verbose_proxy_logger.debug( | |
"Getting /spend for internal_user_id: %s", internal_user_id | |
) | |
sql_query = """ | |
WITH SpendByModelApiKey AS ( | |
SELECT | |
sl.api_key, | |
sl.model, | |
SUM(sl.spend) AS model_cost, | |
SUM(sl.prompt_tokens) AS model_input_tokens, | |
SUM(sl.completion_tokens) AS model_output_tokens | |
FROM | |
"LiteLLM_SpendLogs" sl | |
WHERE | |
sl."startTime" BETWEEN $1::date AND $2::date AND sl.user = $3 | |
GROUP BY | |
sl.api_key, | |
sl.model | |
) | |
SELECT | |
api_key, | |
SUM(model_cost) AS total_cost, | |
SUM(model_input_tokens) AS total_input_tokens, | |
SUM(model_output_tokens) AS total_output_tokens, | |
jsonb_agg(jsonb_build_object( | |
'model', model, | |
'total_cost', model_cost, | |
'total_input_tokens', model_input_tokens, | |
'total_output_tokens', model_output_tokens | |
)) AS model_details | |
FROM | |
SpendByModelApiKey | |
GROUP BY | |
api_key | |
ORDER BY | |
total_cost DESC; | |
""" | |
db_response = await prisma_client.db.query_raw( | |
sql_query, start_date_obj, end_date_obj, internal_user_id | |
) | |
if db_response is None: | |
return [] | |
return db_response | |
elif team_id is not None and customer_id is not None: | |
return await get_spend_by_team_and_customer( | |
start_date_obj, end_date_obj, team_id, customer_id, prisma_client | |
) | |
if group_by == "team": | |
# first get data from spend logs -> SpendByModelApiKey | |
# then read data from "SpendByModelApiKey" to format the response obj | |
sql_query = """ | |
WITH SpendByModelApiKey AS ( | |
SELECT | |
date_trunc('day', sl."startTime") AS group_by_day, | |
COALESCE(tt.team_alias, 'Unassigned Team') AS team_name, | |
sl.model, | |
sl.api_key, | |
SUM(sl.spend) AS model_api_spend, | |
SUM(sl.total_tokens) AS model_api_tokens | |
FROM | |
"LiteLLM_SpendLogs" sl | |
LEFT JOIN | |
"LiteLLM_TeamTable" tt | |
ON | |
sl.team_id = tt.team_id | |
WHERE | |
sl."startTime" BETWEEN $1::date AND $2::date | |
GROUP BY | |
date_trunc('day', sl."startTime"), | |
tt.team_alias, | |
sl.model, | |
sl.api_key | |
) | |
SELECT | |
group_by_day, | |
jsonb_agg(jsonb_build_object( | |
'team_name', team_name, | |
'total_spend', total_spend, | |
'metadata', metadata | |
)) AS teams | |
FROM ( | |
SELECT | |
group_by_day, | |
team_name, | |
SUM(model_api_spend) AS total_spend, | |
jsonb_agg(jsonb_build_object( | |
'model', model, | |
'api_key', api_key, | |
'spend', model_api_spend, | |
'total_tokens', model_api_tokens | |
)) AS metadata | |
FROM | |
SpendByModelApiKey | |
GROUP BY | |
group_by_day, | |
team_name | |
) AS aggregated | |
GROUP BY | |
group_by_day | |
ORDER BY | |
group_by_day; | |
""" | |
db_response = await prisma_client.db.query_raw( | |
sql_query, start_date_obj, end_date_obj | |
) | |
if db_response is None: | |
return [] | |
return db_response | |
elif group_by == "customer": | |
sql_query = """ | |
WITH SpendByModelApiKey AS ( | |
SELECT | |
date_trunc('day', sl."startTime") AS group_by_day, | |
sl.end_user AS customer, | |
sl.model, | |
sl.api_key, | |
SUM(sl.spend) AS model_api_spend, | |
SUM(sl.total_tokens) AS model_api_tokens | |
FROM | |
"LiteLLM_SpendLogs" sl | |
WHERE | |
sl."startTime" BETWEEN $1::date AND $2::date | |
GROUP BY | |
date_trunc('day', sl."startTime"), | |
customer, | |
sl.model, | |
sl.api_key | |
) | |
SELECT | |
group_by_day, | |
jsonb_agg(jsonb_build_object( | |
'customer', customer, | |
'total_spend', total_spend, | |
'metadata', metadata | |
)) AS customers | |
FROM | |
( | |
SELECT | |
group_by_day, | |
customer, | |
SUM(model_api_spend) AS total_spend, | |
jsonb_agg(jsonb_build_object( | |
'model', model, | |
'api_key', api_key, | |
'spend', model_api_spend, | |
'total_tokens', model_api_tokens | |
)) AS metadata | |
FROM | |
SpendByModelApiKey | |
GROUP BY | |
group_by_day, | |
customer | |
) AS aggregated | |
GROUP BY | |
group_by_day | |
ORDER BY | |
group_by_day; | |
""" | |
db_response = await prisma_client.db.query_raw( | |
sql_query, start_date_obj, end_date_obj | |
) | |
if db_response is None: | |
return [] | |
return db_response | |
elif group_by == "api_key": | |
sql_query = """ | |
WITH SpendByModelApiKey AS ( | |
SELECT | |
sl.api_key, | |
sl.model, | |
SUM(sl.spend) AS model_cost, | |
SUM(sl.prompt_tokens) AS model_input_tokens, | |
SUM(sl.completion_tokens) AS model_output_tokens | |
FROM | |
"LiteLLM_SpendLogs" sl | |
WHERE | |
sl."startTime" BETWEEN $1::date AND $2::date | |
GROUP BY | |
sl.api_key, | |
sl.model | |
) | |
SELECT | |
api_key, | |
SUM(model_cost) AS total_cost, | |
SUM(model_input_tokens) AS total_input_tokens, | |
SUM(model_output_tokens) AS total_output_tokens, | |
jsonb_agg(jsonb_build_object( | |
'model', model, | |
'total_cost', model_cost, | |
'total_input_tokens', model_input_tokens, | |
'total_output_tokens', model_output_tokens | |
)) AS model_details | |
FROM | |
SpendByModelApiKey | |
GROUP BY | |
api_key | |
ORDER BY | |
total_cost DESC; | |
""" | |
db_response = await prisma_client.db.query_raw( | |
sql_query, start_date_obj, end_date_obj | |
) | |
if db_response is None: | |
return [] | |
return db_response | |
except Exception as e: | |
raise HTTPException( | |
status_code=status.HTTP_400_BAD_REQUEST, | |
detail={"error": str(e)}, | |
) | |
async def global_get_all_tag_names(): | |
try: | |
from litellm.proxy.proxy_server import prisma_client | |
if prisma_client is None: | |
raise Exception( | |
"Database not connected. Connect a database to your proxy - https://docs.litellm.ai/docs/simple_proxy#managing-auth---virtual-keys" | |
) | |
sql_query = """ | |
SELECT DISTINCT | |
jsonb_array_elements_text(request_tags) AS individual_request_tag | |
FROM "LiteLLM_SpendLogs"; | |
""" | |
db_response = await prisma_client.db.query_raw(sql_query) | |
if db_response is None: | |
return [] | |
_tag_names = [] | |
for row in db_response: | |
_tag_names.append(row.get("individual_request_tag")) | |
return {"tag_names": _tag_names} | |
except Exception as e: | |
if isinstance(e, HTTPException): | |
raise ProxyException( | |
message=getattr(e, "detail", f"/spend/all_tag_names Error({str(e)})"), | |
type="internal_error", | |
param=getattr(e, "param", "None"), | |
code=getattr(e, "status_code", status.HTTP_500_INTERNAL_SERVER_ERROR), | |
) | |
elif isinstance(e, ProxyException): | |
raise e | |
raise ProxyException( | |
message="/spend/all_tag_names Error" + str(e), | |
type="internal_error", | |
param=getattr(e, "param", "None"), | |
code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
) | |
async def global_view_spend_tags( | |
start_date: Optional[str] = fastapi.Query( | |
default=None, | |
description="Time from which to start viewing key spend", | |
), | |
end_date: Optional[str] = fastapi.Query( | |
default=None, | |
description="Time till which to view key spend", | |
), | |
tags: Optional[str] = fastapi.Query( | |
default=None, | |
description="comman separated tags to filter on", | |
), | |
): | |
""" | |
LiteLLM Enterprise - View Spend Per Request Tag. Used by LiteLLM UI | |
Example Request: | |
``` | |
curl -X GET "http://0.0.0.0:4000/spend/tags" \ | |
-H "Authorization: Bearer sk-1234" | |
``` | |
Spend with Start Date and End Date | |
``` | |
curl -X GET "http://0.0.0.0:4000/spend/tags?start_date=2022-01-01&end_date=2022-02-01" \ | |
-H "Authorization: Bearer sk-1234" | |
``` | |
""" | |
import traceback | |
from litellm.proxy.proxy_server import prisma_client | |
try: | |
if prisma_client is None: | |
raise Exception( | |
"Database not connected. Connect a database to your proxy - https://docs.litellm.ai/docs/simple_proxy#managing-auth---virtual-keys" | |
) | |
if end_date is None or start_date is None: | |
raise ProxyException( | |
message="Please provide start_date and end_date", | |
type="bad_request", | |
param=None, | |
code=status.HTTP_400_BAD_REQUEST, | |
) | |
response = await ui_get_spend_by_tags( | |
start_date=start_date, | |
end_date=end_date, | |
tags_str=tags, | |
prisma_client=prisma_client, | |
) | |
return response | |
except Exception as e: | |
error_trace = traceback.format_exc() | |
error_str = str(e) + "\n" + error_trace | |
if isinstance(e, HTTPException): | |
raise ProxyException( | |
message=getattr(e, "detail", f"/spend/tags Error({error_str})"), | |
type="internal_error", | |
param=getattr(e, "param", "None"), | |
code=getattr(e, "status_code", status.HTTP_500_INTERNAL_SERVER_ERROR), | |
) | |
elif isinstance(e, ProxyException): | |
raise e | |
raise ProxyException( | |
message="/spend/tags Error" + error_str, | |
type="internal_error", | |
param=getattr(e, "param", "None"), | |
code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
) | |
async def _get_spend_report_for_time_range( | |
start_date: str, | |
end_date: str, | |
): | |
from litellm.proxy.proxy_server import prisma_client | |
if prisma_client is None: | |
verbose_proxy_logger.error( | |
"Database not connected. Connect a database to your proxy for weekly, monthly spend reports" | |
) | |
return None | |
try: | |
sql_query = """ | |
SELECT | |
t.team_alias, | |
SUM(s.spend) AS total_spend | |
FROM | |
"LiteLLM_SpendLogs" s | |
LEFT JOIN | |
"LiteLLM_TeamTable" t ON s.team_id = t.team_id | |
WHERE | |
s."startTime"::DATE >= $1::date AND s."startTime"::DATE <= $2::date | |
GROUP BY | |
t.team_alias | |
ORDER BY | |
total_spend DESC; | |
""" | |
response = await prisma_client.db.query_raw(sql_query, start_date, end_date) | |
# get spend per tag for today | |
sql_query = """ | |
SELECT | |
jsonb_array_elements_text(request_tags) AS individual_request_tag, | |
SUM(spend) AS total_spend | |
FROM "LiteLLM_SpendLogs" | |
WHERE "startTime"::DATE >= $1::date AND "startTime"::DATE <= $2::date | |
GROUP BY individual_request_tag | |
ORDER BY total_spend DESC; | |
""" | |
spend_per_tag = await prisma_client.db.query_raw( | |
sql_query, start_date, end_date | |
) | |
return response, spend_per_tag | |
except Exception as e: | |
verbose_proxy_logger.error( | |
"Exception in _get_daily_spend_reports {}".format(str(e)) | |
) | |
async def calculate_spend(request: SpendCalculateRequest): | |
""" | |
Accepts all the params of completion_cost. | |
Calculate spend **before** making call: | |
Note: If you see a spend of $0.0 you need to set custom_pricing for your model: https://docs.litellm.ai/docs/proxy/custom_pricing | |
``` | |
curl --location 'http://localhost:4000/spend/calculate' | |
--header 'Authorization: Bearer sk-1234' | |
--header 'Content-Type: application/json' | |
--data '{ | |
"model": "anthropic.claude-v2", | |
"messages": [{"role": "user", "content": "Hey, how'''s it going?"}] | |
}' | |
``` | |
Calculate spend **after** making call: | |
``` | |
curl --location 'http://localhost:4000/spend/calculate' | |
--header 'Authorization: Bearer sk-1234' | |
--header 'Content-Type: application/json' | |
--data '{ | |
"completion_response": { | |
"id": "chatcmpl-123", | |
"object": "chat.completion", | |
"created": 1677652288, | |
"model": "gpt-3.5-turbo-0125", | |
"system_fingerprint": "fp_44709d6fcb", | |
"choices": [{ | |
"index": 0, | |
"message": { | |
"role": "assistant", | |
"content": "Hello there, how may I assist you today?" | |
}, | |
"logprobs": null, | |
"finish_reason": "stop" | |
}] | |
"usage": { | |
"prompt_tokens": 9, | |
"completion_tokens": 12, | |
"total_tokens": 21 | |
} | |
} | |
}' | |
``` | |
""" | |
try: | |
from litellm import completion_cost | |
from litellm.cost_calculator import CostPerToken | |
from litellm.proxy.proxy_server import llm_router | |
_cost = None | |
if request.model is not None: | |
if request.messages is None: | |
raise HTTPException( | |
status_code=400, | |
detail="Bad Request - messages must be provided if 'model' is provided", | |
) | |
# check if model in llm_router | |
_model_in_llm_router = None | |
cost_per_token: Optional[CostPerToken] = None | |
if llm_router is not None: | |
if ( | |
llm_router.model_group_alias is not None | |
and request.model in llm_router.model_group_alias | |
): | |
# lookup alias in llm_router | |
_model_group_name = llm_router.model_group_alias[request.model] | |
for model in llm_router.model_list: | |
if model.get("model_name") == _model_group_name: | |
_model_in_llm_router = model | |
else: | |
# no model_group aliases set -> try finding model in llm_router | |
# find model in llm_router | |
for model in llm_router.model_list: | |
if model.get("model_name") == request.model: | |
_model_in_llm_router = model | |
""" | |
3 cases for /spend/calculate | |
1. user passes model, and model is defined on litellm config.yaml or in DB. use info on config or in DB in this case | |
2. user passes model, and model is not defined on litellm config.yaml or in DB. Pass model as is to litellm.completion_cost | |
3. user passes completion_response | |
""" | |
if _model_in_llm_router is not None: | |
_litellm_params = _model_in_llm_router.get("litellm_params") | |
_litellm_model_name = _litellm_params.get("model") | |
input_cost_per_token = _litellm_params.get("input_cost_per_token") | |
output_cost_per_token = _litellm_params.get("output_cost_per_token") | |
if ( | |
input_cost_per_token is not None | |
or output_cost_per_token is not None | |
): | |
cost_per_token = CostPerToken( | |
input_cost_per_token=input_cost_per_token, | |
output_cost_per_token=output_cost_per_token, | |
) | |
_cost = completion_cost( | |
model=_litellm_model_name, | |
messages=request.messages, | |
custom_cost_per_token=cost_per_token, | |
) | |
else: | |
_cost = completion_cost(model=request.model, messages=request.messages) | |
elif request.completion_response is not None: | |
_completion_response = litellm.ModelResponse(**request.completion_response) | |
_cost = completion_cost(completion_response=_completion_response) | |
else: | |
raise HTTPException( | |
status_code=400, | |
detail="Bad Request - Either 'model' or 'completion_response' must be provided", | |
) | |
return {"cost": _cost} | |
except Exception as e: | |
if isinstance(e, HTTPException): | |
raise ProxyException( | |
message=getattr(e, "detail", str(e)), | |
type=getattr(e, "type", "None"), | |
param=getattr(e, "param", "None"), | |
code=getattr(e, "status_code", status.HTTP_400_BAD_REQUEST), | |
) | |
error_msg = f"{str(e)}" | |
raise ProxyException( | |
message=getattr(e, "message", error_msg), | |
type=getattr(e, "type", "None"), | |
param=getattr(e, "param", "None"), | |
code=getattr(e, "status_code", 500), | |
) | |
async def ui_view_spend_logs( # noqa: PLR0915 | |
api_key: Optional[str] = fastapi.Query( | |
default=None, | |
description="Get spend logs based on api key", | |
), | |
user_id: Optional[str] = fastapi.Query( | |
default=None, | |
description="Get spend logs based on user_id", | |
), | |
request_id: Optional[str] = fastapi.Query( | |
default=None, | |
description="request_id to get spend logs for specific request_id", | |
), | |
team_id: Optional[str] = fastapi.Query( | |
default=None, | |
description="Filter spend logs by team_id", | |
), | |
min_spend: Optional[float] = fastapi.Query( | |
default=None, | |
description="Filter logs with spend greater than or equal to this value", | |
), | |
max_spend: Optional[float] = fastapi.Query( | |
default=None, | |
description="Filter logs with spend less than or equal to this value", | |
), | |
start_date: Optional[str] = fastapi.Query( | |
default=None, | |
description="Time from which to start viewing key spend", | |
), | |
end_date: Optional[str] = fastapi.Query( | |
default=None, | |
description="Time till which to view key spend", | |
), | |
page: int = fastapi.Query( | |
default=1, description="Page number for pagination", ge=1 | |
), | |
page_size: int = fastapi.Query( | |
default=50, description="Number of items per page", ge=1, le=100 | |
), | |
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), | |
): | |
""" | |
View spend logs for UI with pagination support | |
Returns: | |
{ | |
"data": List[LiteLLM_SpendLogs], # Paginated spend logs | |
"total": int, # Total number of records | |
"page": int, # Current page number | |
"page_size": int, # Number of items per page | |
"total_pages": int # Total number of pages | |
} | |
""" | |
from litellm.proxy.proxy_server import prisma_client | |
if prisma_client is None: | |
raise ProxyException( | |
message="Prisma Client is not initialized", | |
type="internal_error", | |
param="None", | |
code=status.HTTP_401_UNAUTHORIZED, | |
) | |
if start_date is None or end_date is None: | |
raise ProxyException( | |
message="Start date and end date are required", | |
type="bad_request", | |
param="None", | |
code=status.HTTP_400_BAD_REQUEST, | |
) | |
try: | |
# Convert the date strings to datetime objects | |
start_date_obj = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S").replace( | |
tzinfo=timezone.utc | |
) | |
end_date_obj = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S").replace( | |
tzinfo=timezone.utc | |
) | |
# Convert to ISO format strings for Prisma | |
start_date_iso = start_date_obj.isoformat() # Already in UTC, no need to add Z | |
end_date_iso = end_date_obj.isoformat() # Already in UTC, no need to add Z | |
# Build where conditions | |
where_conditions: dict[str, Any] = { | |
"startTime": {"gte": start_date_iso, "lte": end_date_iso}, | |
} | |
if team_id is not None: | |
where_conditions["team_id"] = team_id | |
if api_key is not None: | |
where_conditions["api_key"] = api_key | |
if user_id is not None: | |
where_conditions["user"] = user_id | |
if request_id is not None: | |
where_conditions["request_id"] = request_id | |
if min_spend is not None or max_spend is not None: | |
where_conditions["spend"] = {} | |
if min_spend is not None: | |
where_conditions["spend"]["gte"] = min_spend | |
if max_spend is not None: | |
where_conditions["spend"]["lte"] = max_spend | |
# Calculate skip value for pagination | |
skip = (page - 1) * page_size | |
# Get total count of records | |
total_records = await prisma_client.db.litellm_spendlogs.count( | |
where=where_conditions, | |
) | |
# Get paginated data | |
data = await prisma_client.db.litellm_spendlogs.find_many( | |
where=where_conditions, | |
order={ | |
"startTime": "desc", | |
}, | |
skip=skip, | |
take=page_size, | |
) | |
# Calculate total pages | |
total_pages = (total_records + page_size - 1) // page_size | |
verbose_proxy_logger.debug("data= %s", json.dumps(data, indent=4, default=str)) | |
return { | |
"data": data, | |
"total": total_records, | |
"page": page, | |
"page_size": page_size, | |
"total_pages": total_pages, | |
} | |
except Exception as e: | |
verbose_proxy_logger.exception(f"Error in ui_view_spend_logs: {e}") | |
raise handle_exception_on_proxy(e) | |
async def ui_view_request_response_for_request_id( | |
request_id: str, | |
start_date: Optional[str] = fastapi.Query( | |
default=None, | |
description="Time from which to start viewing key spend", | |
), | |
end_date: Optional[str] = fastapi.Query( | |
default=None, | |
description="Time till which to view key spend", | |
), | |
): | |
""" | |
View request / response for a specific request_id | |
- goes through all callbacks, checks if any of them have a @property -> has_request_response_payload | |
- if so, it will return the request and response payload | |
""" | |
custom_loggers = ( | |
litellm.logging_callback_manager.get_active_additional_logging_utils_from_custom_logger() | |
) | |
start_date_obj: Optional[datetime] = None | |
end_date_obj: Optional[datetime] = None | |
if start_date is not None: | |
start_date_obj = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S").replace( | |
tzinfo=timezone.utc | |
) | |
if end_date is not None: | |
end_date_obj = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S").replace( | |
tzinfo=timezone.utc | |
) | |
for custom_logger in custom_loggers: | |
payload = await custom_logger.get_request_response_payload( | |
request_id=request_id, | |
start_time_utc=start_date_obj, | |
end_time_utc=end_date_obj, | |
) | |
if payload is not None: | |
return payload | |
return None | |
async def view_spend_logs( # noqa: PLR0915 | |
api_key: Optional[str] = fastapi.Query( | |
default=None, | |
description="Get spend logs based on api key", | |
), | |
user_id: Optional[str] = fastapi.Query( | |
default=None, | |
description="Get spend logs based on user_id", | |
), | |
request_id: Optional[str] = fastapi.Query( | |
default=None, | |
description="request_id to get spend logs for specific request_id. If none passed then pass spend logs for all requests", | |
), | |
start_date: Optional[str] = fastapi.Query( | |
default=None, | |
description="Time from which to start viewing key spend", | |
), | |
end_date: Optional[str] = fastapi.Query( | |
default=None, | |
description="Time till which to view key spend", | |
), | |
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), | |
): | |
""" | |
View all spend logs, if request_id is provided, only logs for that request_id will be returned | |
Example Request for all logs | |
``` | |
curl -X GET "http://0.0.0.0:8000/spend/logs" \ | |
-H "Authorization: Bearer sk-1234" | |
``` | |
Example Request for specific request_id | |
``` | |
curl -X GET "http://0.0.0.0:8000/spend/logs?request_id=chatcmpl-6dcb2540-d3d7-4e49-bb27-291f863f112e" \ | |
-H "Authorization: Bearer sk-1234" | |
``` | |
Example Request for specific api_key | |
``` | |
curl -X GET "http://0.0.0.0:8000/spend/logs?api_key=sk-Fn8Ej39NkBQmUagFEoUWPQ" \ | |
-H "Authorization: Bearer sk-1234" | |
``` | |
Example Request for specific user_id | |
``` | |
curl -X GET "http://0.0.0.0:8000/spend/[email protected]" \ | |
-H "Authorization: Bearer sk-1234" | |
``` | |
""" | |
from litellm.proxy.proxy_server import prisma_client | |
if ( | |
user_api_key_dict.user_role == LitellmUserRoles.INTERNAL_USER | |
or user_api_key_dict.user_role == LitellmUserRoles.INTERNAL_USER_VIEW_ONLY | |
): | |
user_id = user_api_key_dict.user_id | |
try: | |
verbose_proxy_logger.debug("inside view_spend_logs") | |
if prisma_client is None: | |
raise Exception( | |
"Database not connected. Connect a database to your proxy - https://docs.litellm.ai/docs/simple_proxy#managing-auth---virtual-keys" | |
) | |
spend_logs = [] | |
if ( | |
start_date is not None | |
and isinstance(start_date, str) | |
and end_date is not None | |
and isinstance(end_date, str) | |
): | |
# Convert the date strings to datetime objects | |
start_date_obj = datetime.strptime(start_date, "%Y-%m-%d") | |
end_date_obj = datetime.strptime(end_date, "%Y-%m-%d") | |
filter_query = { | |
"startTime": { | |
"gte": start_date_obj, # Greater than or equal to Start Date | |
"lte": end_date_obj, # Less than or equal to End Date | |
} | |
} | |
if api_key is not None and isinstance(api_key, str): | |
filter_query["api_key"] = api_key # type: ignore | |
elif request_id is not None and isinstance(request_id, str): | |
filter_query["request_id"] = request_id # type: ignore | |
elif user_id is not None and isinstance(user_id, str): | |
filter_query["user"] = user_id # type: ignore | |
# SQL query | |
response = await prisma_client.db.litellm_spendlogs.group_by( | |
by=["api_key", "user", "model", "startTime"], | |
where=filter_query, # type: ignore | |
sum={ | |
"spend": True, | |
}, | |
) | |
if ( | |
isinstance(response, list) | |
and len(response) > 0 | |
and isinstance(response[0], dict) | |
): | |
result: dict = {} | |
for record in response: | |
dt_object = datetime.strptime(str(record["startTime"]), "%Y-%m-%dT%H:%M:%S.%fZ") # type: ignore | |
date = dt_object.date() | |
if date not in result: | |
result[date] = {"users": {}, "models": {}} | |
api_key = record["api_key"] # type: ignore | |
user_id = record["user"] # type: ignore | |
model = record["model"] # type: ignore | |
result[date]["spend"] = result[date].get("spend", 0) + record.get( | |
"_sum", {} | |
).get("spend", 0) | |
result[date][api_key] = result[date].get(api_key, 0) + record.get( | |
"_sum", {} | |
).get("spend", 0) | |
result[date]["users"][user_id] = result[date]["users"].get( | |
user_id, 0 | |
) + record.get("_sum", {}).get("spend", 0) | |
result[date]["models"][model] = result[date]["models"].get( | |
model, 0 | |
) + record.get("_sum", {}).get("spend", 0) | |
return_list = [] | |
final_date = None | |
for k, v in sorted(result.items()): | |
return_list.append({**v, "startTime": k}) | |
final_date = k | |
end_date_date = end_date_obj.date() | |
if final_date is not None and final_date < end_date_date: | |
current_date = final_date + timedelta(days=1) | |
while current_date <= end_date_date: | |
# Represent current_date as string because original response has it this way | |
return_list.append( | |
{ | |
"startTime": current_date, | |
"spend": 0, | |
"users": {}, | |
"models": {}, | |
} | |
) # If no data, will stay as zero | |
current_date += timedelta(days=1) # Move on to the next day | |
return return_list | |
return response | |
elif api_key is not None and isinstance(api_key, str): | |
if api_key.startswith("sk-"): | |
hashed_token = prisma_client.hash_token(token=api_key) | |
else: | |
hashed_token = api_key | |
spend_log = await prisma_client.get_data( | |
table_name="spend", | |
query_type="find_all", | |
key_val={"key": "api_key", "value": hashed_token}, | |
) | |
if isinstance(spend_log, list): | |
return spend_log | |
else: | |
return [spend_log] | |
elif request_id is not None: | |
spend_log = await prisma_client.get_data( | |
table_name="spend", | |
query_type="find_unique", | |
key_val={"key": "request_id", "value": request_id}, | |
) | |
return [spend_log] | |
elif user_id is not None: | |
spend_log = await prisma_client.get_data( | |
table_name="spend", | |
query_type="find_all", | |
key_val={"key": "user", "value": user_id}, | |
) | |
if isinstance(spend_log, list): | |
return spend_log | |
else: | |
return [spend_log] | |
else: | |
spend_logs = await prisma_client.get_data( | |
table_name="spend", query_type="find_all" | |
) | |
return spend_logs | |
return None | |
except Exception as e: | |
if isinstance(e, HTTPException): | |
raise ProxyException( | |
message=getattr(e, "detail", f"/spend/logs Error({str(e)})"), | |
type="internal_error", | |
param=getattr(e, "param", "None"), | |
code=getattr(e, "status_code", status.HTTP_500_INTERNAL_SERVER_ERROR), | |
) | |
elif isinstance(e, ProxyException): | |
raise e | |
raise ProxyException( | |
message="/spend/logs Error" + str(e), | |
type="internal_error", | |
param=getattr(e, "param", "None"), | |
code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
) | |
async def global_spend_reset(): | |
""" | |
ADMIN ONLY / MASTER KEY Only Endpoint | |
Globally reset spend for All API Keys and Teams, maintain LiteLLM_SpendLogs | |
1. LiteLLM_SpendLogs will maintain the logs on spend, no data gets deleted from there | |
2. LiteLLM_VerificationTokens spend will be set = 0 | |
3. LiteLLM_TeamTable spend will be set = 0 | |
""" | |
from litellm.proxy.proxy_server import prisma_client | |
if prisma_client is None: | |
raise ProxyException( | |
message="Prisma Client is not initialized", | |
type="internal_error", | |
param="None", | |
code=status.HTTP_401_UNAUTHORIZED, | |
) | |
await prisma_client.db.litellm_verificationtoken.update_many( | |
data={"spend": 0.0}, where={} | |
) | |
await prisma_client.db.litellm_teamtable.update_many(data={"spend": 0.0}, where={}) | |
return { | |
"message": "Spend for all API Keys and Teams reset successfully", | |
"status": "success", | |
} | |
async def global_spend_refresh(): | |
""" | |
ADMIN ONLY / MASTER KEY Only Endpoint | |
Globally refresh spend MonthlyGlobalSpend view | |
""" | |
from litellm.proxy.proxy_server import prisma_client | |
if prisma_client is None: | |
raise ProxyException( | |
message="Prisma Client is not initialized", | |
type="internal_error", | |
param="None", | |
code=status.HTTP_401_UNAUTHORIZED, | |
) | |
## RESET GLOBAL SPEND VIEW ### | |
async def is_materialized_global_spend_view() -> bool: | |
""" | |
Return True if materialized view exists | |
Else False | |
""" | |
sql_query = """ | |
SELECT relname, relkind | |
FROM pg_class | |
WHERE relname = 'MonthlyGlobalSpend'; | |
""" | |
try: | |
resp = await prisma_client.db.query_raw(sql_query) | |
return resp[0]["relkind"] == "m" | |
except Exception: | |
return False | |
view_exists = await is_materialized_global_spend_view() | |
if view_exists: | |
# refresh materialized view | |
sql_query = """ | |
REFRESH MATERIALIZED VIEW "MonthlyGlobalSpend"; | |
""" | |
try: | |
from litellm.proxy._types import CommonProxyErrors | |
from litellm.proxy.proxy_server import proxy_logging_obj | |
from litellm.proxy.utils import PrismaClient | |
db_url = os.getenv("DATABASE_URL") | |
if db_url is None: | |
raise Exception(CommonProxyErrors.db_not_connected_error.value) | |
new_client = PrismaClient( | |
database_url=db_url, | |
proxy_logging_obj=proxy_logging_obj, | |
http_client={ | |
"timeout": 6000, | |
}, | |
) | |
await new_client.db.connect() | |
await new_client.db.query_raw(sql_query) | |
verbose_proxy_logger.info("MonthlyGlobalSpend view refreshed") | |
return { | |
"message": "MonthlyGlobalSpend view refreshed", | |
"status": "success", | |
} | |
except Exception as e: | |
verbose_proxy_logger.exception( | |
"Failed to refresh materialized view - {}".format(str(e)) | |
) | |
return { | |
"message": "Failed to refresh materialized view", | |
"status": "failure", | |
} | |
async def global_spend_for_internal_user( | |
api_key: Optional[str] = None, | |
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), | |
): | |
from litellm.proxy.proxy_server import prisma_client | |
if prisma_client is None: | |
raise ProxyException( | |
message="Prisma Client is not initialized", | |
type="internal_error", | |
param="None", | |
code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
) | |
try: | |
user_id = user_api_key_dict.user_id | |
if user_id is None: | |
raise ValueError("/global/spend/logs Error: User ID is None") | |
if api_key is not None: | |
sql_query = """ | |
SELECT * FROM "MonthlyGlobalSpendPerUserPerKey" | |
WHERE "api_key" = $1 AND "user" = $2 | |
ORDER BY "date"; | |
""" | |
response = await prisma_client.db.query_raw(sql_query, api_key, user_id) | |
return response | |
sql_query = """SELECT * FROM "MonthlyGlobalSpendPerUserPerKey" WHERE "user" = $1 ORDER BY "date";""" | |
response = await prisma_client.db.query_raw(sql_query, user_id) | |
return response | |
except Exception as e: | |
verbose_proxy_logger.error(f"/global/spend/logs Error: {str(e)}") | |
raise e | |
async def global_spend_logs( | |
api_key: Optional[str] = fastapi.Query( | |
default=None, | |
description="API Key to get global spend (spend per day for last 30d). Admin-only endpoint", | |
), | |
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), | |
): | |
""" | |
[BETA] This is a beta endpoint. It will change. | |
Use this to get global spend (spend per day for last 30d). Admin-only endpoint | |
More efficient implementation of /spend/logs, by creating a view over the spend logs table. | |
""" | |
import traceback | |
from litellm.integrations.prometheus_helpers.prometheus_api import ( | |
get_daily_spend_from_prometheus, | |
is_prometheus_connected, | |
) | |
from litellm.proxy.proxy_server import prisma_client | |
try: | |
if prisma_client is None: | |
raise ProxyException( | |
message="Prisma Client is not initialized", | |
type="internal_error", | |
param="None", | |
code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
) | |
if ( | |
user_api_key_dict.user_role == LitellmUserRoles.INTERNAL_USER | |
or user_api_key_dict.user_role == LitellmUserRoles.INTERNAL_USER_VIEW_ONLY | |
): | |
response = await global_spend_for_internal_user( | |
api_key=api_key, user_api_key_dict=user_api_key_dict | |
) | |
return response | |
prometheus_api_enabled = is_prometheus_connected() | |
if prometheus_api_enabled: | |
response = await get_daily_spend_from_prometheus(api_key=api_key) | |
return response | |
else: | |
if api_key is None: | |
sql_query = """SELECT * FROM "MonthlyGlobalSpend" ORDER BY "date";""" | |
response = await prisma_client.db.query_raw(query=sql_query) | |
return response | |
else: | |
sql_query = """ | |
SELECT * FROM "MonthlyGlobalSpendPerKey" | |
WHERE "api_key" = $1 | |
ORDER BY "date"; | |
""" | |
response = await prisma_client.db.query_raw(sql_query, api_key) | |
return response | |
except Exception as e: | |
error_trace = traceback.format_exc() | |
error_str = str(e) + "\n" + error_trace | |
verbose_proxy_logger.error(f"/global/spend/logs Error: {error_str}") | |
if isinstance(e, HTTPException): | |
raise ProxyException( | |
message=getattr(e, "detail", f"/global/spend/logs Error({error_str})"), | |
type="internal_error", | |
param=getattr(e, "param", "None"), | |
code=getattr(e, "status_code", status.HTTP_500_INTERNAL_SERVER_ERROR), | |
) | |
elif isinstance(e, ProxyException): | |
raise e | |
raise ProxyException( | |
message="/global/spend/logs Error" + error_str, | |
type="internal_error", | |
param=getattr(e, "param", "None"), | |
code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
) | |
async def global_spend(): | |
""" | |
[BETA] This is a beta endpoint. It will change. | |
View total spend across all proxy keys | |
""" | |
import traceback | |
from litellm.proxy.proxy_server import prisma_client | |
try: | |
total_spend = 0.0 | |
if prisma_client is None: | |
raise HTTPException(status_code=500, detail={"error": "No db connected"}) | |
sql_query = """SELECT SUM(spend) as total_spend FROM "MonthlyGlobalSpend";""" | |
response = await prisma_client.db.query_raw(query=sql_query) | |
if response is not None: | |
if isinstance(response, list) and len(response) > 0: | |
total_spend = response[0].get("total_spend", 0.0) | |
return {"spend": total_spend, "max_budget": litellm.max_budget} | |
except Exception as e: | |
error_trace = traceback.format_exc() | |
error_str = str(e) + "\n" + error_trace | |
if isinstance(e, HTTPException): | |
raise ProxyException( | |
message=getattr(e, "detail", f"/global/spend Error({error_str})"), | |
type="internal_error", | |
param=getattr(e, "param", "None"), | |
code=getattr(e, "status_code", status.HTTP_500_INTERNAL_SERVER_ERROR), | |
) | |
elif isinstance(e, ProxyException): | |
raise e | |
raise ProxyException( | |
message="/global/spend Error" + error_str, | |
type="internal_error", | |
param=getattr(e, "param", "None"), | |
code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
) | |
async def global_spend_key_internal_user( | |
user_api_key_dict: UserAPIKeyAuth, limit: int = 10 | |
): | |
from litellm.proxy.proxy_server import prisma_client | |
if prisma_client is None: | |
raise HTTPException(status_code=500, detail={"error": "No db connected"}) | |
user_id = user_api_key_dict.user_id | |
if user_id is None: | |
raise HTTPException(status_code=500, detail={"error": "No user_id found"}) | |
sql_query = """ | |
WITH top_api_keys AS ( | |
SELECT | |
api_key, | |
SUM(spend) as total_spend | |
FROM | |
"LiteLLM_SpendLogs" | |
WHERE | |
"user" = $1 | |
GROUP BY | |
api_key | |
ORDER BY | |
total_spend DESC | |
LIMIT $2 -- Adjust this number to get more or fewer top keys | |
) | |
SELECT | |
t.api_key, | |
t.total_spend, | |
v.key_alias, | |
v.key_name | |
FROM | |
top_api_keys t | |
LEFT JOIN | |
"LiteLLM_VerificationToken" v ON t.api_key = v.token | |
ORDER BY | |
t.total_spend DESC; | |
""" | |
response = await prisma_client.db.query_raw(sql_query, user_id, limit) | |
return response | |
async def global_spend_keys( | |
limit: int = fastapi.Query( | |
default=None, | |
description="Number of keys to get. Will return Top 'n' keys.", | |
), | |
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), | |
): | |
""" | |
[BETA] This is a beta endpoint. It will change. | |
Use this to get the top 'n' keys with the highest spend, ordered by spend. | |
""" | |
from litellm.proxy.proxy_server import prisma_client | |
if ( | |
user_api_key_dict.user_role == LitellmUserRoles.INTERNAL_USER | |
or user_api_key_dict.user_role == LitellmUserRoles.INTERNAL_USER_VIEW_ONLY | |
): | |
response = await global_spend_key_internal_user( | |
user_api_key_dict=user_api_key_dict | |
) | |
return response | |
if prisma_client is None: | |
raise HTTPException(status_code=500, detail={"error": "No db connected"}) | |
sql_query = """SELECT * FROM "Last30dKeysBySpend";""" | |
if limit is None: | |
response = await prisma_client.db.query_raw(sql_query) | |
return response | |
try: | |
limit = int(limit) | |
if limit < 1: | |
raise ValueError("Limit must be greater than 0") | |
sql_query = """SELECT * FROM "Last30dKeysBySpend" LIMIT $1 ;""" | |
response = await prisma_client.db.query_raw(sql_query, limit) | |
except ValueError as e: | |
raise HTTPException( | |
status_code=422, detail={"error": f"Invalid limit: {limit}, error: {e}"} | |
) from e | |
return response | |
async def global_spend_per_team(): | |
""" | |
[BETA] This is a beta endpoint. It will change. | |
Use this to get daily spend, grouped by `team_id` and `date` | |
""" | |
from litellm.proxy.proxy_server import prisma_client | |
if prisma_client is None: | |
raise HTTPException(status_code=500, detail={"error": "No db connected"}) | |
sql_query = """ | |
SELECT | |
t.team_alias as team_alias, | |
DATE(s."startTime") AS spend_date, | |
SUM(s.spend) AS total_spend | |
FROM | |
"LiteLLM_SpendLogs" s | |
LEFT JOIN | |
"LiteLLM_TeamTable" t ON s.team_id = t.team_id | |
WHERE | |
s."startTime" >= CURRENT_DATE - INTERVAL '30 days' | |
GROUP BY | |
t.team_alias, | |
DATE(s."startTime") | |
ORDER BY | |
spend_date; | |
""" | |
response = await prisma_client.db.query_raw(query=sql_query) | |
# transform the response for the Admin UI | |
spend_by_date = {} | |
team_aliases = set() | |
total_spend_per_team = {} | |
for row in response: | |
row_date = row["spend_date"] | |
if row_date is None: | |
continue | |
team_alias = row["team_alias"] | |
if team_alias is None: | |
team_alias = "Unassigned" | |
team_aliases.add(team_alias) | |
if row_date in spend_by_date: | |
# get the team_id for this entry | |
# get the spend for this entry | |
spend = row["total_spend"] | |
spend = round(spend, 2) | |
current_date_entries = spend_by_date[row_date] | |
current_date_entries[team_alias] = spend | |
else: | |
spend = row["total_spend"] | |
spend = round(spend, 2) | |
spend_by_date[row_date] = {team_alias: spend} | |
if team_alias in total_spend_per_team: | |
total_spend_per_team[team_alias] += spend | |
else: | |
total_spend_per_team[team_alias] = spend | |
total_spend_per_team_ui = [] | |
# order the elements in total_spend_per_team by spend | |
total_spend_per_team = dict( | |
sorted(total_spend_per_team.items(), key=lambda item: item[1], reverse=True) | |
) | |
for team_id in total_spend_per_team: | |
# only add first 10 elements to total_spend_per_team_ui | |
if len(total_spend_per_team_ui) >= 10: | |
break | |
if team_id is None: | |
team_id = "Unassigned" | |
total_spend_per_team_ui.append( | |
{"team_id": team_id, "total_spend": total_spend_per_team[team_id]} | |
) | |
# sort spend_by_date by it's key (which is a date) | |
response_data = [] | |
for key in spend_by_date: | |
value = spend_by_date[key] | |
response_data.append({"date": key, **value}) | |
return { | |
"daily_spend": response_data, | |
"teams": list(team_aliases), | |
"total_spend_per_team": total_spend_per_team_ui, | |
} | |
async def global_view_all_end_users(): | |
""" | |
[BETA] This is a beta endpoint. It will change. | |
Use this to just get all the unique `end_users` | |
""" | |
from litellm.proxy.proxy_server import prisma_client | |
if prisma_client is None: | |
raise HTTPException(status_code=500, detail={"error": "No db connected"}) | |
sql_query = """ | |
SELECT DISTINCT end_user FROM "LiteLLM_SpendLogs" | |
""" | |
db_response = await prisma_client.db.query_raw(query=sql_query) | |
if db_response is None: | |
return [] | |
_end_users = [] | |
for row in db_response: | |
_end_users.append(row["end_user"]) | |
return {"end_users": _end_users} | |
async def global_spend_end_users(data: Optional[GlobalEndUsersSpend] = None): | |
""" | |
[BETA] This is a beta endpoint. It will change. | |
Use this to get the top 'n' keys with the highest spend, ordered by spend. | |
""" | |
from litellm.proxy.proxy_server import prisma_client | |
if prisma_client is None: | |
raise HTTPException(status_code=500, detail={"error": "No db connected"}) | |
""" | |
Gets the top 100 end-users for a given api key | |
""" | |
startTime = None | |
endTime = None | |
selected_api_key = None | |
if data is not None: | |
startTime = data.startTime | |
endTime = data.endTime | |
selected_api_key = data.api_key | |
startTime = startTime or datetime.now() - timedelta(days=30) | |
endTime = endTime or datetime.now() | |
sql_query = """ | |
SELECT end_user, COUNT(*) AS total_count, SUM(spend) AS total_spend | |
FROM "LiteLLM_SpendLogs" | |
WHERE "startTime" >= $1::timestamp | |
AND "startTime" < $2::timestamp | |
AND ( | |
CASE | |
WHEN $3::TEXT IS NULL THEN TRUE | |
ELSE api_key = $3 | |
END | |
) | |
GROUP BY end_user | |
ORDER BY total_spend DESC | |
LIMIT 100 | |
""" | |
response = await prisma_client.db.query_raw( | |
sql_query, startTime, endTime, selected_api_key | |
) | |
return response | |
async def global_spend_models_internal_user( | |
user_api_key_dict: UserAPIKeyAuth, limit: int = 10 | |
): | |
from litellm.proxy.proxy_server import prisma_client | |
if prisma_client is None: | |
raise HTTPException(status_code=500, detail={"error": "No db connected"}) | |
user_id = user_api_key_dict.user_id | |
if user_id is None: | |
raise HTTPException(status_code=500, detail={"error": "No user_id found"}) | |
sql_query = """ | |
SELECT | |
model, | |
SUM(spend) as total_spend, | |
SUM(total_tokens) as total_tokens | |
FROM | |
"LiteLLM_SpendLogs" | |
WHERE | |
"user" = $1 | |
GROUP BY | |
model | |
ORDER BY | |
total_spend DESC | |
LIMIT $2; | |
""" | |
response = await prisma_client.db.query_raw(sql_query, user_id, limit) | |
return response | |
async def global_spend_models( | |
limit: int = fastapi.Query( | |
default=10, | |
description="Number of models to get. Will return Top 'n' models.", | |
), | |
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), | |
): | |
""" | |
[BETA] This is a beta endpoint. It will change. | |
Use this to get the top 'n' models with the highest spend, ordered by spend. | |
""" | |
from litellm.proxy.proxy_server import prisma_client | |
if ( | |
user_api_key_dict.user_role == LitellmUserRoles.INTERNAL_USER | |
or user_api_key_dict.user_role == LitellmUserRoles.INTERNAL_USER_VIEW_ONLY | |
): | |
response = await global_spend_models_internal_user( | |
user_api_key_dict=user_api_key_dict, limit=limit | |
) | |
return response | |
if prisma_client is None: | |
raise HTTPException(status_code=500, detail={"error": "No db connected"}) | |
sql_query = """SELECT * FROM "Last30dModelsBySpend" LIMIT $1 ;""" | |
response = await prisma_client.db.query_raw(sql_query, int(limit)) | |
return response | |
async def provider_budgets() -> ProviderBudgetResponse: | |
""" | |
Provider Budget Routing - Get Budget, Spend Details https://docs.litellm.ai/docs/proxy/provider_budget_routing | |
Use this endpoint to check current budget, spend and budget reset time for a provider | |
Example Request | |
```bash | |
curl -X GET http://localhost:4000/provider/budgets \ | |
-H "Content-Type: application/json" \ | |
-H "Authorization: Bearer sk-1234" | |
``` | |
Example Response | |
```json | |
{ | |
"providers": { | |
"openai": { | |
"budget_limit": 1e-12, | |
"time_period": "1d", | |
"spend": 0.0, | |
"budget_reset_at": null | |
}, | |
"azure": { | |
"budget_limit": 100.0, | |
"time_period": "1d", | |
"spend": 0.0, | |
"budget_reset_at": null | |
}, | |
"anthropic": { | |
"budget_limit": 100.0, | |
"time_period": "10d", | |
"spend": 0.0, | |
"budget_reset_at": null | |
}, | |
"vertex_ai": { | |
"budget_limit": 100.0, | |
"time_period": "12d", | |
"spend": 0.0, | |
"budget_reset_at": null | |
} | |
} | |
} | |
``` | |
""" | |
from litellm.proxy.proxy_server import llm_router | |
try: | |
if llm_router is None: | |
raise HTTPException( | |
status_code=500, detail={"error": "No llm_router found"} | |
) | |
provider_budget_config = llm_router.provider_budget_config | |
if provider_budget_config is None: | |
raise ValueError( | |
"No provider budget config found. Please set a provider budget config in the router settings. https://docs.litellm.ai/docs/proxy/provider_budget_routing" | |
) | |
provider_budget_response_dict: Dict[str, ProviderBudgetResponseObject] = {} | |
for _provider, _budget_info in provider_budget_config.items(): | |
if llm_router.router_budget_logger is None: | |
raise ValueError("No router budget logger found") | |
_provider_spend = ( | |
await llm_router.router_budget_logger._get_current_provider_spend( | |
_provider | |
) | |
or 0.0 | |
) | |
_provider_budget_ttl = await llm_router.router_budget_logger._get_current_provider_budget_reset_at( | |
_provider | |
) | |
provider_budget_response_object = ProviderBudgetResponseObject( | |
budget_limit=_budget_info.max_budget, | |
time_period=_budget_info.budget_duration, | |
spend=_provider_spend, | |
budget_reset_at=_provider_budget_ttl, | |
) | |
provider_budget_response_dict[_provider] = provider_budget_response_object | |
return ProviderBudgetResponse(providers=provider_budget_response_dict) | |
except Exception as e: | |
verbose_proxy_logger.exception( | |
"/provider/budgets: Exception occured - {}".format(str(e)) | |
) | |
raise handle_exception_on_proxy(e) | |
async def get_spend_by_tags( | |
prisma_client: PrismaClient, start_date=None, end_date=None | |
): | |
response = await prisma_client.db.query_raw( | |
""" | |
SELECT | |
jsonb_array_elements_text(request_tags) AS individual_request_tag, | |
COUNT(*) AS log_count, | |
SUM(spend) AS total_spend | |
FROM "LiteLLM_SpendLogs" | |
GROUP BY individual_request_tag; | |
""" | |
) | |
return response | |
async def ui_get_spend_by_tags( | |
start_date: str, | |
end_date: str, | |
prisma_client: Optional[PrismaClient] = None, | |
tags_str: Optional[str] = None, | |
): | |
""" | |
Should cover 2 cases: | |
1. When user is getting spend for all_tags. "all_tags" in tags_list | |
2. When user is getting spend for specific tags. | |
""" | |
# tags_str is a list of strings csv of tags | |
# tags_str = tag1,tag2,tag3 | |
# convert to list if it's not None | |
tags_list: Optional[List[str]] = None | |
if tags_str is not None and len(tags_str) > 0: | |
tags_list = tags_str.split(",") | |
if prisma_client is None: | |
raise HTTPException(status_code=500, detail={"error": "No db connected"}) | |
response = None | |
if tags_list is None or (isinstance(tags_list, list) and "all-tags" in tags_list): | |
# Get spend for all tags | |
sql_query = """ | |
SELECT | |
individual_request_tag, | |
spend_date, | |
log_count, | |
total_spend | |
FROM "DailyTagSpend" | |
WHERE spend_date >= $1::date AND spend_date <= $2::date | |
ORDER BY total_spend DESC; | |
""" | |
response = await prisma_client.db.query_raw( | |
sql_query, | |
start_date, | |
end_date, | |
) | |
else: | |
# filter by tags list | |
sql_query = """ | |
SELECT | |
individual_request_tag, | |
SUM(log_count) AS log_count, | |
SUM(total_spend) AS total_spend | |
FROM "DailyTagSpend" | |
WHERE spend_date >= $1::date AND spend_date <= $2::date | |
AND individual_request_tag = ANY($3::text[]) | |
GROUP BY individual_request_tag | |
ORDER BY total_spend DESC; | |
""" | |
response = await prisma_client.db.query_raw( | |
sql_query, | |
start_date, | |
end_date, | |
tags_list, | |
) | |
# print("tags - spend") | |
# print(response) | |
# Bar Chart 1 - Spend per tag - Top 10 tags by spend | |
total_spend_per_tag: collections.defaultdict = collections.defaultdict(float) | |
total_requests_per_tag: collections.defaultdict = collections.defaultdict(int) | |
for row in response: | |
tag_name = row["individual_request_tag"] | |
tag_spend = row["total_spend"] | |
total_spend_per_tag[tag_name] += tag_spend | |
total_requests_per_tag[tag_name] += row["log_count"] | |
sorted_tags = sorted(total_spend_per_tag.items(), key=lambda x: x[1], reverse=True) | |
# convert to ui format | |
ui_tags = [] | |
for tag in sorted_tags: | |
current_spend = tag[1] | |
if current_spend is not None and isinstance(current_spend, float): | |
current_spend = round(current_spend, 4) | |
ui_tags.append( | |
{ | |
"name": tag[0], | |
"spend": current_spend, | |
"log_count": total_requests_per_tag[tag[0]], | |
} | |
) | |
return {"spend_per_tag": ui_tags} | |
async def ui_view_session_spend_logs( | |
session_id: str = fastapi.Query( | |
description="Get all spend logs for a particular session", | |
), | |
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), | |
): | |
""" | |
Get all spend logs for a particular session | |
""" | |
from litellm.proxy.proxy_server import prisma_client | |
try: | |
if prisma_client is None: | |
raise HTTPException( | |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
detail="Database not connected", | |
) | |
# Build query conditions | |
where_conditions = {"session_id": session_id} | |
# Query the database | |
result = await prisma_client.db.litellm_spendlogs.find_many( | |
where=where_conditions, order={"startTime": "asc"} | |
) | |
return result | |
except Exception as e: | |
if isinstance(e, HTTPException): | |
raise e | |
else: | |
raise HTTPException( | |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
detail=str(e), | |
) | |