Fixed Log Tab Key Alias filtering inaccurately for failed logs

This commit is contained in:
Sashanken
2025-09-11 13:05:48 -07:00
parent 258b674dbb
commit c6626559a2
7 changed files with 8186 additions and 38 deletions
@@ -1659,6 +1659,9 @@ async def ui_view_spend_logs( # noqa: PLR0915
model: Optional[str] = fastapi.Query(
default=None, description="Filter logs by model"
),
key_alias: Optional[str] = fastapi.Query(
default=None, description="Filter logs by key alias"
),
):
"""
View spend logs for UI with pagination support
@@ -1726,6 +1729,12 @@ async def ui_view_spend_logs( # noqa: PLR0915
if model is not None:
where_conditions["model"] = model
if key_alias is not None:
where_conditions["metadata"] = {
"path": ["user_api_key_alias"],
"string_contains": key_alias
}
if min_spend is not None or max_spend is not None:
where_conditions["spend"] = {}
+7975 -4
View File
File diff suppressed because it is too large Load Diff
+4 -1
View File
@@ -5,6 +5,9 @@
"react-copy-to-clipboard": "^5.1.0"
},
"devDependencies": {
"@types/react-copy-to-clipboard": "^5.0.7"
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^14.3.1",
"@types/react-copy-to-clipboard": "^5.0.7",
"jest": "^29.7.0"
}
}
@@ -0,0 +1,87 @@
import { uiSpendLogsCall } from '../../../ui/litellm-dashboard/src/components/networking';
// Mock the networking module
jest.mock('../../../ui/litellm-dashboard/src/components/networking', () => ({
uiSpendLogsCall: jest.fn(),
}));
const mockUiSpendLogsCall = uiSpendLogsCall as jest.MockedFunction<typeof uiSpendLogsCall>;
describe('Key Alias Filtering Integration Test', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should call API with correct key_alias parameter', async () => {
// Mock API response with both success and failure logs
const mockResponse = {
data: [
{ request_id: 'req-1', status: 'success', metadata: { user_api_key_alias: 'test-key' } },
{ request_id: 'req-2', status: 'failure', metadata: { user_api_key_alias: 'test-key' } }
],
total: 2,
page: 1,
page_size: 50,
total_pages: 1
};
mockUiSpendLogsCall.mockResolvedValueOnce(mockResponse);
// Simulate the API call that would happen when filtering by key alias
const result = await uiSpendLogsCall(
'test-token',
undefined,
undefined,
undefined,
'2024-01-15 09:00:00',
'2024-01-15 11:00:00',
1,
50,
undefined,
undefined,
undefined,
undefined,
'test-key-alias' // key_alias - this is the fix
);
// Verify the API was called correctly
expect(mockUiSpendLogsCall).toHaveBeenCalledWith(
'test-token',
undefined,
undefined,
undefined,
'2024-01-15 09:00:00',
'2024-01-15 11:00:00',
1,
50,
undefined,
undefined,
undefined,
undefined,
'test-key-alias' // The key assertion - this parameter should be passed through
);
// Verify response contains both success and failure logs
expect(result.data).toHaveLength(2);
expect(result.data[0].status).toBe('success');
expect(result.data[1].status).toBe('failure');
});
it('should pass undefined for empty key alias', async () => {
mockUiSpendLogsCall.mockResolvedValueOnce({ data: [], total: 0, page: 1, page_size: 50, total_pages: 0 });
await uiSpendLogsCall(
'test-token', undefined, undefined, undefined,
'2024-01-15 09:00:00', '2024-01-15 11:00:00',
1, 50, undefined, undefined, undefined, undefined,
undefined // Empty string should become undefined
);
expect(mockUiSpendLogsCall).toHaveBeenCalledWith(
'test-token', undefined, undefined, undefined,
'2024-01-15 09:00:00', '2024-01-15 11:00:00',
1, 50, undefined, undefined, undefined, undefined,
undefined // Should be undefined for empty key alias
);
});
});
+91 -12
View File
@@ -8,13 +8,14 @@
"name": "ui-unit-tests",
"version": "1.0.0",
"dependencies": {
"antd": "^5.0.0",
"@ant-design/icons": "^5.0.0",
"antd": "^5.12.5",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^14.0.0",
"@types/antd": "^1.0.0",
"@types/jest": "^29.5.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
@@ -25,6 +26,13 @@
"typescript": "^5.0.0"
}
},
"node_modules/@adobe/css-tools": {
"version": "4.4.4",
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz",
"integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==",
"dev": true,
"license": "MIT"
},
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
@@ -1167,6 +1175,33 @@
"node": ">=14"
}
},
"node_modules/@testing-library/jest-dom": {
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.8.0.tgz",
"integrity": "sha512-WgXcWzVM6idy5JaftTVC8Vs83NKRmGJz4Hqs4oyOuO2J4r/y79vvKZsb+CaGyCSEbUPI6OsewfPd0G1A0/TUZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@adobe/css-tools": "^4.4.0",
"aria-query": "^5.0.0",
"css.escape": "^1.5.1",
"dom-accessibility-api": "^0.6.3",
"picocolors": "^1.1.1",
"redent": "^3.0.0"
},
"engines": {
"node": ">=14",
"npm": ">=6",
"yarn": ">=1"
}
},
"node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
"integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
"dev": true,
"license": "MIT"
},
"node_modules/@testing-library/react": {
"version": "14.3.1",
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.3.1.tgz",
@@ -1194,16 +1229,6 @@
"node": ">= 10"
}
},
"node_modules/@types/antd": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@types/antd/-/antd-1.0.4.tgz",
"integrity": "sha512-gp4PGQckP1kNjj2H6juhjKIVwkpXwCIyIvOlwp2DC6geuhVpDHEEB5gwH4hJabVgBAFtrjBPJ58VIRV9VV9W2g==",
"deprecated": "This is a stub types definition. antd provides its own type definitions, so you do not need this installed.",
"dev": true,
"dependencies": {
"antd": "*"
}
},
"node_modules/@types/aria-query": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
@@ -2081,6 +2106,13 @@
"node": ">= 8"
}
},
"node_modules/css.escape": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
"integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
"dev": true,
"license": "MIT"
},
"node_modules/cssom": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
@@ -2974,6 +3006,16 @@
"node": ">=0.8.19"
}
},
"node_modules/indent-string": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -4559,6 +4601,16 @@
"node": ">=6"
}
},
"node_modules/min-indent": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -5552,6 +5604,20 @@
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true
},
"node_modules/redent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
"integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
"dev": true,
"license": "MIT",
"dependencies": {
"indent-string": "^4.0.0",
"strip-indent": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
@@ -5965,6 +6031,19 @@
"node": ">=6"
}
},
"node_modules/strip-indent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
"integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"min-indent": "^1.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -2633,7 +2633,8 @@ export const uiSpendLogsCall = async (
user_id?: string,
end_user?: string,
status_filter?: string,
model?: string
model?: string,
keyAlias?: string
) => {
try {
// Construct base URL
@@ -2652,6 +2653,7 @@ export const uiSpendLogsCall = async (
if (end_user) queryParams.append("end_user", end_user);
if (status_filter) queryParams.append("status_filter", status_filter);
if (model) queryParams.append("model", model);
if (keyAlias) queryParams.append("key_alias", keyAlias);
// Append query parameters to URL if any exist
const queryString = queryParams.toString();
if (queryString) {
@@ -60,6 +60,7 @@ export function useLogFilterLogic({
const performSearch = useCallback(async (filters: LogFilterState, page = 1) => {
if (!accessToken) return;
console.log("Filters being sent to API:", filters);
const currentTimestamp = Date.now();
lastSearchTimestamp.current = currentTimestamp;
@@ -81,7 +82,8 @@ export function useLogFilterLogic({
filters[FILTER_KEYS.USER_ID] || undefined,
filters[FILTER_KEYS.END_USER] || undefined,
filters[FILTER_KEYS.STATUS] || undefined,
filters[FILTER_KEYS.MODEL] || undefined
filters[FILTER_KEYS.MODEL] || undefined,
filters[FILTER_KEYS.KEY_ALIAS] || undefined
);
if (currentTimestamp === lastSearchTimestamp.current && response.data) {
@@ -123,6 +125,19 @@ export function useLogFilterLogic({
});
return;
}
// Only do client-side filtering if no backend filters are active
const hasBackendFilters =
filters[FILTER_KEYS.KEY_ALIAS] ||
filters[FILTER_KEYS.KEY_HASH] ||
filters[FILTER_KEYS.REQUEST_ID] ||
filters[FILTER_KEYS.USER_ID] ||
filters[FILTER_KEYS.END_USER];
if (hasBackendFilters) {
// Backend is handling filtering, don't override the results
return;
}
let filteredData = [...logs.data];
@@ -148,7 +163,7 @@ export function useLogFilterLogic({
log => log.model === filters[FILTER_KEYS.MODEL]
);
}
if (filters[FILTER_KEYS.KEY_HASH]) {
filteredData = filteredData.filter(
log => log.api_key === filters[FILTER_KEYS.KEY_HASH]
@@ -161,24 +176,6 @@ export function useLogFilterLogic({
);
}
// Add key alias filtering
if (filters[FILTER_KEYS.KEY_ALIAS]) {
// We need to fetch the key info to get the key hash for the selected alias
try {
// Get the key hash for the selected alias
const selectedKey = filters[FILTER_KEYS.KEY_ALIAS]
if (selectedKey) {
// Filter logs by the key hash
filteredData = filteredData.filter(
log => log.metadata?.user_api_key_alias === selectedKey
);
}
} catch (error) {
console.error("Error fetching key info for alias:", error);
}
}
const newFilteredLogs: PaginatedResponse = {
data: filteredData,
total: logs.total,