+ {openAlerts.length > 0 && (
+ <>
+
+
+
Model migrations across your keys and teams.
+
+ {openAlerts.slice(0, 3).map((alert) => (
+
+
+
+
+ ))}
+
+
Announcements
+ >
+ )}
+ {deprecationAlerts && openAlerts.length === 0 && (
+
+ View model deprecations
+
+ )}
+
{announcements.map((announcement, index) => (
{index > 0 ? : null}
@@ -162,10 +209,12 @@ export const NotificationsBell: React.FC<{ accessToken: string | null; deploymen
}) => {
const { data: healthData } = useHealthReadinessDetails(accessToken);
const { data: latestRelease } = useLatestReleaseInfo(accessToken);
+ const { data: deprecationAlerts } = useDeprecationAlerts(accessToken);
return (
);
diff --git a/ui/litellm-dashboard/src/components/leftnav.test.tsx b/ui/litellm-dashboard/src/components/leftnav.test.tsx
index 1c44d4dae6d..bab51aab121 100644
--- a/ui/litellm-dashboard/src/components/leftnav.test.tsx
+++ b/ui/litellm-dashboard/src/components/leftnav.test.tsx
@@ -693,6 +693,7 @@ describe("Sidebar (leftnav)", () => {
expectHref("Playground", "/ui/playground");
expectHref("Models + Endpoints", "/ui/models-and-endpoints");
expectHref("Usage", "/ui/usage");
+ expectHref("Alerts", "/ui/alerts");
expectHref("API Reference", "/ui/api-reference");
});
diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx
index 488113d726e..33113b4ad6f 100644
--- a/ui/litellm-dashboard/src/components/leftnav.tsx
+++ b/ui/litellm-dashboard/src/components/leftnav.tsx
@@ -299,6 +299,13 @@ const menuGroups: MenuGroup[] = [
roles: [...all_admin_roles, ...internalUserRoles],
label: "Usage",
},
+ {
+ key: "alerts",
+ page: "alerts",
+ label: "Alerts",
+ icon: ,
+ roles: rolesWithCapability("viewGlobalSpend"),
+ },
{
key: "model-insights",
page: "model-insights",
diff --git a/ui/litellm-dashboard/src/components/model_info_view.integration.test.tsx b/ui/litellm-dashboard/src/components/model_info_view.integration.test.tsx
index 82221826a68..a42db06d251 100644
--- a/ui/litellm-dashboard/src/components/model_info_view.integration.test.tsx
+++ b/ui/litellm-dashboard/src/components/model_info_view.integration.test.tsx
@@ -1,10 +1,9 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
-import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import React, { ReactNode } from "react";
-import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { beforeEach, describe, expect, it, vi } from "vitest";
import ModelInfoView from "./model_info_view";
-import ModelDeprecationBanner from "@/app/(dashboard)/models-and-endpoints/components/ModelDeprecationBanner";
import { toast } from "@/lib/toast";
import * as networking from "./networking";
import { useUserLookup } from "@/app/(dashboard)/hooks/users/useUsers";
@@ -46,10 +45,6 @@ vi.mock("@/app/(dashboard)/hooks/models/useModelCostMap", () => ({
useModelCostMap: (...args: any[]) => mockUseModelCostMap(...args),
}));
-vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
- default: () => ({ accessToken: "test-token", userRole: "Admin" }),
-}));
-
const mockUseTeams = vi.fn();
vi.mock("@/app/(dashboard)/hooks/teams/useTeams", () => ({
useTeams: () => mockUseTeams(),
@@ -82,8 +77,6 @@ const mockModelDeleteCall = vi.mocked(networking.modelDeleteCall);
const mockCredentialCreateCall = vi.mocked(networking.credentialCreateCall);
const mockVectorStoreListCall = vi.mocked(networking.vectorStoreListCall);
-const TEST_TODAY = new Date(2026, 10, 4);
-
describe("ModelInfoView", () => {
let queryClient: QueryClient;
@@ -202,10 +195,6 @@ describe("ModelInfoView", () => {
mockCredentialCreateCall.mockResolvedValue({});
});
- afterEach(() => {
- vi.unstubAllGlobals();
- });
-
const wrapper = ({ children }: { children: ReactNode }) =>
React.createElement(QueryClientProvider, { client: queryClient }, children);
@@ -216,70 +205,6 @@ describe("ModelInfoView", () => {
});
});
- it("refreshes deprecation banner data after updating model credentials", async () => {
- const deprecatedResponse = {
- checked_at: "2026-06-01T00:00:00Z",
- warn_within_days: 30,
- deprecated: [
- {
- model_name: "old-model",
- litellm_provider: null,
- deprecation_date: "2026-06-01",
- days_until_deprecation: -1,
- status: "deprecated",
- successor_model: null,
- },
- ],
- imminent: [],
- upcoming: [],
- };
- const currentResponse = {
- checked_at: "2026-06-02T00:00:00Z",
- warn_within_days: 30,
- deprecated: [],
- imminent: [
- {
- model_name: "current-model",
- litellm_provider: null,
- deprecation_date: "2026-11-30",
- days_until_deprecation: 26,
- status: "imminent",
- successor_model: null,
- },
- ],
- upcoming: [],
- };
- const fetchMock = vi
- .fn()
- .mockResolvedValueOnce(Response.json(deprecatedResponse))
- .mockResolvedValueOnce(Response.json(currentResponse));
- vi.stubGlobal("fetch", fetchMock);
-
- render(
- <>
-
-
- >,
- { wrapper },
- );
-
- expect(await screen.findByText("old-model was deprecated on 2026-06-01")).toBeInTheDocument();
- await userEvent.click(screen.getByTestId("update-api-key-button"));
- const dialog = screen.getByRole("dialog");
- fireEvent.change(within(dialog).getByPlaceholderText("Enter the new API key"), {
- target: { value: "new-api-key" },
- });
- await userEvent.click(within(dialog).getByRole("button", { name: "Update API Key" }));
-
- expect(await screen.findByText("current-model deprecates on 2026-11-30, in 26 days")).toBeInTheDocument();
- await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(2));
- expect(
- fetchMock.mock.calls.map(
- ([input]) => new URL(input instanceof Request ? input.url : String(input), "http://localhost").pathname,
- ),
- ).toEqual(["/model/deprecations", "/model/deprecations"]);
- });
-
it("shows Kubernetes pods for discovery-enabled models", async () => {
mockUseModelsInfo.mockReturnValue({
data: {
diff --git a/ui/litellm-dashboard/src/components/model_info_view.tsx b/ui/litellm-dashboard/src/components/model_info_view.tsx
index e0498fd1ea9..efa7caf550d 100644
--- a/ui/litellm-dashboard/src/components/model_info_view.tsx
+++ b/ui/litellm-dashboard/src/components/model_info_view.tsx
@@ -1,6 +1,5 @@
import { useModelCostMap } from "@/app/(dashboard)/hooks/models/useModelCostMap";
import { useModelHub, useModelsInfo } from "@/app/(dashboard)/hooks/models/useModels";
-import { invalidateModelQueries } from "@/app/(dashboard)/hooks/models/invalidateModelQueries";
import { useQueryClient } from "@tanstack/react-query";
import { transformModelData } from "@/app/(dashboard)/models-and-endpoints/utils/modelDataTransformer";
import { KeyIcon, RefreshIcon, TrashIcon } from "@heroicons/react/outline";
@@ -825,7 +824,7 @@ export default function ModelInfoView({
accessToken={accessToken}
modelId={modelId}
onUpdated={() => {
- void invalidateModelQueries(queryClient);
+ queryClient.invalidateQueries({ queryKey: ["models", "list"] });
}}
/>
)}
diff --git a/ui/litellm-dashboard/src/components/page_metadata.ts b/ui/litellm-dashboard/src/components/page_metadata.ts
index 97220a0ef76..80c3a9d2ed7 100644
--- a/ui/litellm-dashboard/src/components/page_metadata.ts
+++ b/ui/litellm-dashboard/src/components/page_metadata.ts
@@ -20,6 +20,7 @@ export const pageDescriptions: Record = {
"tool-policies": "Configure tool use policies and permissions",
"vector-stores": "Manage vector databases for embeddings",
new_usage: "View usage analytics and metrics",
+ alerts: "Review and resolve model deprecation alerts across keys and teams",
"cost-optimization": "Track and configure cost-saving features: prompt compression, caching, and auto routing",
"model-insights": "Model Leaderboard: compare usage, spend, tokens, and task mix across this gateway",
logs: "Access request and response logs",
diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts
index a2073bb00d8..1f12db1c063 100644
--- a/ui/litellm-dashboard/src/lib/http/schema.d.ts
+++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts
@@ -944,6 +944,44 @@ export interface paths {
patch?: never;
trace?: never;
};
+ "/alerting/model-deprecations/state": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Get Deprecation Alert State */
+ get: operations["get_deprecation_alert_state_alerting_model_deprecations_state_get"];
+ /** Update Deprecation Alert State */
+ put: operations["update_deprecation_alert_state_alerting_model_deprecations_state_put"];
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/alerting/model-deprecations/tags": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Get Deprecation Usage Tags
+ * @description Distinct tags observed with this model for a page of affected reporting key IDs.
+ */
+ get: operations["get_deprecation_usage_tags_alerting_model_deprecations_tags_get"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/alerting/settings": {
parameters: {
query?: never;
@@ -32376,6 +32414,59 @@ export interface components {
/** Warnings */
warnings: components["schemas"]["DeploymentWarning"][];
};
+ /** DeprecationAlertState */
+ DeprecationAlertState: {
+ /**
+ * Deprecation Date
+ * Format: date
+ */
+ deprecation_date: string;
+ /** Last Seen Date */
+ last_seen_date?: string | null;
+ /**
+ * Last Seen Requests
+ * @default 0
+ */
+ last_seen_requests: number;
+ /** Model */
+ model: string;
+ /** @default open */
+ status: components["schemas"]["DeprecationAlertStatus"];
+ /** Updated At */
+ updated_at?: string | null;
+ /** Updated By */
+ updated_by?: string | null;
+ };
+ /**
+ * DeprecationAlertStatus
+ * @enum {string}
+ */
+ DeprecationAlertStatus: "open" | "resolved" | "dismissed";
+ /** DeprecationAlertUpdate */
+ DeprecationAlertUpdate: {
+ /**
+ * Deprecation Date
+ * Format: date
+ */
+ deprecation_date: string;
+ /**
+ * Last Seen Date
+ * Format: date
+ */
+ last_seen_date: string;
+ /** Last Seen Requests */
+ last_seen_requests: number;
+ /** Model */
+ model: string;
+ status: components["schemas"]["DeprecationAlertStatus"];
+ };
+ /** DeprecationTagUsage */
+ DeprecationTagUsage: {
+ /** Api Key */
+ api_key: string;
+ /** Tag */
+ tag: string;
+ };
/** DiscoverAgentRequest */
DiscoverAgentRequest: {
/**
@@ -53157,6 +53248,105 @@ export interface operations {
};
};
};
+ get_deprecation_alert_state_alerting_model_deprecations_state_get: {
+ parameters: {
+ query: {
+ model: string;
+ deprecation_date: string;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["DeprecationAlertState"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
+ update_deprecation_alert_state_alerting_model_deprecations_state_put: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["DeprecationAlertUpdate"];
+ };
+ };
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["DeprecationAlertState"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
+ get_deprecation_usage_tags_alerting_model_deprecations_tags_get: {
+ parameters: {
+ query: {
+ model: string;
+ api_keys: string[];
+ start_date: string;
+ end_date: string;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["DeprecationTagUsage"][];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
alerting_settings_alerting_settings_get: {
parameters: {
query?: never;