[Feat] UI - Allow scheduling key rotations when creating virtual keys (#14960)

* fix design of key reset interval

* fix: LiteLLM_VerificationToken

* fix key manager

* add key_rotation_at

* ui fix

* fix ui view

* set key_rotation_at  on creation

* add key_rotation_at

* fix info

* fix: _set_key_rotation_fields

* fix KeyRotationManager

* fix key edit view

* test_update_key_fn_auto_rotate_enable

* fix KeyRotationManager
This commit is contained in:
Ishaan Jaff 2025-09-26 16:24:40 -07:00 committed by GitHub
parent 076cb4654e
commit 628cd13755
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 458 additions and 93 deletions

View file

@ -225,6 +225,7 @@ model LiteLLM_VerificationToken {
auto_rotate Boolean? @default(false) // Whether this key should be auto-rotated
rotation_interval String? // How often to rotate (e.g., "30d", "90d")
last_rotation_at DateTime? // When this key was last rotated
key_rotation_at DateTime? // When this key should next be rotated
litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id])
litellm_organization_table LiteLLM_OrganizationTable? @relation(fields: [organization_id], references: [organization_id])
object_permission LiteLLM_ObjectPermissionTable? @relation(fields: [object_permission_id], references: [object_permission_id])

View file

@ -842,6 +842,8 @@ class UpdateKeyRequest(KeyRequestBase):
metadata: Optional[dict] = None
temp_budget_increase: Optional[float] = None
temp_budget_expiry: Optional[datetime] = None
auto_rotate: Optional[bool] = None
rotation_interval: Optional[str] = None
@model_validator(mode="after")
def validate_temp_budget(self) -> "UpdateKeyRequest":
@ -1814,6 +1816,7 @@ class LiteLLM_VerificationToken(LiteLLMPydanticObjectBase):
auto_rotate: Optional[bool] = False # Whether this key should be auto-rotated
rotation_interval: Optional[str] = None # How often to rotate (e.g., "30d", "90d")
last_rotation_at: Optional[datetime] = None # When this key was last rotated
key_rotation_at: Optional[datetime] = None # When this key should next be rotated
model_config = ConfigDict(protected_namespaces=())

View file

@ -4,7 +4,7 @@ Key Rotation Manager - Automated key rotation based on rotation schedules
Handles finding keys that need rotation based on their individual schedules.
"""
from datetime import datetime, timedelta, timezone
from datetime import datetime, timezone
from typing import List
from litellm._logging import verbose_proxy_logger
@ -16,6 +16,7 @@ from litellm.proxy._types import (
)
from litellm.proxy.hooks.key_management_event_hooks import KeyManagementEventHooks
from litellm.proxy.management_endpoints.key_management_endpoints import (
_calculate_key_rotation_time,
regenerate_key_fn,
)
from litellm.proxy.utils import PrismaClient
@ -60,49 +61,39 @@ class KeyRotationManager:
async def _find_keys_needing_rotation(self) -> List[LiteLLM_VerificationToken]:
"""
Find keys that are due for rotation based on their rotation interval.
Find keys that are due for rotation based on their key_rotation_at timestamp.
Logic:
- Key has auto_rotate = true
- Key has rotation_interval set
- Either: never been rotated (last_rotation_at is null) OR
- Time since last rotation >= rotation_interval
- key_rotation_at is null (needs initial setup) OR key_rotation_at <= now
"""
now = datetime.now(timezone.utc)
keys_with_rotation = await self.prisma_client.db.litellm_verificationtoken.find_many(
where={
"auto_rotate": True, # Only keys marked for auto rotation
"rotation_interval": {"not": None} # Must have rotation interval set
"OR": [
{"key_rotation_at": None}, # Keys that need initial rotation time setup
{"key_rotation_at": {"lte": now}} # Keys where rotation time has passed
]
}
)
# Filter keys that need rotation based on last_rotation_at + interval
keys_needing_rotation = []
now = datetime.now(timezone.utc)
for key in keys_with_rotation:
if self._should_rotate_key(key, now):
keys_needing_rotation.append(key)
return keys_needing_rotation
return keys_with_rotation
def _should_rotate_key(self, key: LiteLLM_VerificationToken, now: datetime) -> bool:
"""
Determine if a key should be rotated based on last rotation time and interval.
Determine if a key should be rotated based on key_rotation_at timestamp.
"""
if not key.rotation_interval:
return False
# If never rotated, rotate immediately
if key.last_rotation_at is None:
# If key_rotation_at is not set, rotate immediately (and set it)
if key.key_rotation_at is None:
return True
# Calculate if enough time has passed since last rotation
from litellm.litellm_core_utils.duration_parser import duration_in_seconds
interval_seconds = duration_in_seconds(key.rotation_interval)
next_rotation_time = key.last_rotation_at + timedelta(seconds=interval_seconds)
return now >= next_rotation_time
# Check if the rotation time has passed
return now >= key.key_rotation_at
async def _rotate_key(self, key: LiteLLM_VerificationToken):
"""
@ -125,12 +116,16 @@ class KeyRotationManager:
)
# Update the NEW key with rotation info (regenerate_key_fn creates a new token)
if isinstance(response, GenerateKeyResponse) and response.token_id:
if isinstance(response, GenerateKeyResponse) and response.token_id and key.rotation_interval:
# Calculate next rotation time using helper function
now = datetime.now(timezone.utc)
next_rotation_time = _calculate_key_rotation_time(key.rotation_interval)
await self.prisma_client.db.litellm_verificationtoken.update(
where={"token": response.token_id},
data={
"rotation_count": (key.rotation_count or 0) + 1,
"last_rotation_at": datetime.now(timezone.utc)
"last_rotation_at": now,
"key_rotation_at": next_rotation_time
}
)

View file

@ -87,6 +87,38 @@ def _get_user_in_team(
return None
def _calculate_key_rotation_time(rotation_interval: str) -> datetime:
"""
Helper function to calculate the next rotation time for a key based on the rotation interval.
Args:
rotation_interval: String representing the rotation interval (e.g., '30d', '90d', '1h')
Returns:
datetime: The calculated next rotation time in UTC
"""
now = datetime.now(timezone.utc)
interval_seconds = duration_in_seconds(rotation_interval)
return now + timedelta(seconds=interval_seconds)
def _set_key_rotation_fields(data: dict, auto_rotate: bool, rotation_interval: Optional[str]) -> None:
"""
Helper function to set rotation fields in key data if auto_rotate is enabled.
Args:
data: Dictionary to update with rotation fields
auto_rotate: Whether auto rotation is enabled
rotation_interval: The rotation interval string (required if auto_rotate is True)
"""
if auto_rotate and rotation_interval:
data.update({
"auto_rotate": auto_rotate,
"rotation_interval": rotation_interval,
"key_rotation_at": _calculate_key_rotation_time(rotation_interval)
})
def _is_allowed_to_make_key_request(
user_api_key_dict: UserAPIKeyAuth, user_id: Optional[str], team_id: Optional[str]
) -> bool:
@ -1071,6 +1103,8 @@ async def update_key_fn(
- allowed_routes: Optional[list] - List of allowed routes for the key. Store the actual route or store a wildcard pattern for a set of routes. Example - ["/chat/completions", "/embeddings", "/keys/*"]
- prompts: Optional[List[str]] - List of allowed prompts for the key. If specified, the key will only be able to use these specific prompts.
- object_permission: Optional[LiteLLM_ObjectPermissionBase] - key-specific object permission. Example - {"vector_stores": ["vector_store_1", "vector_store_2"]}. IF null or {} then no object permission.
- auto_rotate: Optional[bool] - Whether this key should be automatically rotated
- rotation_interval: Optional[str] - How often to rotate this key (e.g., '30d', '90d'). Required if auto_rotate=True
Example:
```bash
curl --location 'http://0.0.0.0:4000/key/update' \
@ -1162,6 +1196,13 @@ async def update_key_fn(
existing_key_token=existing_key_row.token,
)
# Handle rotation fields if auto_rotate is being enabled
_set_key_rotation_fields(
non_default_values,
non_default_values.get("auto_rotate", False),
non_default_values.get("rotation_interval")
)
_data = {**non_default_values, "token": key}
response = await prisma_client.update_data(token=key, data=_data)
@ -1727,12 +1768,11 @@ async def generate_key_helper_fn( # noqa: PLR0915
}
# Add rotation fields if auto_rotate is enabled
if auto_rotate and rotation_interval:
key_data.update({
"auto_rotate": auto_rotate,
"rotation_interval": rotation_interval
# last_rotation_at will be null initially - rotation happens on first check
})
_set_key_rotation_fields(
data=key_data,
auto_rotate=auto_rotate or False,
rotation_interval=rotation_interval
)
if (
get_secret("DISABLE_KEY_NAME", False) is True

View file

@ -225,6 +225,7 @@ model LiteLLM_VerificationToken {
auto_rotate Boolean? @default(false) // Whether this key should be auto-rotated
rotation_interval String? // How often to rotate (e.g., "30d", "90d")
last_rotation_at DateTime? // When this key was last rotated
key_rotation_at DateTime? // When this key should next be rotated
litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id])
litellm_organization_table LiteLLM_OrganizationTable? @relation(fields: [organization_id], references: [organization_id])
object_permission LiteLLM_ObjectPermissionTable? @relation(fields: [object_permission_id], references: [object_permission_id])

View file

@ -225,6 +225,7 @@ model LiteLLM_VerificationToken {
auto_rotate Boolean? @default(false) // Whether this key should be auto-rotated
rotation_interval String? // How often to rotate (e.g., "30d", "90d")
last_rotation_at DateTime? // When this key was last rotated
key_rotation_at DateTime? // When this key should next be rotated
litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id])
litellm_organization_table LiteLLM_OrganizationTable? @relation(fields: [organization_id], references: [organization_id])
object_permission LiteLLM_ObjectPermissionTable? @relation(fields: [object_permission_id], references: [object_permission_id])

View file

@ -26,9 +26,9 @@ class TestKeyRotationManager:
Test the core logic for determining when a key should be rotated.
This tests:
- Keys with null last_rotation_at should rotate immediately
- Keys with recent rotation should not rotate
- Keys with old rotation should rotate
- Keys with null key_rotation_at should rotate immediately
- Keys with future key_rotation_at should not rotate
- Keys with past key_rotation_at should rotate
"""
# Setup
mock_prisma_client = AsyncMock()
@ -36,45 +36,56 @@ class TestKeyRotationManager:
now = datetime.now(timezone.utc)
# Test Case 1: Never rotated (last_rotation_at = None) - should rotate
key_never_rotated = LiteLLM_VerificationToken(
# Test Case 1: No rotation time set (key_rotation_at = None) - should rotate
key_no_rotation_time = LiteLLM_VerificationToken(
token="test-token-1",
auto_rotate=True,
rotation_interval="30s",
last_rotation_at=None,
key_rotation_at=None,
rotation_count=0
)
assert manager._should_rotate_key(key_never_rotated, now) == True
assert manager._should_rotate_key(key_no_rotation_time, now) == True
# Test Case 2: Recently rotated (10s ago, interval 30s) - should NOT rotate
key_recently_rotated = LiteLLM_VerificationToken(
# Test Case 2: Future rotation time - should NOT rotate
key_future_rotation = LiteLLM_VerificationToken(
token="test-token-2",
auto_rotate=True,
rotation_interval="30s",
last_rotation_at=now - timedelta(seconds=10),
key_rotation_at=now + timedelta(seconds=10),
rotation_count=1
)
assert manager._should_rotate_key(key_recently_rotated, now) == False
assert manager._should_rotate_key(key_future_rotation, now) == False
# Test Case 3: Old rotation (60s ago, interval 30s) - should rotate
key_old_rotation = LiteLLM_VerificationToken(
# Test Case 3: Past rotation time - should rotate
key_past_rotation = LiteLLM_VerificationToken(
token="test-token-3",
auto_rotate=True,
rotation_interval="30s",
last_rotation_at=now - timedelta(seconds=60),
key_rotation_at=now - timedelta(seconds=10),
rotation_count=2
)
assert manager._should_rotate_key(key_old_rotation, now) == True
assert manager._should_rotate_key(key_past_rotation, now) == True
# Test Case 4: No rotation interval - should NOT rotate
key_no_interval = LiteLLM_VerificationToken(
# Test Case 4: Exact rotation time - should rotate
key_exact_rotation = LiteLLM_VerificationToken(
token="test-token-4",
auto_rotate=True,
rotation_interval="30s",
key_rotation_at=now,
rotation_count=1
)
assert manager._should_rotate_key(key_exact_rotation, now) == True
# Test Case 5: No rotation interval - should NOT rotate
key_no_interval = LiteLLM_VerificationToken(
token="test-token-5",
auto_rotate=True,
rotation_interval=None,
last_rotation_at=None,
key_rotation_at=None,
rotation_count=0
)
@ -86,61 +97,63 @@ class TestKeyRotationManager:
Test finding keys that need rotation from database.
This tests:
- Only keys with auto_rotate=True and rotation_interval are considered
- Filtering logic works correctly
- Database query is constructed properly
- Only keys with auto_rotate=True are considered
- Database query filters by key_rotation_at properly
- Keys are returned based on key_rotation_at being null or <= now
"""
# Setup
mock_prisma_client = AsyncMock()
manager = KeyRotationManager(mock_prisma_client)
now = datetime.now(timezone.utc)
# Use a fixed timestamp to avoid timing issues in tests
now = datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
# Mock database response
# Mock database response - these are the keys the database query would return
mock_keys = [
LiteLLM_VerificationToken(
token="token-1",
auto_rotate=True,
rotation_interval="30s",
last_rotation_at=None, # Should rotate
key_rotation_at=None, # Should rotate (null key_rotation_at)
rotation_count=0
),
LiteLLM_VerificationToken(
token="token-2",
auto_rotate=True,
rotation_interval="60s",
last_rotation_at=now - timedelta(seconds=30), # Should NOT rotate (30s < 60s)
key_rotation_at=now - timedelta(seconds=10), # Should rotate (past time)
rotation_count=1
),
LiteLLM_VerificationToken(
token="token-3",
auto_rotate=True,
rotation_interval="30s",
last_rotation_at=now - timedelta(seconds=45), # Should rotate (45s > 30s)
rotation_count=2
)
]
mock_prisma_client.db.litellm_verificationtoken.find_many.return_value = mock_keys
# Execute
keys_needing_rotation = await manager._find_keys_needing_rotation()
# Mock datetime.now to return our fixed timestamp
from unittest.mock import patch
with patch('litellm.proxy.common_utils.key_rotation_manager.datetime') as mock_datetime:
mock_datetime.now.return_value = now
mock_datetime.side_effect = lambda *args, **kwargs: datetime(*args, **kwargs)
# Execute
keys_needing_rotation = await manager._find_keys_needing_rotation()
# Verify database query
# Verify database query - should use OR condition for key_rotation_at
mock_prisma_client.db.litellm_verificationtoken.find_many.assert_called_once_with(
where={
"auto_rotate": True,
"rotation_interval": {"not": None}
"OR": [
{"key_rotation_at": None},
{"key_rotation_at": {"lte": now}}
]
}
)
# Verify filtering logic
assert len(keys_needing_rotation) == 2 # token-1 and token-3 should need rotation
# Verify all keys returned by database query are included (no additional filtering)
assert len(keys_needing_rotation) == 2
tokens_needing_rotation = [key.token for key in keys_needing_rotation]
assert "token-1" in tokens_needing_rotation # Never rotated
assert "token-2" not in tokens_needing_rotation # Recently rotated
assert "token-3" in tokens_needing_rotation # Old rotation
assert "token-1" in tokens_needing_rotation # Null key_rotation_at
assert "token-2" in tokens_needing_rotation # Past key_rotation_at
@pytest.mark.asyncio
async def test_rotate_key_updates_database(self):
@ -150,6 +163,7 @@ class TestKeyRotationManager:
This tests:
- Rotation count is incremented
- last_rotation_at is set to current time
- key_rotation_at is set to next rotation time
- New key token is updated (not old one)
"""
# Setup
@ -162,6 +176,7 @@ class TestKeyRotationManager:
auto_rotate=True,
rotation_interval="30s",
last_rotation_at=None,
key_rotation_at=None,
rotation_count=0
)
@ -192,3 +207,11 @@ class TestKeyRotationManager:
assert update_data["rotation_count"] == 1 # Incremented from 0
assert "last_rotation_at" in update_data
assert isinstance(update_data["last_rotation_at"], datetime)
assert "key_rotation_at" in update_data
assert isinstance(update_data["key_rotation_at"], datetime)
# Verify key_rotation_at is set to future time (30s from now)
now = datetime.now(timezone.utc)
next_rotation = update_data["key_rotation_at"]
time_diff = (next_rotation - now).total_seconds()
assert 25 <= time_diff <= 35 # Should be around 30 seconds, allow some tolerance

View file

@ -1162,3 +1162,75 @@ def test_key_rotation_fields_helper():
# Verify rotation fields are NOT added (missing interval)
assert "auto_rotate" not in key_data3
assert "rotation_interval" not in key_data3
@pytest.mark.asyncio
async def test_update_key_fn_auto_rotate_enable():
"""Test that update_key_fn properly handles enabling auto rotation."""
from litellm.proxy._types import LiteLLM_VerificationToken, UpdateKeyRequest
from litellm.proxy.management_endpoints.key_management_endpoints import (
prepare_key_update_data,
)
# Mock existing key
existing_key = LiteLLM_VerificationToken(
token="test-token",
key_alias="test-key",
models=["gpt-3.5-turbo"],
user_id="test-user",
team_id=None,
auto_rotate=False,
rotation_interval=None,
metadata={}
)
# Test enabling auto rotation
update_request = UpdateKeyRequest(
key="test-token",
auto_rotate=True,
rotation_interval="30d"
)
result = await prepare_key_update_data(
data=update_request,
existing_key_row=existing_key
)
# Verify rotation fields are included
assert result["auto_rotate"] is True
assert result["rotation_interval"] == "30d"
@pytest.mark.asyncio
async def test_update_key_fn_auto_rotate_disable():
"""Test that update_key_fn properly handles disabling auto rotation."""
from litellm.proxy._types import LiteLLM_VerificationToken, UpdateKeyRequest
from litellm.proxy.management_endpoints.key_management_endpoints import (
prepare_key_update_data,
)
# Mock existing key with rotation enabled
existing_key = LiteLLM_VerificationToken(
token="test-token",
key_alias="test-key",
models=["gpt-3.5-turbo"],
user_id="test-user",
team_id=None,
auto_rotate=True,
rotation_interval="30d",
metadata={}
)
# Test disabling auto rotation
update_request = UpdateKeyRequest(
key="test-token",
auto_rotate=False
)
result = await prepare_key_update_data(
data=update_request,
existing_key_row=existing_key
)
# Verify auto_rotate is set to False
assert result["auto_rotate"] is False

View file

@ -0,0 +1,129 @@
import React from "react";
import { Card, Text, Badge } from "@tremor/react";
import { RefreshIcon, ClockIcon } from "@heroicons/react/outline";
interface AutoRotationViewProps {
autoRotate?: boolean;
rotationInterval?: string;
lastRotationAt?: string;
keyRotationAt?: string;
nextRotationAt?: string;
variant?: "card" | "inline";
className?: string;
}
const AutoRotationView: React.FC<AutoRotationViewProps> = ({
autoRotate = false,
rotationInterval,
lastRotationAt,
keyRotationAt,
nextRotationAt,
variant = "card",
className = "",
}) => {
const formatTimestamp = (timestamp: string | Date) => {
const date = new Date(timestamp);
const dateStr = date.toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
});
const timeStr = date.toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
hour12: true,
});
return `${dateStr} at ${timeStr}`;
};
const content = (
<div className="space-y-6">
{/* Status Section */}
<div className="space-y-3">
<div className="flex items-center gap-2">
<RefreshIcon className="h-4 w-4 text-blue-600" />
<Text className="font-semibold text-gray-900">Auto-Rotation</Text>
<Badge color={autoRotate ? "green" : "gray"} size="xs">
{autoRotate ? "Enabled" : "Disabled"}
</Badge>
{autoRotate && rotationInterval && (
<>
<Text className="text-gray-400"></Text>
<Text className="text-sm text-gray-600">Every {rotationInterval}</Text>
</>
)}
</div>
</div>
{/* Rotation History - Show if there's any rotation data OR if auto-rotation is enabled */}
{(autoRotate || lastRotationAt || keyRotationAt || nextRotationAt) && (
<div className="space-y-3">
{/* Last Rotation - Show when available */}
{lastRotationAt && (
<div className="flex items-center gap-2 p-3 bg-gray-50 border border-gray-200 rounded-md">
<ClockIcon className="w-4 h-4 text-gray-500" />
<div className="flex-1">
<Text className="font-medium text-gray-700">Last Rotation</Text>
<Text className="text-sm text-gray-600">{formatTimestamp(lastRotationAt)}</Text>
</div>
</div>
)}
{/* Next Scheduled Rotation - Show when available */}
{(keyRotationAt || nextRotationAt) && (
<div className="flex items-center gap-2 p-3 bg-gray-50 border border-gray-200 rounded-md">
<ClockIcon className="w-4 h-4 text-gray-500" />
<div className="flex-1">
<Text className="font-medium text-gray-700">Next Scheduled Rotation</Text>
<Text className="text-sm text-gray-600">
{formatTimestamp(nextRotationAt || keyRotationAt || "")}
</Text>
</div>
</div>
)}
{/* No rotation data message - Only show if auto-rotation is enabled but no data */}
{autoRotate && !lastRotationAt && !keyRotationAt && !nextRotationAt && (
<div className="flex items-center gap-2 p-3 bg-gray-50 border border-gray-100 rounded-md">
<ClockIcon className="w-4 h-4 text-gray-500" />
<Text className="text-gray-600">No rotation history available</Text>
</div>
)}
</div>
)}
{/* Disabled State - Only show if auto-rotation is disabled AND there's no rotation history */}
{!autoRotate && !lastRotationAt && !keyRotationAt && !nextRotationAt && (
<div className="flex items-center gap-2 p-3 bg-gray-50 border border-gray-100 rounded-md">
<RefreshIcon className="w-4 h-4 text-gray-400" />
<Text className="text-gray-600">Auto-rotation is not enabled for this key</Text>
</div>
)}
</div>
);
if (variant === "card") {
return (
<div className={`bg-white border border-gray-200 rounded-lg p-6 ${className}`}>
<div className="flex items-center gap-2 mb-6">
<div>
<Text className="font-semibold text-gray-900">Auto-Rotation</Text>
<Text className="text-xs text-gray-500">
Automatic key rotation settings and status for this key
</Text>
</div>
</div>
{content}
</div>
);
}
return (
<div className={`${className}`}>
<Text className="font-medium text-gray-900 mb-3">Auto-Rotation</Text>
{content}
</div>
);
};
export default AutoRotationView;

View file

@ -1,4 +1,4 @@
import React from "react";
import React, { useState } from "react";
import { Select, Tooltip, Divider, Switch } from "antd";
import { InfoCircleOutlined } from "@ant-design/icons";
import { TextInput } from "@tremor/react";
@ -20,6 +20,31 @@ const KeyLifecycleSettings: React.FC<KeyLifecycleSettingsProps> = ({
rotationInterval,
onRotationIntervalChange,
}) => {
// Predefined intervals
const predefinedIntervals = ["7d", "30d", "90d", "180d", "365d"];
// Check if current interval is custom
const isCustomInterval = rotationInterval && !predefinedIntervals.includes(rotationInterval);
const [showCustomInput, setShowCustomInput] = useState(isCustomInterval);
const [customInterval, setCustomInterval] = useState(isCustomInterval ? rotationInterval : "");
const handleIntervalChange = (value: string) => {
if (value === "custom") {
setShowCustomInput(true);
// Don't change the actual interval yet, wait for custom input
} else {
setShowCustomInput(false);
setCustomInterval("");
onRotationIntervalChange(value);
}
};
const handleCustomIntervalChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setCustomInterval(value);
onRotationIntervalChange(value);
};
return (
<div className="space-y-6">
{/* Key Expiry Section */}
@ -71,18 +96,34 @@ const KeyLifecycleSettings: React.FC<KeyLifecycleSettingsProps> = ({
<InfoCircleOutlined className="text-gray-400 cursor-help text-xs" />
</Tooltip>
</label>
<Select
value={rotationInterval}
onChange={onRotationIntervalChange}
className="w-full"
placeholder="Select interval"
>
<Option value="7d">7 days</Option>
<Option value="30d">30 days</Option>
<Option value="90d">90 days</Option>
<Option value="180d">180 days</Option>
<Option value="365d">365 days</Option>
</Select>
<div className="space-y-2">
<Select
value={showCustomInput ? "custom" : rotationInterval}
onChange={handleIntervalChange}
className="w-full"
placeholder="Select interval"
>
<Option value="7d">7 days</Option>
<Option value="30d">30 days</Option>
<Option value="90d">90 days</Option>
<Option value="180d">180 days</Option>
<Option value="365d">365 days</Option>
<Option value="custom">Custom interval</Option>
</Select>
{showCustomInput && (
<div className="space-y-1">
<TextInput
value={customInterval}
onChange={handleCustomIntervalChange}
placeholder="e.g., 1s, 5m, 2h, 14d"
/>
<div className="text-xs text-gray-500">
Supported formats: seconds (s), minutes (m), hours (h), days (d)
</div>
</div>
)}
</div>
</div>
)}
</div>

View file

@ -82,6 +82,11 @@ export interface KeyResponse {
mcp_access_groups?: string[];
vector_stores: string[];
};
auto_rotate?: boolean;
rotation_interval?: string;
last_rotation_at?: string;
key_rotation_at?: string;
next_rotation_at?: string;
}
interface KeyListResponse {

View file

@ -327,13 +327,10 @@ const CreateKey: React.FC<CreateKeyProps> = ({
};
}
// Add auto-rotation settings to the metadata
// Add auto-rotation settings as top-level fields
if (autoRotationEnabled) {
metadata = {
...metadata,
auto_rotation_enabled: true,
rotation_interval: rotationInterval
};
formValues.auto_rotate = true;
formValues.rotation_interval = rotationInterval;
}
// Handle duration field for key expiry

View file

@ -12,6 +12,7 @@ import { extractLoggingSettings, formatMetadataForDisplay } from "../key_info_ut
import { fetchMCPAccessGroups } from "../networking"
import { mapInternalToDisplayNames, mapDisplayToInternalNames } from "../callback_info_helpers"
import GuardrailSelector from "@/components/guardrails/GuardrailSelector"
import KeyLifecycleSettings from "../common_components/KeyLifecycleSettings"
interface KeyEditViewProps {
keyData: KeyResponse
@ -65,6 +66,8 @@ export function KeyEditView({
? mapInternalToDisplayNames(keyData.metadata.litellm_disabled_callbacks)
: [],
)
const [autoRotationEnabled, setAutoRotationEnabled] = useState<boolean>(keyData.auto_rotate || false)
const [rotationInterval, setRotationInterval] = useState<string>(keyData.rotation_interval || "")
const fetchMcpAccessGroups = async () => {
if (!accessToken) return
@ -145,6 +148,8 @@ export function KeyEditView({
disabled_callbacks: Array.isArray(keyData.metadata?.litellm_disabled_callbacks)
? mapInternalToDisplayNames(keyData.metadata.litellm_disabled_callbacks)
: [],
auto_rotate: keyData.auto_rotate || false,
...(keyData.rotation_interval && { rotation_interval: keyData.rotation_interval }),
}
useEffect(() => {
@ -164,9 +169,22 @@ export function KeyEditView({
disabled_callbacks: Array.isArray(keyData.metadata?.litellm_disabled_callbacks)
? mapInternalToDisplayNames(keyData.metadata.litellm_disabled_callbacks)
: [],
auto_rotate: keyData.auto_rotate || false,
...(keyData.rotation_interval && { rotation_interval: keyData.rotation_interval })
})
}, [keyData, form])
// Sync auto-rotation state with form values
useEffect(() => {
form.setFieldValue("auto_rotate", autoRotationEnabled)
}, [autoRotationEnabled, form])
useEffect(() => {
if (rotationInterval) {
form.setFieldValue("rotation_interval", rotationInterval)
}
}, [rotationInterval, form])
console.log("premiumUser:", premiumUser)
return (
@ -291,6 +309,17 @@ export function KeyEditView({
<Input.TextArea rows={10} />
</Form.Item>
{/* Auto-Rotation Settings */}
<div className="mb-4">
<KeyLifecycleSettings
form={form}
autoRotationEnabled={autoRotationEnabled}
onAutoRotationChange={setAutoRotationEnabled}
rotationInterval={rotationInterval}
onRotationIntervalChange={setRotationInterval}
/>
</div>
{/* Hidden form field for token */}
<Form.Item name="token" hidden>
<Input />
@ -301,6 +330,14 @@ export function KeyEditView({
<Input />
</Form.Item>
{/* Hidden form fields for auto-rotation */}
<Form.Item name="auto_rotate" hidden>
<Input />
</Form.Item>
<Form.Item name="rotation_interval" hidden>
<Input />
</Form.Item>
<div className="sticky z-10 bg-white p-4 border-t border-gray-200 bottom-[-1.5rem] inset-x-[-1.5rem]">
<div className="flex justify-end items-center gap-2">
<AntdButton onClick={onCancel}>Cancel</AntdButton>

View file

@ -30,6 +30,7 @@ import { extractLoggingSettings, formatMetadataForDisplay } from "../key_info_ut
import { CopyIcon, CheckIcon } from "lucide-react"
import { callback_map, mapInternalToDisplayNames, mapDisplayToInternalNames } from "../callback_info_helpers"
import { parseErrorMessage } from "../shared/errorUtils"
import AutoRotationView from "../common_components/AutoRotationView"
interface KeyInfoViewProps {
keyId: string
@ -484,6 +485,15 @@ export default function KeyInfoView({
}
variant="card"
/>
<AutoRotationView
autoRotate={currentKeyData.auto_rotate}
rotationInterval={currentKeyData.rotation_interval}
lastRotationAt={currentKeyData.last_rotation_at}
keyRotationAt={currentKeyData.key_rotation_at}
nextRotationAt={currentKeyData.next_rotation_at}
variant="card"
/>
</Grid>
</TabPanel>
@ -559,6 +569,16 @@ export default function KeyInfoView({
<Text>{currentKeyData.expires ? formatTimestamp(currentKeyData.expires) : "Never"}</Text>
</div>
<AutoRotationView
autoRotate={currentKeyData.auto_rotate}
rotationInterval={currentKeyData.rotation_interval}
lastRotationAt={currentKeyData.last_rotation_at}
keyRotationAt={currentKeyData.key_rotation_at}
nextRotationAt={currentKeyData.next_rotation_at}
variant="inline"
className="pt-4 border-t border-gray-200"
/>
<div>
<Text className="font-medium">Spend</Text>
<Text>${formatNumberWithCommas(currentKeyData.spend, 4)} USD</Text>