Merge pull request #22956 from BerriAI/litellm_key_null_duration

[Fix] Key Expiry Default Duration
This commit is contained in:
yuneng-jiang 2026-03-05 21:06:55 -08:00 committed by GitHub
commit 8523bb6b48
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 67 additions and 11 deletions

View File

@ -534,8 +534,8 @@ async def _common_key_generation_helper( # noqa: PLR0915
upperbound_duration = duration_in_seconds(
duration=upperbound_value
)
# Handle special case where duration is "-1" (never expires)
if value == "-1":
# Handle special case where duration is None or "-1" (never expires)
if value is None or value == "-1":
user_duration = float("inf") # Infinite duration
else:
user_duration = duration_in_seconds(duration=value)
@ -1462,7 +1462,7 @@ async def prepare_key_update_data(
if "duration" in non_default_values:
duration = non_default_values.pop("duration")
if duration == "-1":
if duration is None or duration == "-1":
# Set expires to None to indicate the key never expires
non_default_values["expires"] = None
elif duration and (isinstance(duration, str)) and len(duration) > 0:
@ -1786,7 +1786,7 @@ async def update_key_fn(
- tpm_limit_type: Optional[str] - TPM rate limit type - "best_effort_throughput", "guaranteed_throughput", or "dynamic"
- rpm_limit_type: Optional[str] - RPM rate limit type - "best_effort_throughput", "guaranteed_throughput", or "dynamic"
- allowed_cache_controls: Optional[list] - List of allowed cache control values
- duration: Optional[str] - Key validity duration ("30d", "1h", etc.) or "-1" to never expire
- duration: Optional[str] - Key validity duration ("30d", "1h", etc.), null to never expire, or "-1" to never expire (deprecated, use null)
- permissions: Optional[dict] - Key-specific permissions
- send_invite_email: Optional[bool] - Send invite email to user_id
- guardrails: Optional[List[str]] - List of active guardrails for the key

View File

@ -1087,6 +1087,34 @@ async def test_prepare_key_update_data_duration_never_expires():
assert result["expires"] is None
@pytest.mark.asyncio
async def test_prepare_key_update_data_duration_none_never_expires():
"""Test that duration=None sets expires to None (never expires)."""
from litellm.proxy._types import UpdateKeyRequest
from litellm.proxy.management_endpoints.key_management_endpoints import (
prepare_key_update_data,
)
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={},
)
update_request = UpdateKeyRequest(key="test-token", duration=None)
result = await prepare_key_update_data(
data=update_request, existing_key_row=existing_key
)
assert result["expires"] is None
@pytest.mark.asyncio
async def test_validate_team_id_used_in_service_account_request_requires_team_id():
"""

View File

@ -1,5 +1,5 @@
import React, { useState } from "react";
import { Select, Tooltip, Divider, Switch } from "antd";
import { Select, Tooltip, Divider, Switch, Checkbox } from "antd";
import { InfoCircleOutlined } from "@ant-design/icons";
import { TextInput } from "@tremor/react";
@ -12,6 +12,8 @@ interface KeyLifecycleSettingsProps {
rotationInterval: string;
onRotationIntervalChange: (interval: string) => void;
isCreateMode?: boolean; // If true, shows "leave empty to never expire" instead of "-1 to never expire"
neverExpire?: boolean;
onNeverExpireChange?: (checked: boolean) => void;
}
const KeyLifecycleSettings: React.FC<KeyLifecycleSettingsProps> = ({
@ -21,6 +23,8 @@ const KeyLifecycleSettings: React.FC<KeyLifecycleSettingsProps> = ({
rotationInterval,
onRotationIntervalChange,
isCreateMode = false,
neverExpire = false,
onNeverExpireChange,
}) => {
// Predefined intervals
const predefinedIntervals = ["7d", "30d", "90d", "180d", "365d"];
@ -67,21 +71,38 @@ const KeyLifecycleSettings: React.FC<KeyLifecycleSettingsProps> = ({
<label className="text-sm font-medium text-gray-700 flex items-center space-x-1">
<span>Expire Key</span>
<Tooltip
title={
isCreateMode
? "Set when this key should expire. Format: 30s (seconds), 30m (minutes), 30h (hours), 30d (days). Leave empty to never expire."
: "Set when this key should expire. Format: 30s (seconds), 30m (minutes), 30h (hours), 30d (days). Use -1 to never expire."
}
title="Set when this key should expire. Format: 30s (seconds), 30m (minutes), 30h (hours), 30d (days). Leave empty to keep the current expiry unchanged."
>
<InfoCircleOutlined className="text-gray-400 cursor-help text-xs" />
</Tooltip>
{!isCreateMode && onNeverExpireChange && (
<Checkbox
checked={neverExpire}
onChange={(e) => {
const checked = e.target.checked;
onNeverExpireChange(checked);
if (checked) {
setDurationValue("");
if (form && typeof form.setFieldValue === "function") {
form.setFieldValue("duration", "");
} else if (form && typeof form.setFieldsValue === "function") {
form.setFieldsValue({ duration: "" });
}
}
}}
className="ml-2 text-sm font-normal text-gray-600"
>
Never Expire
</Checkbox>
)}
</label>
<TextInput
name="duration"
placeholder={isCreateMode ? "e.g., 30d or leave empty to never expire" : "e.g., 30d or -1 to never expire"}
placeholder={isCreateMode ? "e.g., 30d or leave empty to never expire" : "e.g., 30d"}
className="w-full"
value={durationValue}
onValueChange={handleDurationChange}
disabled={!isCreateMode && neverExpire}
/>
</div>
</div>

View File

@ -98,6 +98,7 @@ export function KeyEditView({
);
const [autoRotationEnabled, setAutoRotationEnabled] = useState<boolean>(keyData.auto_rotate || false);
const [rotationInterval, setRotationInterval] = useState<string>(keyData.rotation_interval || "");
const [neverExpire, setNeverExpire] = useState<boolean>(!keyData.expires);
const [isKeySaving, setIsKeySaving] = useState(false);
const { data: projects } = useProjects();
const { data: uiSettingsData } = useUISettings();
@ -265,6 +266,10 @@ export function KeyEditView({
}
// If it's already an array (shouldn't happen, but handle it), keep as is
if (neverExpire) {
values.duration = null;
}
await onSubmit(values);
} finally {
setIsKeySaving(false);
@ -660,6 +665,8 @@ export function KeyEditView({
onAutoRotationChange={setAutoRotationEnabled}
rotationInterval={rotationInterval}
onRotationIntervalChange={setRotationInterval}
neverExpire={neverExpire}
onNeverExpireChange={setNeverExpire}
/>
<Form.Item name="duration" hidden initialValue="">
<Input />