diff --git a/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.41-py3-none-any.whl b/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.41-py3-none-any.whl
new file mode 100644
index 00000000000..9d7fdb78f72
Binary files /dev/null and b/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.41-py3-none-any.whl differ
diff --git a/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.41.tar.gz b/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.41.tar.gz
new file mode 100644
index 00000000000..a478356f886
Binary files /dev/null and b/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.41.tar.gz differ
diff --git a/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.43-py3-none-any.whl b/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.43-py3-none-any.whl
new file mode 100644
index 00000000000..ee821fed313
Binary files /dev/null and b/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.43-py3-none-any.whl differ
diff --git a/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.43.tar.gz b/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.43.tar.gz
new file mode 100644
index 00000000000..d0304bd9825
Binary files /dev/null and b/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.43.tar.gz differ
diff --git a/litellm-proxy-extras/litellm_proxy_extras/migrations/20260218231534_add_last_active_to_key_table/migration.sql b/litellm-proxy-extras/litellm_proxy_extras/migrations/20260218231534_add_last_active_to_key_table/migration.sql
new file mode 100644
index 00000000000..ded1856059b
--- /dev/null
+++ b/litellm-proxy-extras/litellm_proxy_extras/migrations/20260218231534_add_last_active_to_key_table/migration.sql
@@ -0,0 +1,6 @@
+-- AlterTable
+ALTER TABLE "LiteLLM_DeletedVerificationToken" ADD COLUMN "last_active" TIMESTAMP(3);
+
+-- AlterTable
+ALTER TABLE "LiteLLM_VerificationToken" ADD COLUMN "last_active" TIMESTAMP(3);
+
diff --git a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma
index 84d6f3a391f..d1695e21ff0 100644
--- a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma
+++ b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma
@@ -384,6 +384,7 @@ model LiteLLM_VerificationToken {
created_by String?
updated_at DateTime? @default(now()) @updatedAt @map("updated_at")
updated_by String?
+ last_active DateTime? // When this key was last used
rotation_count Int? @default(0) // Number of times key has been rotated
auto_rotate Boolean? @default(false) // Whether this key should be auto-rotated
rotation_interval String? // How often to rotate (e.g., "30d", "90d")
@@ -455,6 +456,7 @@ model LiteLLM_DeletedVerificationToken {
created_by String? // Original creator
updated_at DateTime? // Last update timestamp before deletion
updated_by String? // Last user who updated before deletion
+ last_active DateTime? // When this key was last used before deletion
rotation_count Int? @default(0)
auto_rotate Boolean? @default(false)
rotation_interval String?
diff --git a/litellm-proxy-extras/pyproject.toml b/litellm-proxy-extras/pyproject.toml
index 5c8ade56400..28179d7656a 100644
--- a/litellm-proxy-extras/pyproject.toml
+++ b/litellm-proxy-extras/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "litellm-proxy-extras"
-version = "0.4.42"
+version = "0.4.43"
description = "Additional files for the LiteLLM Proxy. Reduces the size of the main litellm package."
authors = ["BerriAI"]
readme = "README.md"
@@ -22,7 +22,7 @@ requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.commitizen]
-version = "0.4.42"
+version = "0.4.43"
version_files = [
"pyproject.toml:version",
"../requirements.txt:litellm-proxy-extras==",
diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py
index 9a208d48392..231b9fdd1f6 100644
--- a/litellm/proxy/_types.py
+++ b/litellm/proxy/_types.py
@@ -2206,6 +2206,7 @@ class LiteLLM_VerificationToken(LiteLLMPydanticObjectBase):
created_by: Optional[str] = None
updated_at: Optional[datetime] = None
updated_by: Optional[str] = None
+ last_active: Optional[datetime] = None
object_permission_id: Optional[str] = None
object_permission: Optional[LiteLLM_ObjectPermissionTable] = None
access_group_ids: Optional[List[str]] = None
diff --git a/litellm/proxy/db/db_spend_update_writer.py b/litellm/proxy/db/db_spend_update_writer.py
index 9675b82b145..03628fda47f 100644
--- a/litellm/proxy/db/db_spend_update_writer.py
+++ b/litellm/proxy/db/db_spend_update_writer.py
@@ -12,7 +12,7 @@ import os
import random
import time
import traceback
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union, cast, overload
import litellm
@@ -792,7 +792,10 @@ class DBSpendUpdateWriter:
) in key_list_transactions.items():
batcher.litellm_verificationtoken.update_many( # 'update_many' prevents error from being raised if no row exists
where={"token": token},
- data={"spend": {"increment": response_cost}},
+ data={
+ "spend": {"increment": response_cost},
+ "last_active": datetime.now(timezone.utc),
+ },
)
break
except DB_CONNECTION_ERROR_TYPES as e:
diff --git a/litellm/proxy/schema.prisma b/litellm/proxy/schema.prisma
index 67395b8be72..b646db013c5 100644
--- a/litellm/proxy/schema.prisma
+++ b/litellm/proxy/schema.prisma
@@ -337,6 +337,7 @@ model LiteLLM_VerificationToken {
created_by String?
updated_at DateTime? @default(now()) @updatedAt @map("updated_at")
updated_by String?
+ last_active DateTime? // When this key was last used
rotation_count Int? @default(0) // Number of times key has been rotated
auto_rotate Boolean? @default(false) // Whether this key should be auto-rotated
rotation_interval String? // How often to rotate (e.g., "30d", "90d")
@@ -408,6 +409,7 @@ model LiteLLM_DeletedVerificationToken {
created_by String? // Original creator
updated_at DateTime? // Last update timestamp before deletion
updated_by String? // Last user who updated before deletion
+ last_active DateTime? // When this key was last used before deletion
rotation_count Int? @default(0)
auto_rotate Boolean? @default(false)
rotation_interval String?
diff --git a/pyproject.toml b/pyproject.toml
index f2b8a8019f0..b707d1cae33 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -61,7 +61,11 @@ boto3 = { version = "1.40.76", optional = true }
redisvl = {version = "^0.4.1", optional = true, markers = "python_version >= '3.9' and python_version < '3.14'"}
mcp = {version = ">=1.25.0,<2.0.0", optional = true, python = ">=3.10"}
a2a-sdk = {version = "^0.3.22", optional = true, python = ">=3.10"}
-litellm-proxy-extras = {version = "0.4.42", optional = true}
+<<<<<<< HEAD
+litellm-proxy-extras = {version = "0.4.41", optional = true}
+=======
+litellm-proxy-extras = {version = "0.4.43", optional = true}
+>>>>>>> origin
rich = {version = "13.7.1", optional = true}
litellm-enterprise = {version = "0.1.32", optional = true}
diskcache = {version = "^5.6.1", optional = true}
diff --git a/requirements.txt b/requirements.txt
index 3cf902b5320..c7b9a203b21 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -55,7 +55,7 @@ grpcio>=1.75.0; python_version >= "3.14"
sentry_sdk==2.21.0 # for sentry error handling
detect-secrets==1.5.0 # Enterprise - secret detection / masking in LLM requests
tzdata==2025.1 # IANA time zone database
-litellm-proxy-extras==0.4.42 # for proxy extras - e.g. prisma migrations
+litellm-proxy-extras==0.4.43 # for proxy extras - e.g. prisma migrations
llm-sandbox==0.3.31 # for skill execution in sandbox
### LITELLM PACKAGE DEPENDENCIES
python-dotenv==1.0.1 # for env
diff --git a/schema.prisma b/schema.prisma
index 67395b8be72..b646db013c5 100644
--- a/schema.prisma
+++ b/schema.prisma
@@ -337,6 +337,7 @@ model LiteLLM_VerificationToken {
created_by String?
updated_at DateTime? @default(now()) @updatedAt @map("updated_at")
updated_by String?
+ last_active DateTime? // When this key was last used
rotation_count Int? @default(0) // Number of times key has been rotated
auto_rotate Boolean? @default(false) // Whether this key should be auto-rotated
rotation_interval String? // How often to rotate (e.g., "30d", "90d")
@@ -408,6 +409,7 @@ model LiteLLM_DeletedVerificationToken {
created_by String? // Original creator
updated_at DateTime? // Last update timestamp before deletion
updated_by String? // Last user who updated before deletion
+ last_active DateTime? // When this key was last used before deletion
rotation_count Int? @default(0)
auto_rotate Boolean? @default(false)
rotation_interval String?
diff --git a/tests/test_litellm/proxy/db/test_db_spend_update_writer.py b/tests/test_litellm/proxy/db/test_db_spend_update_writer.py
index 1dd5cba2c4b..0fa0d4cf10b 100644
--- a/tests/test_litellm/proxy/db/test_db_spend_update_writer.py
+++ b/tests/test_litellm/proxy/db/test_db_spend_update_writer.py
@@ -7,7 +7,7 @@ sys.path.insert(
) # Adds the parent directory to the system path
-from datetime import datetime
+from datetime import datetime, timezone
from unittest.mock import AsyncMock, MagicMock, patch, call
import pytest
@@ -1000,3 +1000,79 @@ async def test_update_daily_spend_re_raises_exception_after_logging():
table_name="litellm_dailyuserspend",
unique_constraint_name="user_id_date_api_key_model_custom_llm_provider_mcp_namespaced_tool_name_endpoint",
)
+
+
+@pytest.mark.asyncio
+async def test_commit_key_spend_updates_includes_last_active():
+ """
+ Test that _commit_spend_updates_to_db sets last_active alongside spend
+ when updating the key table.
+ """
+ db_writer = DBSpendUpdateWriter()
+
+ # Create mock prisma client with transaction support
+ mock_batcher = MagicMock()
+ mock_batcher.litellm_verificationtoken = MagicMock()
+ mock_batcher.litellm_verificationtoken.update_many = MagicMock()
+
+ mock_transaction = AsyncMock()
+ mock_transaction.__aenter__ = AsyncMock(return_value=mock_transaction)
+ mock_transaction.__aexit__ = AsyncMock(return_value=False)
+ mock_transaction.batch_ = MagicMock(return_value=AsyncMock(
+ __aenter__=AsyncMock(return_value=mock_batcher),
+ __aexit__=AsyncMock(return_value=False),
+ ))
+
+ mock_prisma_client = MagicMock()
+ mock_prisma_client.db = MagicMock()
+ mock_prisma_client.db.tx = MagicMock(return_value=mock_transaction)
+
+ # Also mock the other table batchers to avoid errors
+ mock_batcher.litellm_usertable = MagicMock()
+ mock_batcher.litellm_usertable.update_many = MagicMock()
+ mock_batcher.litellm_teamtable = MagicMock()
+ mock_batcher.litellm_teamtable.update_many = MagicMock()
+ mock_batcher.litellm_organizationtable = MagicMock()
+ mock_batcher.litellm_organizationtable.update_many = MagicMock()
+
+ mock_proxy_logging = MagicMock()
+
+ db_spend_update_transactions = {
+ "user_list_transactions": {},
+ "end_user_list_transactions": {},
+ "key_list_transactions": {"hashed_token_abc": 0.05},
+ "team_list_transactions": {},
+ "team_member_list_transactions": {},
+ "org_list_transactions": {},
+ "tag_list_transactions": {},
+ }
+
+ before_call = datetime.now(timezone.utc)
+
+ with patch(
+ "litellm.proxy.utils._raise_failed_update_spend_exception"
+ ):
+ await db_writer._commit_spend_updates_to_db(
+ prisma_client=mock_prisma_client,
+ n_retry_times=0,
+ proxy_logging_obj=mock_proxy_logging,
+ db_spend_update_transactions=db_spend_update_transactions,
+ )
+
+ after_call = datetime.now(timezone.utc)
+
+ # Verify update_many was called on the key table
+ mock_batcher.litellm_verificationtoken.update_many.assert_called_once()
+ call_kwargs = mock_batcher.litellm_verificationtoken.update_many.call_args[1]
+
+ # Verify the where clause targets the correct token
+ assert call_kwargs["where"] == {"token": "hashed_token_abc"}
+
+ # Verify data includes both spend increment and last_active
+ assert call_kwargs["data"]["spend"] == {"increment": 0.05}
+ assert "last_active" in call_kwargs["data"]
+
+ # Verify last_active is a datetime within the expected range
+ last_active = call_kwargs["data"]["last_active"]
+ assert isinstance(last_active, datetime)
+ assert before_call <= last_active <= after_call
diff --git a/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx
index 749396c82f3..9dc468c6a9f 100644
--- a/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx
+++ b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx
@@ -99,6 +99,7 @@ const mockKey: KeyResponse = {
created_at: "2024-11-01T10:00:00Z",
created_by: "user-1",
updated_at: "2024-11-15T10:00:00Z",
+ last_active: "2024-11-20T14:30:00Z",
team_spend: 5.5,
team_alias: "Test Team",
team_tpm_limit: 5000,
@@ -625,3 +626,78 @@ it("should render table without crashing when models is undefined", async () =>
expect(screen.getByText("Test Key Alias")).toBeInTheDocument();
});
});
+
+it("should render Last Active column header with info icon", () => {
+ const mockProps = {
+ teams: [mockTeam],
+ organizations: [mockOrganization],
+ onSortChange: vi.fn(),
+ currentSort: {
+ sortBy: "created_at",
+ sortOrder: "desc" as const,
+ },
+ };
+
+ renderWithProviders();
+
+ expect(screen.getByText("Last Active")).toBeInTheDocument();
+});
+
+it("should display formatted date for last_active when value exists", async () => {
+ const mockProps = {
+ teams: [mockTeam],
+ organizations: [mockOrganization],
+ onSortChange: vi.fn(),
+ currentSort: {
+ sortBy: "created_at",
+ sortOrder: "desc" as const,
+ },
+ };
+
+ renderWithProviders();
+
+ await waitFor(() => {
+ const expectedDate = new Date("2024-11-20T14:30:00Z").toLocaleDateString();
+ expect(screen.getByText(expectedDate)).toBeInTheDocument();
+ });
+});
+
+it("should display 'Unknown' for last_active when value is null", async () => {
+ const keyWithNullLastActive = {
+ ...mockKey,
+ last_active: null,
+ };
+
+ mockUseFilterLogic.mockReturnValue({
+ filters: {
+ "Team ID": "",
+ "Organization ID": "",
+ "Key Alias": "",
+ "User ID": "",
+ "Sort By": "created_at",
+ "Sort Order": "desc",
+ },
+ filteredKeys: [keyWithNullLastActive],
+ allKeyAliases: ["test-key-alias"],
+ allTeams: [mockTeam],
+ allOrganizations: [mockOrganization],
+ handleFilterChange: vi.fn(),
+ handleFilterReset: vi.fn(),
+ });
+
+ const mockProps = {
+ teams: [mockTeam],
+ organizations: [mockOrganization],
+ onSortChange: vi.fn(),
+ currentSort: {
+ sortBy: "created_at",
+ sortOrder: "desc" as const,
+ },
+ };
+
+ renderWithProviders();
+
+ await waitFor(() => {
+ expect(screen.getByText("Unknown")).toBeInTheDocument();
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx
index f7c47943e7e..ff996a0434a 100644
--- a/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx
+++ b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx
@@ -24,8 +24,9 @@ import {
TableRow,
Text,
} from "@tremor/react";
-import { Skeleton, Tooltip } from "antd";
-import React, { useEffect, useState } from "react";
+import { InfoCircleOutlined } from "@ant-design/icons";
+import { Popover, Skeleton, Tooltip } from "antd";
+import React, { useEffect, useMemo, useState } from "react";
import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key";
import { useFilterLogic } from "../key_team_helpers/filter_logic";
import { KeyResponse, Team } from "../key_team_helpers/key_list";
@@ -112,7 +113,7 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo
}
}, [refetch]);
- const columns: ColumnDef[] = [
+ const columns: ColumnDef[] = useMemo(() => [
{
id: "expander",
header: () => null,
@@ -292,6 +293,33 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo
return value ? new Date(value as string).toLocaleDateString() : "Never";
},
},
+ {
+ id: "last_active",
+ accessorKey: "last_active",
+ header: () => (
+
+ Last Active
+
+
+
+
+ ),
+ size: 130,
+ enableSorting: false,
+ cell: (info) => {
+ const value = info.getValue();
+ if (!value) return "Unknown";
+ const date = new Date(value as string);
+ return (
+
+ {date.toLocaleDateString()}
+
+ );
+ },
+ },
{
id: "expires",
accessorKey: "expires",
@@ -437,7 +465,7 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo
);
},
},
- ];
+ ], []);
const filterOptions: FilterOption[] = [
{
diff --git a/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx b/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx
index b54eb21a0ae..5512809ba3f 100644
--- a/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx
+++ b/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx
@@ -48,6 +48,7 @@ export interface KeyResponse {
organization_id: string | null;
created_at: string;
updated_at: string;
+ last_active: string | null;
team_spend: number;
team_alias: string;
team_tpm_limit: number;