mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-10 22:41:41 +00:00
* model filter added * Make status logic it's own helper + add a unit test in test_spend_management_endpoints Co-authored-by: tanjiro <56165694+NANDINI-star@users.noreply.github.com>
This commit is contained in:
parent
d5d5166777
commit
f335bb91dd
6 changed files with 284 additions and 14 deletions
|
|
@ -1658,6 +1658,10 @@ async def ui_view_spend_logs( # noqa: PLR0915
|
|||
default=None,
|
||||
description="Filter logs by status (e.g., success, failure)"
|
||||
),
|
||||
model: Optional[str] = fastapi.Query(
|
||||
default=None,
|
||||
description="Filter logs by model"
|
||||
),
|
||||
):
|
||||
"""
|
||||
View spend logs for UI with pagination support
|
||||
|
|
@ -1710,11 +1714,9 @@ async def ui_view_spend_logs( # noqa: PLR0915
|
|||
if team_id is not None:
|
||||
where_conditions["team_id"] = team_id
|
||||
|
||||
if status_filter is not None:
|
||||
if status_filter == "success":
|
||||
where_conditions["status"] = {"in": ["success", None]} # Assuming None means empty status
|
||||
else:
|
||||
where_conditions["status"] = status_filter # Filtering for other status values
|
||||
status_condition = _build_status_filter_condition(status_filter)
|
||||
if status_condition:
|
||||
where_conditions.update(status_condition)
|
||||
|
||||
if api_key is not None:
|
||||
where_conditions["api_key"] = api_key
|
||||
|
|
@ -1725,6 +1727,9 @@ async def ui_view_spend_logs( # noqa: PLR0915
|
|||
if request_id is not None:
|
||||
where_conditions["request_id"] = request_id
|
||||
|
||||
if model is not None:
|
||||
where_conditions["model"] = model
|
||||
|
||||
if min_spend is not None or max_spend is not None:
|
||||
where_conditions["spend"] = {}
|
||||
if min_spend is not None:
|
||||
|
|
@ -2912,3 +2917,27 @@ async def ui_view_session_spend_logs(
|
|||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=str(e),
|
||||
)
|
||||
|
||||
|
||||
def _build_status_filter_condition(status_filter: Optional[str]) -> Dict[str, Any]:
|
||||
"""
|
||||
Helper function to build the status filter condition for database queries.
|
||||
|
||||
Args:
|
||||
status_filter (Optional[str]): The status to filter by. Can be "success" or "failure".
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: A dictionary containing the status filter condition.
|
||||
"""
|
||||
if status_filter is None:
|
||||
return {}
|
||||
|
||||
if status_filter == "success":
|
||||
return {
|
||||
"OR": [
|
||||
{"status": {"equals": "success"}},
|
||||
{"status": None}
|
||||
]
|
||||
}
|
||||
else:
|
||||
return {"status": {"equals": status_filter}}
|
||||
|
|
|
|||
|
|
@ -425,6 +425,193 @@ async def test_ui_view_spend_logs_unauthorized(client):
|
|||
assert response.status_code == 401 or response.status_code == 403
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ui_view_spend_logs_with_status(client, monkeypatch):
|
||||
# Mock data for the test
|
||||
mock_spend_logs = [
|
||||
{
|
||||
"id": "log1",
|
||||
"request_id": "req1",
|
||||
"api_key": "sk-test-key",
|
||||
"user": "test_user_1",
|
||||
"team_id": "team1",
|
||||
"spend": 0.05,
|
||||
"startTime": datetime.datetime.now(timezone.utc).isoformat(),
|
||||
"model": "gpt-3.5-turbo",
|
||||
"status": "success"
|
||||
},
|
||||
{
|
||||
"id": "log2",
|
||||
"request_id": "req2",
|
||||
"api_key": "sk-test-key",
|
||||
"user": "test_user_2",
|
||||
"team_id": "team1",
|
||||
"spend": 0.10,
|
||||
"startTime": datetime.datetime.now(timezone.utc).isoformat(),
|
||||
"model": "gpt-4",
|
||||
"status": "failure"
|
||||
},
|
||||
]
|
||||
|
||||
# Create a mock prisma client
|
||||
class MockDB:
|
||||
async def find_many(self, *args, **kwargs):
|
||||
# Filter based on status in the where conditions
|
||||
if "where" in kwargs:
|
||||
where_conditions = kwargs["where"]
|
||||
if "OR" in where_conditions:
|
||||
# Handle success case (which includes None status)
|
||||
return [mock_spend_logs[0]]
|
||||
elif "status" in where_conditions and where_conditions["status"]["equals"] == "failure":
|
||||
return [mock_spend_logs[1]]
|
||||
return mock_spend_logs
|
||||
|
||||
async def count(self, *args, **kwargs):
|
||||
# Return count based on status filter
|
||||
if "where" in kwargs:
|
||||
where_conditions = kwargs["where"]
|
||||
if "OR" in where_conditions:
|
||||
return 1
|
||||
elif "status" in where_conditions and where_conditions["status"]["equals"] == "failure":
|
||||
return 1
|
||||
return len(mock_spend_logs)
|
||||
|
||||
class MockPrismaClient:
|
||||
def __init__(self):
|
||||
self.db = MockDB()
|
||||
self.db.litellm_spendlogs = self.db
|
||||
|
||||
# Apply the monkeypatch
|
||||
mock_prisma_client = MockPrismaClient()
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
|
||||
|
||||
# Set up test dates
|
||||
start_date = (
|
||||
datetime.datetime.now(timezone.utc) - datetime.timedelta(days=7)
|
||||
).strftime("%Y-%m-%d %H:%M:%S")
|
||||
end_date = datetime.datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# Test success status
|
||||
response = client.get(
|
||||
"/spend/logs/ui",
|
||||
params={
|
||||
"status_filter": "success",
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
},
|
||||
headers={"Authorization": "Bearer sk-test"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["total"] == 1
|
||||
assert len(data["data"]) == 1
|
||||
assert data["data"][0]["status"] == "success"
|
||||
|
||||
# Test failure status
|
||||
response = client.get(
|
||||
"/spend/logs/ui",
|
||||
params={
|
||||
"status_filter": "failure",
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
},
|
||||
headers={"Authorization": "Bearer sk-test"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["total"] == 1
|
||||
assert len(data["data"]) == 1
|
||||
assert data["data"][0]["status"] == "failure"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ui_view_spend_logs_with_model(client, monkeypatch):
|
||||
# Mock data for the test
|
||||
mock_spend_logs = [
|
||||
{
|
||||
"id": "log1",
|
||||
"request_id": "req1",
|
||||
"api_key": "sk-test-key",
|
||||
"user": "test_user_1",
|
||||
"team_id": "team1",
|
||||
"spend": 0.05,
|
||||
"startTime": datetime.datetime.now(timezone.utc).isoformat(),
|
||||
"model": "gpt-3.5-turbo",
|
||||
"status": "success"
|
||||
},
|
||||
{
|
||||
"id": "log2",
|
||||
"request_id": "req2",
|
||||
"api_key": "sk-test-key",
|
||||
"user": "test_user_2",
|
||||
"team_id": "team1",
|
||||
"spend": 0.10,
|
||||
"startTime": datetime.datetime.now(timezone.utc).isoformat(),
|
||||
"model": "gpt-4",
|
||||
"status": "success"
|
||||
},
|
||||
]
|
||||
|
||||
# Create a mock prisma client
|
||||
class MockDB:
|
||||
async def find_many(self, *args, **kwargs):
|
||||
# Filter based on model in the where conditions
|
||||
if (
|
||||
"where" in kwargs
|
||||
and "model" in kwargs["where"]
|
||||
and kwargs["where"]["model"] == "gpt-3.5-turbo"
|
||||
):
|
||||
return [mock_spend_logs[0]]
|
||||
return mock_spend_logs
|
||||
|
||||
async def count(self, *args, **kwargs):
|
||||
# Return count based on model filter
|
||||
if (
|
||||
"where" in kwargs
|
||||
and "model" in kwargs["where"]
|
||||
and kwargs["where"]["model"] == "gpt-3.5-turbo"
|
||||
):
|
||||
return 1
|
||||
return len(mock_spend_logs)
|
||||
|
||||
class MockPrismaClient:
|
||||
def __init__(self):
|
||||
self.db = MockDB()
|
||||
self.db.litellm_spendlogs = self.db
|
||||
|
||||
# Apply the monkeypatch
|
||||
mock_prisma_client = MockPrismaClient()
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
|
||||
|
||||
# Set up test dates
|
||||
start_date = (
|
||||
datetime.datetime.now(timezone.utc) - datetime.timedelta(days=7)
|
||||
).strftime("%Y-%m-%d %H:%M:%S")
|
||||
end_date = datetime.datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# Make the request with model filter
|
||||
response = client.get(
|
||||
"/spend/logs/ui",
|
||||
params={
|
||||
"model": "gpt-3.5-turbo",
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
},
|
||||
headers={"Authorization": "Bearer sk-test"},
|
||||
)
|
||||
|
||||
# Assert response
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
|
||||
# Verify the filtered data
|
||||
assert data["total"] == 1
|
||||
assert len(data["data"]) == 1
|
||||
assert data["data"][0]["model"] == "gpt-3.5-turbo"
|
||||
|
||||
|
||||
class TestSpendLogsPayload:
|
||||
@pytest.mark.asyncio
|
||||
async def test_spend_logs_payload_e2e(self):
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ const FilterComponent: React.FC<FilterComponentProps> = ({
|
|||
"Key Alias",
|
||||
"User ID",
|
||||
"Key Hash",
|
||||
"Model"
|
||||
];
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -2147,7 +2147,8 @@ export const uiSpendLogsCall = async (
|
|||
page?: number,
|
||||
page_size?: number,
|
||||
user_id?: string,
|
||||
status_filter?: string
|
||||
status_filter?: string,
|
||||
model?: string
|
||||
) => {
|
||||
try {
|
||||
// Construct base URL
|
||||
|
|
@ -2164,7 +2165,7 @@ export const uiSpendLogsCall = async (
|
|||
if (page_size) queryParams.append('page_size', page_size.toString());
|
||||
if (user_id) queryParams.append('user_id', user_id);
|
||||
if (status_filter) queryParams.append('status_filter', status_filter);
|
||||
|
||||
if (model) queryParams.append('model', model);
|
||||
// Append query parameters to URL if any exist
|
||||
const queryString = queryParams.toString();
|
||||
if (queryString) {
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ export default function SpendLogsTable({
|
|||
const [tempKeyHash, setTempKeyHash] = useState("");
|
||||
const [selectedTeamId, setSelectedTeamId] = useState("");
|
||||
const [selectedKeyHash, setSelectedKeyHash] = useState("");
|
||||
const [selectedModel, setSelectedModel] = useState("");
|
||||
const [selectedKeyInfo, setSelectedKeyInfo] = useState<KeyResponse | null>(null);
|
||||
const [selectedKeyIdInfoView, setSelectedKeyIdInfoView] = useState<string | null>(null);
|
||||
const [selectedStatus, setSelectedStatus] = useState("");
|
||||
|
|
@ -147,7 +148,8 @@ export default function SpendLogsTable({
|
|||
selectedTeamId,
|
||||
selectedKeyHash,
|
||||
filterByCurrentUser ? userID : null,
|
||||
selectedStatus
|
||||
selectedStatus,
|
||||
selectedModel
|
||||
],
|
||||
queryFn: async () => {
|
||||
if (!accessToken || !token || !userRole || !userID) {
|
||||
|
|
@ -177,7 +179,8 @@ export default function SpendLogsTable({
|
|||
currentPage,
|
||||
pageSize,
|
||||
filterByCurrentUser ? userID : undefined,
|
||||
selectedStatus
|
||||
selectedStatus,
|
||||
selectedModel
|
||||
);
|
||||
|
||||
// Trigger prefetch for all logs
|
||||
|
|
@ -222,6 +225,7 @@ export default function SpendLogsTable({
|
|||
filteredLogs,
|
||||
allTeams: hookAllTeams,
|
||||
allKeyAliases,
|
||||
allModels,
|
||||
handleFilterChange,
|
||||
handleFilterReset
|
||||
} = useLogFilterLogic({
|
||||
|
|
@ -231,7 +235,9 @@ export default function SpendLogsTable({
|
|||
endTime,
|
||||
pageSize,
|
||||
isCustomDate,
|
||||
setCurrentPage
|
||||
setCurrentPage,
|
||||
userID,
|
||||
userRole
|
||||
})
|
||||
|
||||
// Add this effect to update selectedTeamId and selectedStatus when team filter changes
|
||||
|
|
@ -243,6 +249,7 @@ export default function SpendLogsTable({
|
|||
setSelectedTeamId("");
|
||||
}
|
||||
setSelectedStatus(filters['Status'] || "");
|
||||
setSelectedModel(filters['Model'] || "");
|
||||
}, [filters]);
|
||||
|
||||
// Fetch logs for a session if selected
|
||||
|
|
@ -360,6 +367,21 @@ export default function SpendLogsTable({
|
|||
{ label: 'Success', value: 'success' },
|
||||
{ label: 'Failure', value: 'failure' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Model',
|
||||
label: 'Model',
|
||||
isSearchable: true,
|
||||
searchFn: async (searchText: string) => {
|
||||
if (!allModels || allModels.length === 0) return [];
|
||||
const filtered = allModels.filter((model: string) => {
|
||||
return model.toLowerCase().includes(searchText.toLowerCase());
|
||||
});
|
||||
return filtered.map((model: string) => ({
|
||||
label: model,
|
||||
value: model
|
||||
}));
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import moment from "moment";
|
||||
import { useCallback, useEffect, useState, useRef, useMemo } from "react";
|
||||
import { uiSpendLogsCall } from "../networking";
|
||||
import { modelAvailableCall, uiSpendLogsCall } from "../networking";
|
||||
import { Team } from "../key_team_helpers/key_list";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetchAllKeyAliases, fetchAllTeams } from "../../components/key_team_helpers/filter_helpers";
|
||||
|
|
@ -14,7 +14,7 @@ export const FILTER_KEYS = {
|
|||
REQUEST_ID: "Request ID",
|
||||
MODEL: "Model",
|
||||
USER_ID: "User ID",
|
||||
STATUS: "Status"
|
||||
STATUS: "Status",
|
||||
} as const;
|
||||
|
||||
export type FilterKey = keyof typeof FILTER_KEYS;
|
||||
|
|
@ -27,7 +27,9 @@ export function useLogFilterLogic({
|
|||
endTime, // Receive from SpendLogsTable
|
||||
pageSize = defaultPageSize,
|
||||
isCustomDate,
|
||||
setCurrentPage
|
||||
setCurrentPage,
|
||||
userID,
|
||||
userRole
|
||||
}: {
|
||||
logs: PaginatedResponse;
|
||||
accessToken: string | null;
|
||||
|
|
@ -36,6 +38,8 @@ export function useLogFilterLogic({
|
|||
pageSize?: number;
|
||||
isCustomDate: boolean;
|
||||
setCurrentPage: (page: number) => void;
|
||||
userID: string | null;
|
||||
userRole: string | null;
|
||||
}) {
|
||||
const defaultFilters = useMemo<LogFilterState>(() => ({
|
||||
[FILTER_KEYS.TEAM_ID]: "",
|
||||
|
|
@ -71,7 +75,8 @@ export function useLogFilterLogic({
|
|||
page,
|
||||
pageSize,
|
||||
filters[FILTER_KEYS.USER_ID] || undefined,
|
||||
filters[FILTER_KEYS.STATUS] || undefined
|
||||
filters[FILTER_KEYS.STATUS] || undefined,
|
||||
filters[FILTER_KEYS.MODEL] || undefined
|
||||
);
|
||||
|
||||
if (currentTimestamp === lastSearchTimestamp.current && response.data) {
|
||||
|
|
@ -122,6 +127,12 @@ export function useLogFilterLogic({
|
|||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (filters[FILTER_KEYS.MODEL]) {
|
||||
filteredData = filteredData.filter(
|
||||
log => log.model === filters[FILTER_KEYS.MODEL]
|
||||
);
|
||||
}
|
||||
|
||||
const newFilteredLogs: PaginatedResponse = {
|
||||
data: filteredData,
|
||||
|
|
@ -159,6 +170,24 @@ export function useLogFilterLogic({
|
|||
enabled: !!accessToken,
|
||||
});
|
||||
|
||||
const { data: allModels = [] } = useQuery<string[], Error>({
|
||||
queryKey: ['allModels', accessToken, userID, userRole],
|
||||
queryFn: async () => {
|
||||
if (!accessToken || !userID || !userRole) return [];
|
||||
|
||||
const response = await modelAvailableCall(
|
||||
accessToken,
|
||||
userID,
|
||||
userRole,
|
||||
false, // return_wildcard_routes
|
||||
null // teamID
|
||||
);
|
||||
|
||||
return response.data.map((model: { id: string }) => model.id);
|
||||
},
|
||||
enabled: !!accessToken && !!userID && !!userRole,
|
||||
});
|
||||
|
||||
// Update filters state
|
||||
const handleFilterChange = (newFilters: Partial<LogFilterState>) => {
|
||||
setFilters(prev => {
|
||||
|
|
@ -195,6 +224,7 @@ export function useLogFilterLogic({
|
|||
filteredLogs,
|
||||
allKeyAliases,
|
||||
allTeams,
|
||||
allModels,
|
||||
handleFilterChange,
|
||||
handleFilterReset,
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue