From 5da7fa9ac18a0b82a1a294389d6cf146fa443dd9 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 3 Mar 2026 14:39:39 -0800 Subject: [PATCH 1/3] [Feature] UI - Virtual Keys: Add manual spend reset to unblock keys Adds a "Reset Spend" button to the key detail view so proxy admins and team admins can immediately reset a key's spend to $0, unblocking keys that have hit their budget limit without waiting for the next scheduled budget reset. Co-Authored-By: Claude Sonnet 4.6 --- .../hooks/keys/useResetKeySpend.ts | 66 ++++++++ .../components/templates/KeyInfoHeader.tsx | 8 + .../templates/key_info_view.test.tsx | 141 ++++++++++++++++++ .../components/templates/key_info_view.tsx | 50 ++++++- 4 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/hooks/keys/useResetKeySpend.ts diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/keys/useResetKeySpend.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/keys/useResetKeySpend.ts new file mode 100644 index 00000000000..a845fc5881a --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/keys/useResetKeySpend.ts @@ -0,0 +1,66 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { + getProxyBaseUrl, + getGlobalLitellmHeaderName, + deriveErrorMessage, + handleError, +} from "@/components/networking"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import { keyKeys } from "./useKeys"; + +// ── Types ───────────────────────────────────────────────────────────────────── + +export interface ResetKeySpendResponse { + key_hash: string; + spend: number; + previous_spend: number; + max_budget: number | null; + budget_reset_at: string | null; +} + +// ── Fetch function ──────────────────────────────────────────────────────────── + +export const resetKeySpend = async ( + accessToken: string, + keyToken: string, +): Promise => { + const baseUrl = getProxyBaseUrl(); + const url = `${baseUrl ? `${baseUrl}/key/${keyToken}/reset_spend` : `/key/${keyToken}/reset_spend`}`; + + const response = await fetch(url, { + method: "POST", + headers: { + [getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ reset_to: 0 }), + }); + + if (!response.ok) { + const errorData = await response.json(); + const errorMessage = deriveErrorMessage(errorData); + handleError(errorMessage); + throw new Error(errorMessage); + } + + return response.json(); +}; + +// ── Hook ────────────────────────────────────────────────────────────────────── + +export const useResetKeySpend = () => { + const { accessToken } = useAuthorized(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (keyToken) => { + if (!accessToken) { + throw new Error("Access token is required"); + } + return resetKeySpend(accessToken, keyToken); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: keyKeys.all }); + }, + }); +}; diff --git a/ui/litellm-dashboard/src/components/templates/KeyInfoHeader.tsx b/ui/litellm-dashboard/src/components/templates/KeyInfoHeader.tsx index 1befd657843..df3678afab6 100644 --- a/ui/litellm-dashboard/src/components/templates/KeyInfoHeader.tsx +++ b/ui/litellm-dashboard/src/components/templates/KeyInfoHeader.tsx @@ -11,6 +11,7 @@ import { ClockCircleOutlined, ThunderboltOutlined, SafetyCertificateOutlined, + DollarOutlined, } from "@ant-design/icons"; import LabeledField from "../common_components/LabeledField"; @@ -33,6 +34,7 @@ interface KeyInfoHeaderProps { onCreateNew?: () => void; onRegenerate?: () => void; onDelete?: () => void; + onResetSpend?: () => void; canModifyKey?: boolean; backButtonText?: string; regenerateDisabled?: boolean; @@ -45,6 +47,7 @@ export function KeyInfoHeader({ onCreateNew, onRegenerate, onDelete, + onResetSpend, canModifyKey = true, backButtonText = "Back to Keys", regenerateDisabled = false, @@ -77,6 +80,11 @@ export function KeyInfoHeader({ {canModifyKey && ( + {onResetSpend && ( + + )} )} diff --git a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx index 35a75b2f2fe..b77b13b1591 100644 --- a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx @@ -450,8 +450,8 @@ export default function KeyInfoView({ $0?

- Current spend: ${formatNumberWithCommas(currentKeyData.spend, 4)}. The key will be - immediately unblocked and able to make requests again. + Current spend: ${formatNumberWithCommas(currentKeyData.spend, 4)}. Spend history is + preserved in logs. This resets the current period spend counter, the same as an automatic budget reset.

From 5b2110ddb56492c6b8876c1e5cf3608d754d3820 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 3 Mar 2026 15:16:47 -0800 Subject: [PATCH 3/3] Polish Reset Spend button and modal - Move Reset Spend button after Regenerate Key in header - Make modal OK button danger style with text "Reset" Co-Authored-By: Claude Sonnet 4.6 --- .../src/components/templates/KeyInfoHeader.tsx | 10 +++++----- .../src/components/templates/key_info_view.test.tsx | 4 ++-- .../src/components/templates/key_info_view.tsx | 3 ++- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/ui/litellm-dashboard/src/components/templates/KeyInfoHeader.tsx b/ui/litellm-dashboard/src/components/templates/KeyInfoHeader.tsx index c750b1b4ac7..93ebae9c4be 100644 --- a/ui/litellm-dashboard/src/components/templates/KeyInfoHeader.tsx +++ b/ui/litellm-dashboard/src/components/templates/KeyInfoHeader.tsx @@ -80,11 +80,6 @@ export function KeyInfoHeader({ {canModifyKey && ( - {onResetSpend && ( - - )} + {onResetSpend && ( + + )} diff --git a/ui/litellm-dashboard/src/components/templates/key_info_view.test.tsx b/ui/litellm-dashboard/src/components/templates/key_info_view.test.tsx index c19866e5d84..f269ad96a27 100644 --- a/ui/litellm-dashboard/src/components/templates/key_info_view.test.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_info_view.test.tsx @@ -642,7 +642,7 @@ describe("KeyInfoView", () => { await waitFor(() => { expect(screen.getByText("Reset Key Spend")).toBeInTheDocument(); - expect(screen.getByText(/reset to \$0/i)).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /^reset$/i })).toBeInTheDocument(); }); }); @@ -670,7 +670,7 @@ describe("KeyInfoView", () => { }); // Click the confirm button in the modal - await userEvent.click(screen.getByRole("button", { name: /reset to \$0/i })); + await userEvent.click(screen.getByRole("button", { name: /^reset$/i })); await waitFor(() => { expect(mockResetKeySpendMutate).toHaveBeenCalledWith( diff --git a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx index b77b13b1591..6733cd6a595 100644 --- a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx @@ -442,7 +442,8 @@ export default function KeyInfoView({ open={isResetSpendModalOpen} onOk={handleResetSpend} onCancel={() => setIsResetSpendModalOpen(false)} - okText="Reset to $0" + okText="Reset" + okButtonProps={{ danger: true }} confirmLoading={resetSpendLoading} >