mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-12 23:01:41 +00:00
[Fix] Key Expiry: remove -1 magic number, use null for never-expires
Replace the legacy "-1" sentinel string with proper null semantics: - User duration: null means never-expires (no more "-1" accepted) - Team max: absence of the field means no limit (no more "-1" stored) - Creation path team validation now uses model_fields_set to distinguish "duration not sent" (skip) from "duration: null" (never-expires, validate) - Tests updated: -1 sentinels replaced with None, -1 team max tests replaced with "field not set" equivalent Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
62c174d455
commit
1a5a6e7999
3 changed files with 38 additions and 48 deletions
|
|
@ -534,11 +534,7 @@ 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":
|
||||
user_duration = float("inf") # Infinite duration
|
||||
else:
|
||||
user_duration = duration_in_seconds(duration=value)
|
||||
user_duration = duration_in_seconds(duration=value)
|
||||
if user_duration > upperbound_duration:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
|
|
@ -547,28 +543,28 @@ async def _common_key_generation_helper( # noqa: PLR0915
|
|||
},
|
||||
)
|
||||
|
||||
# Validate key duration against the team's max key duration
|
||||
# Validate key duration against the team's max key duration.
|
||||
# Only runs when the caller explicitly included "duration" in the request
|
||||
# (distinguishes "omitted" from "null = never expires").
|
||||
if (
|
||||
team_table is not None
|
||||
and team_table.metadata is not None
|
||||
and team_table.metadata.get("team_member_key_duration")
|
||||
and data.duration is not None
|
||||
and "duration" in data.model_fields_set
|
||||
):
|
||||
team_max_duration = team_table.metadata["team_member_key_duration"]
|
||||
# "-1" on the team side means no limit is enforced
|
||||
if team_max_duration != "-1":
|
||||
team_max_seconds = duration_in_seconds(duration=team_max_duration)
|
||||
if data.duration == "-1":
|
||||
user_key_duration: float = float("inf")
|
||||
else:
|
||||
user_key_duration = duration_in_seconds(duration=data.duration)
|
||||
if user_key_duration > team_max_seconds:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": f"Key duration exceeds team maximum. Requested: {data.duration}; Team maximum: {team_max_duration}"
|
||||
},
|
||||
)
|
||||
team_max_seconds = duration_in_seconds(duration=team_max_duration)
|
||||
if data.duration is None:
|
||||
user_key_duration: float = float("inf") # null = never expires = infinite
|
||||
else:
|
||||
user_key_duration = duration_in_seconds(duration=data.duration)
|
||||
if user_key_duration > team_max_seconds:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": f"Key duration exceeds team maximum. Requested: {data.duration}; Team maximum: {team_max_duration}"
|
||||
},
|
||||
)
|
||||
|
||||
# APPLY ENTERPRISE KEY MANAGEMENT PARAMS
|
||||
try:
|
||||
|
|
@ -1485,8 +1481,8 @@ async def prepare_key_update_data(
|
|||
|
||||
if "duration" in non_default_values:
|
||||
duration = non_default_values.pop("duration")
|
||||
if duration == "-1" or duration is None:
|
||||
# "-1" (legacy) or null (never-expires checkbox) both mean no expiry
|
||||
if duration is None:
|
||||
# null (never-expires checkbox) means no expiry
|
||||
non_default_values["expires"] = None
|
||||
elif duration and (isinstance(duration, str)) and len(duration) > 0:
|
||||
duration_s = duration_in_seconds(duration=duration)
|
||||
|
|
@ -1809,7 +1805,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.) or null to never expire
|
||||
- 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
|
||||
|
|
@ -3411,8 +3407,6 @@ async def _validate_regenerate_key_duration_against_team(
|
|||
if "duration" not in data.model_fields_set:
|
||||
return # Not provided — leave the existing expiry unchanged
|
||||
user_seconds: float = float("inf") # Explicitly null = never expires
|
||||
elif data.duration == "-1":
|
||||
user_seconds = float("inf") # Legacy sentinel for never-expires
|
||||
else:
|
||||
user_seconds = duration_in_seconds(duration=data.duration)
|
||||
|
||||
|
|
@ -3444,10 +3438,6 @@ async def _validate_regenerate_key_duration_against_team(
|
|||
return
|
||||
|
||||
team_max_duration = team_table.metadata["team_member_key_duration"]
|
||||
# "-1" on the team side means no limit is enforced
|
||||
if team_max_duration == "-1":
|
||||
return
|
||||
|
||||
team_max_seconds = duration_in_seconds(duration=team_max_duration)
|
||||
|
||||
if user_seconds > team_max_seconds:
|
||||
|
|
|
|||
|
|
@ -678,8 +678,8 @@ async def test_prepare_key_update_data():
|
|||
updated_data = await prepare_key_update_data(data, existing_key_row)
|
||||
assert updated_data["metadata"] is None
|
||||
|
||||
# Test duration "-1" sets expires to None (never expires)
|
||||
data = UpdateKeyRequest(key="test_key", duration="-1")
|
||||
# Test duration=null sets expires to None (never expires)
|
||||
data = UpdateKeyRequest(key="test_key", duration=None)
|
||||
updated_data = await prepare_key_update_data(data, existing_key_row)
|
||||
assert updated_data["expires"] is None
|
||||
|
||||
|
|
|
|||
|
|
@ -1058,7 +1058,7 @@ async def test_update_service_account_works_with_team_id():
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prepare_key_update_data_duration_never_expires():
|
||||
"""Test that duration="-1" sets expires to None (never expires)."""
|
||||
"""Test that duration=null 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,
|
||||
|
|
@ -1076,8 +1076,8 @@ async def test_prepare_key_update_data_duration_never_expires():
|
|||
metadata={},
|
||||
)
|
||||
|
||||
# Test setting duration to "-1" (never expires)
|
||||
update_request = UpdateKeyRequest(key="test-token", duration="-1")
|
||||
# Test setting duration to null (never expires)
|
||||
update_request = UpdateKeyRequest(key="test-token", duration=None)
|
||||
|
||||
result = await prepare_key_update_data(
|
||||
data=update_request, existing_key_row=existing_key
|
||||
|
|
@ -6480,13 +6480,13 @@ class TestCommonKeyGenerationHelperTeamDurationValidation:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_never_expires_exceeds_team_max_raises(self):
|
||||
"""Duration '-1' (never expires) exceeds any finite team max."""
|
||||
"""duration=null (never expires) exceeds any finite team max."""
|
||||
team = MagicMock(spec=LiteLLM_TeamTableCachedObj)
|
||||
team.metadata = {"team_member_key_duration": "30d"}
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await _common_key_generation_helper(
|
||||
data=GenerateKeyRequest(user_id="user-1", duration="-1"),
|
||||
data=GenerateKeyRequest(user_id="user-1", duration=None),
|
||||
user_api_key_dict=UserAPIKeyAuth(
|
||||
user_role=LitellmUserRoles.PROXY_ADMIN, api_key="sk-1"
|
||||
),
|
||||
|
|
@ -6521,8 +6521,8 @@ class TestCommonKeyGenerationHelperTeamDurationValidation:
|
|||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_no_duration_skips_team_validation(self):
|
||||
"""No duration provided — team validation is skipped."""
|
||||
async def test_duration_not_sent_skips_team_validation(self):
|
||||
"""Duration field absent from request — team validation is skipped."""
|
||||
team = MagicMock(spec=LiteLLM_TeamTableCachedObj)
|
||||
team.metadata = {"team_member_key_duration": "5d"}
|
||||
|
||||
|
|
@ -6536,7 +6536,7 @@ class TestCommonKeyGenerationHelperTeamDurationValidation:
|
|||
"litellm.proxy.proxy_server.premium_user", False
|
||||
):
|
||||
await _common_key_generation_helper(
|
||||
data=GenerateKeyRequest(user_id="user-1", duration=None),
|
||||
data=GenerateKeyRequest(user_id="user-1"),
|
||||
user_api_key_dict=UserAPIKeyAuth(
|
||||
user_role=LitellmUserRoles.PROXY_ADMIN, api_key="sk-1"
|
||||
),
|
||||
|
|
@ -6566,10 +6566,10 @@ class TestCommonKeyGenerationHelperTeamDurationValidation:
|
|||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_team_max_minus_one_skips_validation(self):
|
||||
"""team_member_key_duration='-1' means no limit — any user duration is accepted."""
|
||||
async def test_team_max_not_set_skips_validation(self):
|
||||
"""No team_member_key_duration in metadata — any user duration is accepted."""
|
||||
team = MagicMock(spec=LiteLLM_TeamTableCachedObj)
|
||||
team.metadata = {"team_member_key_duration": "-1"}
|
||||
team.metadata = {} # no team_member_key_duration
|
||||
|
||||
with patch(
|
||||
"litellm.proxy.management_endpoints.key_management_endpoints.generate_key_helper_fn",
|
||||
|
|
@ -6669,8 +6669,8 @@ class TestValidateRegenerateKeyDurationAgainstTeam:
|
|||
assert exc_info.value.status_code == 400
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_team_max_duration_minus_one_skips_validation(self):
|
||||
"""team_member_key_duration='-1' means no team limit — skip validation regardless of user duration."""
|
||||
async def test_team_max_not_set_skips_validation(self):
|
||||
"""No team_member_key_duration in metadata — skip validation regardless of user duration."""
|
||||
from litellm.proxy.management_endpoints.key_management_endpoints import (
|
||||
_validate_regenerate_key_duration_against_team,
|
||||
)
|
||||
|
|
@ -6681,7 +6681,7 @@ class TestValidateRegenerateKeyDurationAgainstTeam:
|
|||
data = RegenerateKeyRequest(duration="999d")
|
||||
|
||||
mock_team = MagicMock(spec=LiteLLM_TeamTableCachedObj)
|
||||
mock_team.metadata = {"team_member_key_duration": "-1"}
|
||||
mock_team.metadata = {} # no team_member_key_duration
|
||||
|
||||
mock_prisma = AsyncMock()
|
||||
mock_cache = MagicMock()
|
||||
|
|
@ -6691,7 +6691,7 @@ class TestValidateRegenerateKeyDurationAgainstTeam:
|
|||
new_callable=AsyncMock,
|
||||
return_value=mock_team,
|
||||
):
|
||||
# Should not raise — team max is "-1" (no limit)
|
||||
# Should not raise — no team max set
|
||||
await _validate_regenerate_key_duration_against_team(
|
||||
data=data,
|
||||
key_in_db=mock_key,
|
||||
|
|
@ -6790,7 +6790,7 @@ class TestValidateRegenerateKeyDurationAgainstTeam:
|
|||
|
||||
mock_key = MagicMock(spec=LiteLLM_VerificationToken)
|
||||
mock_key.team_id = "team-123"
|
||||
data = RegenerateKeyRequest(duration="-1")
|
||||
data = RegenerateKeyRequest(duration=None) # null = never expires = infinite
|
||||
|
||||
mock_team = MagicMock(spec=LiteLLM_TeamTableCachedObj)
|
||||
mock_team.metadata = {"team_member_key_duration": "30d"}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue