mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-22 00:31:44 +00:00
fix(ui): block the global usage export when the aggregated key cap is reached
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
f863e74612
commit
61b3611b8c
4 changed files with 56 additions and 4 deletions
|
|
@ -664,7 +664,7 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
|
|||
{ key: "endpoints", label: "Endpoint Activity", content: <EndpointUsage userSpendData={spendData} /> },
|
||||
];
|
||||
|
||||
const spendFetchState = { coversRange, cancelled, failed };
|
||||
const spendFetchState = { coversRange, cancelled, failed, apiKeyLimitReached: undefined };
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%" }} className="relative">
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import { ActivityMetrics, processActivityData } from "@/components/activity_metr
|
|||
import CloudZeroExportModal from "@/components/cloudzero_export_modal";
|
||||
import UserDropdown from "@/components/common_components/UserDropdown";
|
||||
import EntityUsageExportModal from "@/components/EntityUsageExport";
|
||||
import { getExportBlockedReason } from "@/components/EntityUsageExport/exportBlockedReason";
|
||||
import { getApiKeyLimitReached, getExportBlockedReason } from "@/components/EntityUsageExport/exportBlockedReason";
|
||||
import KeyActivityPanel from "@/components/UsagePage/components/KeyActivityPanel";
|
||||
import { Team } from "@/components/key_team_helpers/key_list";
|
||||
import {
|
||||
|
|
@ -256,6 +256,7 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
|
|||
coversRange: activeAggregated !== null || paginatedResult.coversRange,
|
||||
cancelled: paginatedResult.cancelled,
|
||||
failed: paginatedResult.failed,
|
||||
apiKeyLimitReached: getApiKeyLimitReached(userSpendData.results, userSpendData.metadata?.api_key_limit),
|
||||
};
|
||||
const exportBlockedReason = getExportBlockedReason(spendFetchState);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,24 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { getExportBlockedReason, type UsageFetchState } from "./exportBlockedReason";
|
||||
import type { DailyData } from "@/components/UsagePage/types";
|
||||
|
||||
import { getApiKeyLimitReached, getExportBlockedReason, type UsageFetchState } from "./exportBlockedReason";
|
||||
|
||||
const state = (overrides: Partial<UsageFetchState> = {}): UsageFetchState => ({
|
||||
coversRange: true,
|
||||
cancelled: false,
|
||||
failed: false,
|
||||
apiKeyLimitReached: undefined,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const dayWithKeys = (date: string, ...keys: string[]): DailyData =>
|
||||
({
|
||||
date,
|
||||
metrics: {},
|
||||
breakdown: { api_keys: Object.fromEntries(keys.map((k) => [k, { metrics: {}, metadata: {} }])) },
|
||||
}) as unknown as DailyData;
|
||||
|
||||
describe("getExportBlockedReason", () => {
|
||||
it("lets the export through once the data on screen covers the range", () => {
|
||||
expect(getExportBlockedReason(state())).toBeUndefined();
|
||||
|
|
@ -31,4 +41,29 @@ describe("getExportBlockedReason", () => {
|
|||
expect(reason).toMatch(/failed to load/i);
|
||||
expect(reason).not.toMatch(/stopped/i);
|
||||
});
|
||||
|
||||
it("blocks when the aggregated endpoint hit its key cap, since a per-team CSV would miss keys", () => {
|
||||
const reason = getExportBlockedReason(state({ apiKeyLimitReached: 100 }));
|
||||
|
||||
expect(reason).toMatch(/100 highest-spend keys/);
|
||||
expect(reason).toMatch(/USAGE_TOP_API_KEYS_LIMIT/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getApiKeyLimitReached", () => {
|
||||
it("reports the cap once the distinct keys across every day reach it", () => {
|
||||
const results = [dayWithKeys("2026-06-01", "key-1", "key-2"), dayWithKeys("2026-06-02", "key-2", "key-3")];
|
||||
|
||||
expect(getApiKeyLimitReached(results, 3)).toBe(3);
|
||||
});
|
||||
|
||||
it("stays quiet while fewer keys than the cap came back, which means every key is on screen", () => {
|
||||
const results = [dayWithKeys("2026-06-01", "key-1", "key-2"), dayWithKeys("2026-06-02", "key-2")];
|
||||
|
||||
expect(getApiKeyLimitReached(results, 3)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("stays quiet when the response carries no cap, as the paginated fallback does", () => {
|
||||
expect(getApiKeyLimitReached([dayWithKeys("2026-06-01", "key-1")], undefined)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,13 +1,29 @@
|
|||
import type { DailyData } from "@/components/UsagePage/types";
|
||||
|
||||
export interface UsageFetchState {
|
||||
coversRange: boolean;
|
||||
cancelled: boolean;
|
||||
failed: boolean;
|
||||
apiKeyLimitReached: number | undefined;
|
||||
}
|
||||
|
||||
export const getExportBlockedReason = ({ coversRange, cancelled, failed }: UsageFetchState): string | undefined => {
|
||||
export const getApiKeyLimitReached = (results: DailyData[], apiKeyLimit: unknown): number | undefined => {
|
||||
if (typeof apiKeyLimit !== "number") return undefined;
|
||||
const keys = new Set(results.flatMap((day) => Object.keys(day.breakdown.api_keys ?? {})));
|
||||
return keys.size >= apiKeyLimit ? apiKeyLimit : undefined;
|
||||
};
|
||||
|
||||
export const getExportBlockedReason = ({
|
||||
coversRange,
|
||||
cancelled,
|
||||
failed,
|
||||
apiKeyLimitReached,
|
||||
}: UsageFetchState): string | undefined => {
|
||||
if (failed) return "Some spend data failed to load, so an export would under-report. Reload the page to try again.";
|
||||
if (cancelled)
|
||||
return "Loading was stopped before the whole range arrived, so an export would under-report. Reload the page to load it all.";
|
||||
if (!coversRange) return "Spend data is still loading, so an export would under-report. Wait for it to finish.";
|
||||
if (apiKeyLimitReached !== undefined)
|
||||
return `Only the ${apiKeyLimitReached} highest-spend keys were loaded, so a per-team export would under-report. Raise USAGE_TOP_API_KEYS_LIMIT on the proxy to load more keys.`;
|
||||
return undefined;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue