mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
fix(proxy): add config_updated_at audit timestamp for virtual keys (#36488)
* fix(proxy): add config_updated_at audit timestamp for virtual keys updated_at carries Prisma's @updatedAt, so every batched spend flush rewrites it and it cannot distinguish config changes from usage. Add an additive config_updated_at column stamped only by key management writes (update, bulk update, regenerate, block, unblock) via a shared helper, expose it on key responses, and switch the key page's Last Updated to it with a created_at fallback. * test(proxy): assert config_updated_at survives key archival * refactor(proxy): rename config_updated_at to settings_updated_at
This commit is contained in:
parent
657c4c2b1e
commit
b144b15d48
15 changed files with 269 additions and 14 deletions
|
|
@ -0,0 +1,5 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "LiteLLM_VerificationToken" ADD COLUMN IF NOT EXISTS "settings_updated_at" TIMESTAMP(3);
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "LiteLLM_DeletedVerificationToken" ADD COLUMN IF NOT EXISTS "settings_updated_at" TIMESTAMP(3);
|
||||
|
|
@ -452,6 +452,7 @@ model LiteLLM_VerificationToken {
|
|||
created_by String?
|
||||
updated_at DateTime? @default(now()) @updatedAt @map("updated_at")
|
||||
updated_by String?
|
||||
settings_updated_at DateTime? @map("settings_updated_at")
|
||||
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
|
||||
|
|
@ -548,6 +549,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
|
||||
settings_updated_at DateTime? // Last configuration change before deletion
|
||||
last_active DateTime? // When this key was last used before deletion
|
||||
rotation_count Int? @default(0)
|
||||
auto_rotate Boolean? @default(false)
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ class LiteLLM_VerificationToken(LiteLLMPydanticObjectBase):
|
|||
created_by: str | None = None
|
||||
updated_at: datetime | None = None
|
||||
updated_by: str | None = None
|
||||
settings_updated_at: datetime | None = None
|
||||
last_active: datetime | None = None
|
||||
object_permission_id: str | None = None
|
||||
object_permission: LiteLLM_ObjectPermissionTable | None = None
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ from litellm.proxy.management_endpoints.common_utils import (
|
|||
from litellm.proxy.management_endpoints.model_management_endpoints import (
|
||||
_add_model_to_db,
|
||||
)
|
||||
from litellm.proxy.management_helpers.key_settings_audit import with_settings_updated_at
|
||||
from litellm.proxy.management_helpers.object_permission_utils import (
|
||||
_set_object_permission,
|
||||
attach_object_permission_to_dict,
|
||||
|
|
@ -4693,7 +4694,7 @@ async def _execute_virtual_key_regeneration(
|
|||
|
||||
updated_token: Final = await VerificationTokenRepository(prisma_client).table.update(
|
||||
where={"token": hashed_api_key},
|
||||
data=jsonified_update_data,
|
||||
data=with_settings_updated_at(jsonified_update_data),
|
||||
)
|
||||
updated_token_dict: Final[dict[str, object]] = dict(updated_token) if updated_token is not None else {}
|
||||
updated_token_dict["key"] = new_token
|
||||
|
|
@ -6203,7 +6204,7 @@ async def block_key(
|
|||
|
||||
record: Final = await _prisma_table(VerificationTokenRepository(prisma_client)).update(
|
||||
where={"token": hashed_token},
|
||||
data={"blocked": True},
|
||||
data=with_settings_updated_at({"blocked": True}),
|
||||
)
|
||||
|
||||
## UPDATE KEY CACHE - invalidate so next read re-fetches from DB
|
||||
|
|
@ -6316,7 +6317,7 @@ async def unblock_key(
|
|||
|
||||
record: Final = await _prisma_table(VerificationTokenRepository(prisma_client)).update(
|
||||
where={"token": hashed_token},
|
||||
data={"blocked": False},
|
||||
data=with_settings_updated_at({"blocked": False}),
|
||||
)
|
||||
|
||||
## UPDATE KEY CACHE - invalidate so next read re-fetches from DB
|
||||
|
|
|
|||
14
litellm/proxy/management_helpers/key_settings_audit.py
Normal file
14
litellm/proxy/management_helpers/key_settings_audit.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
"""Audit stamping for virtual key configuration changes."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
def with_settings_updated_at(data: Mapping[str, object]) -> dict[str, object]:
|
||||
"""Stamp a key update payload with the time its configuration changed.
|
||||
|
||||
``updated_at`` carries Prisma's ``@updatedAt`` and so is rewritten by every
|
||||
spend flush, which makes it useless for auditing; ``settings_updated_at`` is
|
||||
written only from key-management write paths.
|
||||
"""
|
||||
return {**data, "settings_updated_at": datetime.now(timezone.utc)}
|
||||
|
|
@ -452,6 +452,7 @@ model LiteLLM_VerificationToken {
|
|||
created_by String?
|
||||
updated_at DateTime? @default(now()) @updatedAt @map("updated_at")
|
||||
updated_by String?
|
||||
settings_updated_at DateTime? @map("settings_updated_at")
|
||||
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
|
||||
|
|
@ -548,6 +549,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
|
||||
settings_updated_at DateTime? // Last configuration change before deletion
|
||||
last_active DateTime? // When this key was last used before deletion
|
||||
rotation_count Int? @default(0)
|
||||
auto_rotate Boolean? @default(false)
|
||||
|
|
|
|||
|
|
@ -135,6 +135,7 @@ from litellm.proxy.hooks.sensitive_data_routing import (
|
|||
_PROXY_SensitiveDataRoutingHandler,
|
||||
)
|
||||
from litellm.proxy.litellm_pre_call_utils import LiteLLMProxyRequestSetup
|
||||
from litellm.proxy.management_helpers.key_settings_audit import with_settings_updated_at
|
||||
from litellm.proxy.policy_engine.pipeline_executor import PipelineExecutor
|
||||
from litellm.repositories.budget_repository import BudgetRepository
|
||||
from litellm.repositories.config_repository import ConfigRepository
|
||||
|
|
@ -3996,7 +3997,7 @@ class PrismaClient:
|
|||
db_data["token"] = token
|
||||
response: Final = await VerificationTokenRepository(self).table.update(
|
||||
where={"token": token},
|
||||
data={**db_data},
|
||||
data=with_settings_updated_at(db_data),
|
||||
)
|
||||
verbose_proxy_logger.debug("\033[91m" + f"DB Token Table update succeeded {response}" + "\033[0m")
|
||||
_data: dict = {}
|
||||
|
|
|
|||
|
|
@ -452,6 +452,7 @@ model LiteLLM_VerificationToken {
|
|||
created_by String?
|
||||
updated_at DateTime? @default(now()) @updatedAt @map("updated_at")
|
||||
updated_by String?
|
||||
settings_updated_at DateTime? @map("settings_updated_at")
|
||||
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
|
||||
|
|
@ -548,6 +549,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
|
||||
settings_updated_at DateTime? // Last configuration change before deletion
|
||||
last_active DateTime? // When this key was last used before deletion
|
||||
rotation_count Int? @default(0)
|
||||
auto_rotate Boolean? @default(false)
|
||||
|
|
|
|||
|
|
@ -2157,3 +2157,67 @@ async def test_daily_transaction_compression_saved_tokens_zero_when_absent():
|
|||
assert transaction["compression_saved_tokens"] == 0
|
||||
assert transaction["compression_savings_spend"] == 0
|
||||
assert transaction["prompt_caching_savings_spend"] == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_commit_spend_updates_to_db_does_not_stamp_key_settings_updated_at():
|
||||
"""Spend flushes must leave settings_updated_at alone, or it decays into
|
||||
another `updated_at` and stops being an audit signal."""
|
||||
db_writer = DBSpendUpdateWriter()
|
||||
|
||||
mock_batcher = MagicMock()
|
||||
mock_batcher.litellm_verificationtoken = MagicMock()
|
||||
mock_batcher.litellm_verificationtoken.update_many = MagicMock()
|
||||
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_teammembership = MagicMock()
|
||||
mock_batcher.litellm_teammembership.update_many = MagicMock()
|
||||
mock_batcher.litellm_organizationtable = MagicMock()
|
||||
mock_batcher.litellm_organizationtable.update_many = MagicMock()
|
||||
mock_batcher.litellm_tagtable = MagicMock()
|
||||
mock_batcher.litellm_tagtable.update_many = MagicMock()
|
||||
mock_batcher.litellm_agentstable = MagicMock()
|
||||
mock_batcher.litellm_agentstable.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)
|
||||
|
||||
token = "hashed-token-abc"
|
||||
response_cost = 0.25
|
||||
db_spend_update_transactions = {
|
||||
"user_list_transactions": {},
|
||||
"end_user_list_transactions": {},
|
||||
"key_list_transactions": {token: response_cost},
|
||||
"team_list_transactions": {},
|
||||
"team_member_list_transactions": {},
|
||||
"org_list_transactions": {},
|
||||
"tag_list_transactions": {},
|
||||
"agent_list_transactions": {},
|
||||
}
|
||||
|
||||
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=MagicMock(),
|
||||
db_spend_update_transactions=db_spend_update_transactions,
|
||||
)
|
||||
|
||||
mock_batcher.litellm_verificationtoken.update_many.assert_called_once()
|
||||
call_kwargs = mock_batcher.litellm_verificationtoken.update_many.call_args[1]
|
||||
assert call_kwargs["where"] == {"token": token}
|
||||
assert set(call_kwargs["data"]) == {"spend", "last_active"}
|
||||
assert call_kwargs["data"]["spend"] == {"increment": response_cost}
|
||||
|
|
|
|||
|
|
@ -2293,9 +2293,9 @@ async def test_unblock_key_supports_both_sk_and_hashed_tokens(monkeypatch):
|
|||
)
|
||||
|
||||
# Verify that the database update was called with hashed token
|
||||
mock_prisma_client.db.litellm_verificationtoken.update.assert_called_with(
|
||||
where={"token": test_hashed_token}, data={"blocked": False}
|
||||
)
|
||||
sk_token_call = mock_prisma_client.db.litellm_verificationtoken.update.call_args.kwargs
|
||||
assert sk_token_call["where"] == {"token": test_hashed_token}
|
||||
assert sk_token_call["data"]["blocked"] is False
|
||||
|
||||
assert result == mock_key_record
|
||||
|
||||
|
|
@ -2313,9 +2313,9 @@ async def test_unblock_key_supports_both_sk_and_hashed_tokens(monkeypatch):
|
|||
)
|
||||
|
||||
# Verify that the database update was called with the same hashed token
|
||||
mock_prisma_client.db.litellm_verificationtoken.update.assert_called_with(
|
||||
where={"token": test_hashed_token}, data={"blocked": False}
|
||||
)
|
||||
hashed_token_call = mock_prisma_client.db.litellm_verificationtoken.update.call_args.kwargs
|
||||
assert hashed_token_call["where"] == {"token": test_hashed_token}
|
||||
assert hashed_token_call["data"]["blocked"] is False
|
||||
|
||||
assert result == mock_key_record
|
||||
|
||||
|
|
@ -2849,9 +2849,10 @@ async def test_block_key_existing_key_succeeds(monkeypatch):
|
|||
mock_prisma_client.db.litellm_verificationtoken.find_unique.assert_called_once_with(
|
||||
where={"token": test_hashed_token}
|
||||
)
|
||||
mock_prisma_client.db.litellm_verificationtoken.update.assert_called_once_with(
|
||||
where={"token": test_hashed_token}, data={"blocked": True}
|
||||
)
|
||||
mock_prisma_client.db.litellm_verificationtoken.update.assert_called_once()
|
||||
block_call = mock_prisma_client.db.litellm_verificationtoken.update.call_args.kwargs
|
||||
assert block_call["where"] == {"token": test_hashed_token}
|
||||
assert block_call["data"]["blocked"] is True
|
||||
assert result == mock_updated_record
|
||||
|
||||
|
||||
|
|
@ -4717,6 +4718,7 @@ def test_transform_verification_tokens_to_deleted_records():
|
|||
user_role=LitellmUserRoles.PROXY_ADMIN.value,
|
||||
)
|
||||
|
||||
config_stamp = datetime(2026, 8, 10, 12, 30, 45, tzinfo=timezone.utc)
|
||||
key1 = LiteLLM_VerificationToken(
|
||||
token="hashed-token-1",
|
||||
user_id="user-123",
|
||||
|
|
@ -4733,6 +4735,7 @@ def test_transform_verification_tokens_to_deleted_records():
|
|||
model_spend={},
|
||||
soft_budget_cooldown=False,
|
||||
allowed_routes=[],
|
||||
settings_updated_at=config_stamp,
|
||||
)
|
||||
|
||||
key2 = LiteLLM_VerificationToken(
|
||||
|
|
@ -4775,6 +4778,7 @@ def test_transform_verification_tokens_to_deleted_records():
|
|||
assert record1["token"] == "hashed-token-1"
|
||||
assert record1["user_id"] == "user-123"
|
||||
assert record1["team_id"] == "team-456"
|
||||
assert record1["settings_updated_at"] == config_stamp
|
||||
assert isinstance(record1["aliases"], str)
|
||||
assert isinstance(record1["config"], str)
|
||||
assert isinstance(record1["permissions"], str)
|
||||
|
|
@ -15817,3 +15821,91 @@ async def test_regenerate_key_output_token_estimate_lowered_rejected_for_non_adm
|
|||
|
||||
assert exc.value.status_code == 403
|
||||
assert "Only proxy admins can set" in str(exc.value.detail)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_virtual_key_regeneration_stamps_settings_updated_at():
|
||||
"""Regenerate rewrites the key's config, so it must move settings_updated_at."""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from litellm.proxy._types import RegenerateKeyRequest
|
||||
from litellm.proxy.management_endpoints.key_management_endpoints import (
|
||||
_execute_virtual_key_regeneration,
|
||||
)
|
||||
|
||||
mock_prisma_client = _make_regenerate_mock_prisma()
|
||||
|
||||
with _patch_regenerate_side_effects():
|
||||
before = datetime.now(timezone.utc)
|
||||
await _execute_virtual_key_regeneration(
|
||||
prisma_client=mock_prisma_client,
|
||||
key_in_db=_make_regenerate_existing_key(),
|
||||
hashed_api_key="abc123",
|
||||
key="abc123",
|
||||
data=RegenerateKeyRequest(max_budget=42.0),
|
||||
user_api_key_dict=_make_regenerate_user_api_key_dict(),
|
||||
litellm_changed_by=None,
|
||||
user_api_key_cache=MagicMock(),
|
||||
proxy_logging_obj=MagicMock(),
|
||||
)
|
||||
after = datetime.now(timezone.utc)
|
||||
|
||||
sent = mock_prisma_client.db.litellm_verificationtoken.update.call_args.kwargs["data"]
|
||||
assert sent["max_budget"] == 42.0
|
||||
assert before <= sent["settings_updated_at"] <= after
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_block_key_stamps_settings_updated_at(monkeypatch):
|
||||
"""Blocking a key is a config change, not spend activity."""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from litellm.proxy._types import BlockKeyRequest
|
||||
from litellm.proxy.management_endpoints.key_management_endpoints import block_key
|
||||
|
||||
mock_prisma_client, _ = _setup_block_unblock_mocks(monkeypatch)
|
||||
|
||||
before = datetime.now(timezone.utc)
|
||||
await block_key(
|
||||
data=BlockKeyRequest(key="sk-test123456789"),
|
||||
http_request=MagicMock(),
|
||||
user_api_key_dict=UserAPIKeyAuth(
|
||||
user_role=LitellmUserRoles.PROXY_ADMIN,
|
||||
api_key="sk-admin",
|
||||
user_id="admin_user",
|
||||
),
|
||||
litellm_changed_by=None,
|
||||
)
|
||||
after = datetime.now(timezone.utc)
|
||||
|
||||
sent = mock_prisma_client.db.litellm_verificationtoken.update.call_args.kwargs["data"]
|
||||
assert sent["blocked"] is True
|
||||
assert before <= sent["settings_updated_at"] <= after
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unblock_key_stamps_settings_updated_at(monkeypatch):
|
||||
"""Unblocking a key is a config change, not spend activity."""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from litellm.proxy._types import BlockKeyRequest
|
||||
from litellm.proxy.management_endpoints.key_management_endpoints import unblock_key
|
||||
|
||||
mock_prisma_client, _ = _setup_block_unblock_mocks(monkeypatch)
|
||||
|
||||
before = datetime.now(timezone.utc)
|
||||
await unblock_key(
|
||||
data=BlockKeyRequest(key="sk-test123456789"),
|
||||
http_request=MagicMock(),
|
||||
user_api_key_dict=UserAPIKeyAuth(
|
||||
user_role=LitellmUserRoles.PROXY_ADMIN,
|
||||
api_key="sk-admin",
|
||||
user_id="admin_user",
|
||||
),
|
||||
litellm_changed_by=None,
|
||||
)
|
||||
after = datetime.now(timezone.utc)
|
||||
|
||||
sent = mock_prisma_client.db.litellm_verificationtoken.update.call_args.kwargs["data"]
|
||||
assert sent["blocked"] is False
|
||||
assert before <= sent["settings_updated_at"] <= after
|
||||
|
|
|
|||
|
|
@ -1169,3 +1169,25 @@ async def test_prisma_health_check_failure_redacts_database_credentials(caplog):
|
|||
assert emitted
|
||||
assert all("hunter2" not in message for message in emitted)
|
||||
assert any("postgresql://REDACTED@db.internal" in message for message in emitted)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_data_key_branch_stamps_settings_updated_at():
|
||||
"""`updated_at` carries Prisma's @updatedAt and is rewritten by every spend
|
||||
flush, so key config edits need their own audit column."""
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from litellm.proxy.utils import PrismaClient
|
||||
|
||||
client = MagicMock()
|
||||
client.jsonify_object = MagicMock(side_effect=lambda data: dict(data))
|
||||
client.db.litellm_verificationtoken.update = AsyncMock(return_value=None)
|
||||
|
||||
before = datetime.now(timezone.utc)
|
||||
await PrismaClient.update_data(client, token="sk-test-key", data={"models": ["gpt-4"]})
|
||||
after = datetime.now(timezone.utc)
|
||||
|
||||
sent = client.db.litellm_verificationtoken.update.call_args.kwargs["data"]
|
||||
assert sent["models"] == ["gpt-4"]
|
||||
assert before <= sent["settings_updated_at"] <= after
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ export interface KeyResponse {
|
|||
created_at: string;
|
||||
created_by?: string;
|
||||
updated_at: string;
|
||||
settings_updated_at?: string | null;
|
||||
last_active: string | null;
|
||||
team_spend: number;
|
||||
team_alias: string;
|
||||
|
|
|
|||
|
|
@ -149,6 +149,46 @@ describe("KeyInfoView", () => {
|
|||
await userEvent.click(await screen.findByRole("button", { name: /more key actions/i }));
|
||||
};
|
||||
|
||||
describe("last updated", () => {
|
||||
const renderWithTimestamps = (overrides: Partial<KeyResponse>) => {
|
||||
vi.mocked(useAuthorized).mockReturnValue(baseUseAuthorizedMock);
|
||||
|
||||
return renderWithProviders(
|
||||
<KeyInfoView
|
||||
keyData={{
|
||||
...MOCK_KEY_DATA,
|
||||
created_at: "2021-06-15T12:00:00Z",
|
||||
updated_at: "2023-06-15T12:00:00Z",
|
||||
...overrides,
|
||||
}}
|
||||
onClose={() => {}}
|
||||
keyId={"test-key-id"}
|
||||
onKeyDataUpdate={() => {}}
|
||||
teams={[]}
|
||||
/>,
|
||||
);
|
||||
};
|
||||
|
||||
const findLastUpdatedText = async () => {
|
||||
const label = await screen.findByText("Last Updated");
|
||||
return label.closest("div")?.parentElement?.parentElement?.textContent ?? "";
|
||||
};
|
||||
|
||||
it("should show when the key was last configured, not when it last recorded spend", async () => {
|
||||
renderWithTimestamps({ settings_updated_at: "2022-06-15T12:00:00Z" });
|
||||
|
||||
expect(await findLastUpdatedText()).toMatch(/Jun \d+, 2022/);
|
||||
expect(screen.queryByText(/Jun \d+, 2023/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should fall back to creation time for a key that was never reconfigured", async () => {
|
||||
renderWithTimestamps({ settings_updated_at: null });
|
||||
|
||||
expect(await findLastUpdatedText()).toMatch(/Jun \d+, 2021/);
|
||||
expect(screen.queryByText(/Jun \d+, 2023/)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should render tags", async () => {
|
||||
vi.mocked(useAuthorized).mockReturnValue(baseUseAuthorizedMock);
|
||||
|
||||
|
|
|
|||
|
|
@ -444,6 +444,8 @@ export default function KeyInfoView({
|
|||
);
|
||||
};
|
||||
|
||||
const lastConfiguredAt = currentKeyData.settings_updated_at || currentKeyData.created_at;
|
||||
|
||||
const parentTeam = currentKeyData.team_id ? teamsData?.find((team) => team.team_id === currentKeyData.team_id) : null;
|
||||
|
||||
const budgetDisplay =
|
||||
|
|
@ -468,7 +470,7 @@ export default function KeyInfoView({
|
|||
currentKeyData.created_by ||
|
||||
"",
|
||||
createdAt: currentKeyData.created_at ? formatTimestamp(currentKeyData.created_at) : "",
|
||||
lastUpdated: currentKeyData.updated_at ? formatTimestamp(currentKeyData.updated_at) : "",
|
||||
lastUpdated: lastConfiguredAt ? formatTimestamp(lastConfiguredAt) : "",
|
||||
lastActive: currentKeyData.last_active ? formatTimestamp(currentKeyData.last_active) : "Never",
|
||||
expires: currentKeyData.expires ? formatTimestamp(currentKeyData.expires) : "Never",
|
||||
}}
|
||||
|
|
|
|||
6
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
6
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
|
|
@ -26280,6 +26280,8 @@ export interface components {
|
|||
} | null;
|
||||
/** Rpm Limit */
|
||||
rpm_limit?: number | null;
|
||||
/** Settings Updated At */
|
||||
settings_updated_at?: string | null;
|
||||
/**
|
||||
* Soft Budget Cooldown
|
||||
* @default false
|
||||
|
|
@ -27719,6 +27721,8 @@ export interface components {
|
|||
} | null;
|
||||
/** Rpm Limit */
|
||||
rpm_limit?: number | null;
|
||||
/** Settings Updated At */
|
||||
settings_updated_at?: string | null;
|
||||
/**
|
||||
* Soft Budget Cooldown
|
||||
* @default false
|
||||
|
|
@ -34887,6 +34891,8 @@ export interface components {
|
|||
rpm_limit_per_model?: {
|
||||
[key: string]: number;
|
||||
} | null;
|
||||
/** Settings Updated At */
|
||||
settings_updated_at?: string | null;
|
||||
/** Soft Budget */
|
||||
soft_budget?: number | null;
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue